diff --git a/.github/workflows/martian-issue-triage.yml b/.github/workflows/martian-issue-triage.yml
index 276c7142e..4cc499b85 100644
--- a/.github/workflows/martian-issue-triage.yml
+++ b/.github/workflows/martian-issue-triage.yml
@@ -8,7 +8,8 @@ jobs:
martian-issue-triage:
# For labeled events, verify the labeler is a repo member to prevent privilege escalation
if: |
- (github.event.action == 'opened' && contains(fromJSON('["strawgate", "jlowin"]'), github.actor)) ||
+ (github.event.action == 'opened' && github.actor == 'strawgate') ||
+ (github.event.action == 'opened' && github.actor == 'jlowin' && contains(github.event.issue.labels.*.name, 'bug')) ||
(github.event.action == 'labeled' && github.event.label.name == 'triage-martian' && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.sender.author_association))
concurrency:
diff --git a/.github/workflows/update-config-schema.yml b/.github/workflows/update-config-schema.yml
index 06f63234a..e5d0509f4 100644
--- a/.github/workflows/update-config-schema.yml
+++ b/.github/workflows/update-config-schema.yml
@@ -1,28 +1,28 @@
name: Update MCPServerConfig Schema
-# This workflow runs on merges to main to automatically update the config schema
-# by creating a PR when changes are needed.
+# Regenerates config schema on PRs and commits it back to the branch,
+# so the PR is self-contained and main is correct after merge.
on:
- push:
+ pull_request:
branches: ["main"]
paths:
- "src/fastmcp/utilities/mcp_server_config/**"
- - "!src/fastmcp/utilities/mcp_server_config/v1/schema.json" # Exclude the local schema file
+ - "!src/fastmcp/utilities/mcp_server_config/v1/schema.json"
workflow_dispatch:
permissions:
contents: write
- pull-requests: write
jobs:
update-config-schema:
timeout-minutes: 5
runs-on: ubuntu-latest
+ if: >-
+ github.event_name == 'workflow_dispatch' ||
+ github.event.pull_request.head.repo.full_name == github.repository
steps:
- - uses: actions/checkout@v6
-
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
@@ -30,6 +30,11 @@ jobs:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
+ - uses: actions/checkout@v6
+ with:
+ ref: ${{ github.head_ref || github.ref }}
+ token: ${{ steps.marvin-token.outputs.token }}
+
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
@@ -41,51 +46,22 @@ jobs:
- name: Generate config schema
run: |
- echo "π Generating fastmcp.json schema..."
-
- # Generate schema in docs/public for web access
uv run python -c "
from fastmcp.utilities.mcp_server_config import generate_schema
generate_schema('docs/public/schemas/fastmcp.json/latest.json')
- print('β
Latest schema generated in docs/public')
- "
-
- # Also update the v1 schema in docs/public
- uv run python -c "
- from fastmcp.utilities.mcp_server_config import generate_schema
generate_schema('docs/public/schemas/fastmcp.json/v1.json')
- print('β
v1 schema generated in docs/public')
- "
-
- # Generate schema in the source directory for local development
- uv run python -c "
- from fastmcp.utilities.mcp_server_config import generate_schema
generate_schema('src/fastmcp/utilities/mcp_server_config/v1/schema.json')
- print('β
Schema generated in utilities/mcp_server_config/v1/')
"
- - name: Create Pull Request
- uses: peter-evans/create-pull-request@v8
- with:
- token: ${{ steps.marvin-token.outputs.token }}
- commit-message: "chore: Update fastmcp.json schema"
- title: "chore: Update fastmcp.json schema"
- body: |
- This PR updates the fastmcp.json schema files to match the current source code.
-
- The schema is automatically generated from `src/fastmcp/utilities/mcp_server_config/` to ensure consistency.
-
- **Note:** This PR is fully automated and will update itself with any subsequent changes to the schema, or close automatically if the schema becomes up-to-date through other means. Feel free to leave it open until you're ready to merge.
-
- π€ Generated by Marvin
- branch: marvin/update-config-schema
- labels: |
- ignore in release notes
- delete-branch: true
- author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
- committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
-
- - name: Summary
+ - name: Commit and push if changed
run: |
- echo "β
Config schema generation workflow completed"
- echo "PR will be created if there are changes, or closed if schema is already up to date"
+ git config user.name "marvin-context-protocol[bot]"
+ git config user.email "225465937+marvin-context-protocol[bot]@users.noreply.github.com"
+ git add docs/public/schemas/ src/fastmcp/utilities/mcp_server_config/v1/schema.json
+ if git diff --cached --quiet; then
+ echo "Config schema is up to date"
+ else
+ git commit -m "chore: Update fastmcp.json schema"
+ git push
+ echo "Config schema updated and pushed"
+ fi
diff --git a/.github/workflows/update-sdk-docs.yml b/.github/workflows/update-sdk-docs.yml
index 10baa1e3f..122f6ddfc 100644
--- a/.github/workflows/update-sdk-docs.yml
+++ b/.github/workflows/update-sdk-docs.yml
@@ -1,10 +1,10 @@
name: Update SDK Documentation
-# This workflow runs on merges to main to automatically update SDK docs
-# by creating a PR when changes are needed.
+# Regenerates SDK docs on PRs and commits them back to the branch,
+# so the PR is self-contained and main is correct after merge.
on:
- push:
+ pull_request:
branches: ["main"]
paths:
- "src/**"
@@ -13,16 +13,16 @@ on:
permissions:
contents: write
- pull-requests: write
jobs:
update-sdk-docs:
timeout-minutes: 5
runs-on: ubuntu-latest
+ if: >-
+ github.event_name == 'workflow_dispatch' ||
+ github.event.pull_request.head.repo.full_name == github.repository
steps:
- - uses: actions/checkout@v6
-
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
@@ -30,6 +30,11 @@ jobs:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
+ - uses: actions/checkout@v6
+ with:
+ ref: ${{ github.head_ref || github.ref }}
+ token: ${{ steps.marvin-token.outputs.token }}
+
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
@@ -43,32 +48,17 @@ jobs:
uses: extractions/setup-just@v3
- name: Generate SDK documentation
+ run: just api-ref-all
+
+ - name: Commit and push if changed
run: |
- echo "π Generating SDK documentation..."
- just api-ref-all
-
- - name: Create Pull Request
- uses: peter-evans/create-pull-request@v8
- with:
- token: ${{ steps.marvin-token.outputs.token }}
- commit-message: "chore: Update SDK documentation"
- title: "chore: Update SDK documentation"
- body: |
- This PR updates the auto-generated SDK documentation to reflect the latest source code changes.
-
- π Documentation is automatically generated from the source code docstrings and type annotations.
-
- **Note:** This PR is fully automated and will update itself with any subsequent changes to the SDK, or close automatically if the documentation becomes up-to-date through other means. Feel free to leave it open until you're ready to merge.
-
- π€ Generated by Marvin
- branch: marvin/update-sdk-docs
- labels: |
- ignore in release notes
- delete-branch: true
- author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
- committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
-
- - name: Summary
- run: |
- echo "β
SDK documentation generation workflow completed"
- echo "PR will be created if there are changes, or closed if documentation is already up to date"
+ git config user.name "marvin-context-protocol[bot]"
+ git config user.email "225465937+marvin-context-protocol[bot]@users.noreply.github.com"
+ git add docs/python-sdk/
+ if git diff --cached --quiet; then
+ echo "SDK documentation is up to date"
+ else
+ git commit -m "chore: Update SDK documentation"
+ git push
+ echo "SDK documentation updated and pushed"
+ fi
diff --git a/docs/clients/auth/oauth.mdx b/docs/clients/auth/oauth.mdx
index 25804adc3..84fbe2164 100644
--- a/docs/clients/auth/oauth.mdx
+++ b/docs/clients/auth/oauth.mdx
@@ -55,6 +55,8 @@ You don't need to pass `mcp_url` when using `OAuth` with `Client(auth=...)` β
- **`scopes`** (`str | list[str]`, optional): OAuth scopes to request. Can be space-separated string or list of strings
- **`client_name`** (`str`, optional): Client name for dynamic registration. Defaults to `"FastMCP Client"`
+- **`client_id`** (`str`, optional): Pre-registered OAuth client ID. When provided, skips Dynamic Client Registration entirely. See [Pre-Registered Clients](#pre-registered-clients)
+- **`client_secret`** (`str`, optional): OAuth client secret for pre-registered clients. Optional β public clients that rely on PKCE can omit this
- **`client_metadata_url`** (`str`, optional): URL-based client identity (CIMD). See [CIMD Authentication](/clients/auth/cimd) for details
- **`token_storage`** (`AsyncKeyValue`, optional): Storage backend for persisting OAuth tokens. Defaults to in-memory storage (tokens lost on restart). See [Token Storage](#token-storage) for encrypted storage options
- **`additional_client_metadata`** (`dict[str, Any]`, optional): Extra metadata for client registration
@@ -74,7 +76,7 @@ The client first checks the configured `token_storage` backend for existing, val
If no valid tokens exist, the client attempts to discover the OAuth server's endpoints using a well-known URI (e.g., `/.well-known/oauth-authorization-server`) based on the `mcp_url`.
-If the OAuth server supports it and the client isn't already registered (or credentials aren't cached), the client performs dynamic client registration according to RFC 7591. Alternatively, if a `client_metadata_url` is configured and the server supports CIMD, the client uses its metadata URL as its identity instead of registering.
+If a `client_id` is provided, the client uses those pre-registered credentials directly and skips this step entirely. Otherwise, if a `client_metadata_url` is configured and the server supports CIMD, the client uses its metadata URL as its identity. As a fallback, the client performs Dynamic Client Registration (RFC 7591) if the server supports it.
A temporary local HTTP server is started on an available port (or the port specified via `callback_port`). This server's address (e.g., `http://127.0.0.1:/callback`) acts as the `redirect_uri` for the OAuth flow.
@@ -152,3 +154,33 @@ async with Client(
```
See the [CIMD Authentication](/clients/auth/cimd) page for complete documentation on creating, hosting, and validating CIMD documents.
+
+## Pre-Registered Clients
+
+
+
+Some OAuth servers don't support Dynamic Client Registration β the MCP spec explicitly makes DCR optional. If your client has been pre-registered with the server (you already have a `client_id` and optionally a `client_secret`), you can provide them directly to skip DCR entirely.
+
+```python
+from fastmcp import Client
+from fastmcp.client.auth import OAuth
+
+async with Client(
+ "https://mcp-server.example.com/mcp",
+ auth=OAuth(
+ client_id="my-registered-client-id",
+ client_secret="my-client-secret",
+ ),
+) as client:
+ await client.ping()
+```
+
+Public clients that rely on PKCE for security can omit `client_secret`:
+
+```python
+oauth = OAuth(client_id="my-public-client-id")
+```
+
+
+When using pre-registered credentials, the client will not attempt Dynamic Client Registration. If the server rejects the credentials, the error is surfaced immediately rather than falling back to DCR.
+
diff --git a/docs/clients/generate-cli.mdx b/docs/clients/generate-cli.mdx
index 833637423..4d05e0515 100644
--- a/docs/clients/generate-cli.mdx
+++ b/docs/clients/generate-cli.mdx
@@ -23,7 +23,7 @@ fastmcp generate-cli http://localhost:8000/mcp
fastmcp generate-cli server.py my_weather_cli.py
```
-The second positional argument sets the output path. When omitted, it defaults to `cli.py`. If the file already exists, the command refuses to overwrite unless you pass `-f`:
+The second positional argument sets the output path. When omitted, it defaults to `cli.py`. If either the CLI file or its companion `SKILL.md` already exists, the command refuses to overwrite unless you pass `-f`:
```bash
fastmcp generate-cli weather -f
@@ -85,6 +85,42 @@ Options:
Tool names are preserved exactly as the server defines them β underscores stay as underscores, so `call-tool get_forecast` matches what the server expects.
+## Agent Skill
+
+Alongside the CLI script, `generate-cli` also writes a `SKILL.md` file β a [Claude Code agent skill](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/skills) that documents the generated CLI. The skill includes every tool's exact invocation syntax, parameter flags with types and descriptions, and the utility commands, so an agent can use the CLI immediately without running `--help` or experimenting with flag names.
+
+The skill is written to the same directory as the CLI script. For a weather server, it looks something like:
+
+````markdown
+---
+name: "weather-cli"
+description: "CLI for the weather MCP server. Call tools, list resources, and get prompts."
+---
+
+# weather CLI
+
+## Tool Commands
+
+### get_forecast
+
+Get the weather forecast for a city.
+
+```bash
+uv run --with fastmcp python cli.py call-tool get_forecast --city --days
+```
+
+| Flag | Type | Required | Description |
+|------|------|----------|-------------|
+| `--city` | string | yes | City name |
+| `--days` | integer | no | Number of forecast days |
+````
+
+To skip skill generation, pass `--no-skill`:
+
+```bash
+fastmcp generate-cli weather --no-skill
+```
+
## How It Works
The generated script is a client, not a server. It doesn't bundle or embed the MCP server β it connects to it on every invocation. For URL-based servers, the server needs to be running. For stdio-based servers, the command specified in `CLIENT_SPEC` must be available on the system's `PATH`.
diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx
index f982513d9..2a286961f 100644
--- a/docs/development/v3-notes/v3-features.mdx
+++ b/docs/development/v3-notes/v3-features.mdx
@@ -120,6 +120,29 @@ Key details:
Documentation: [CIMD Authentication](/clients/auth/cimd), [OAuth Proxy CIMD config](/servers/auth/oauth-proxy#cimd-support)
+### Pre-Registered OAuth Clients
+
+The `OAuth` client helper now accepts `client_id` and `client_secret` parameters for servers where the client is already registered ([#3086](https://github.com/jlowin/fastmcp/pull/3086)). This bypasses Dynamic Client Registration entirely β useful when DCR is disabled, or when the server has pre-provisioned credentials for your application.
+
+```python
+from fastmcp import Client
+from fastmcp.client.auth import OAuth
+
+async with Client(
+ "https://mcp-server.example.com/mcp",
+ auth=OAuth(
+ client_id="my-registered-app",
+ client_secret="my-secret",
+ scopes=["read", "write"],
+ ),
+) as client:
+ await client.ping()
+```
+
+The static credentials are injected before the OAuth flow begins, so the client never attempts DCR. If the server rejects the credentials, the error surfaces immediately rather than retrying with fresh registration (which can't help for fixed credentials). Public clients can omit `client_secret`.
+
+Documentation: [Pre-Registered Clients](/clients/auth/oauth#pre-registered-clients)
+
### CLI: `fastmcp generate-cli`
`fastmcp generate-cli` connects to any MCP server, reads its tool schemas, and writes a standalone Python CLI script where every tool becomes a typed subcommand with flags, help text, and tab completion ([#3065](https://github.com/jlowin/fastmcp/pull/3065)). The insight is that MCP tool schemas already contain everything a CLI framework needs β parameter names, types, descriptions, required/optional status β so the generator maps JSON Schema directly into [cyclopts](https://cyclopts.readthedocs.io/) commands.
@@ -203,18 +226,20 @@ The `require_auth` authorization check introduced in beta1 has been removed in f
Support for [MCP Apps](https://modelcontextprotocol.io/specification/2025-06-18/server/apps) β the spec extension that lets MCP servers deliver interactive UIs via sandboxed iframes. Extension negotiation, typed UI metadata on tools and resources, and the `ui://` resource scheme. No component DSL, renderer, or `FastMCPApp` class yet β those are future phases.
-**Registering tools with UI metadata:**
+**Breaking change from beta 2:** The `ui=` parameter on `@mcp.tool()` and `@mcp.resource()` has been renamed to `app=`, and the `ToolUI`/`ResourceUI` classes have been consolidated into a single `AppConfig` class. This follows the established `task=True`/`TaskConfig` pattern. The wire format (`meta["ui"]`, `_meta.ui`) is unchanged.
+
+**Registering tools with app metadata:**
```python
from fastmcp import FastMCP
-from fastmcp.server.apps import ToolUI, ResourceUI, ResourceCSP, ResourcePermissions
+from fastmcp.server.apps import AppConfig, ResourceCSP, ResourcePermissions
mcp = FastMCP("My Server")
# Register the HTML bundle as a ui:// resource with CSP
@mcp.resource(
"ui://my-app/view.html",
- ui=ResourceUI(
+ app=AppConfig(
csp=ResourceCSP(resource_domains=["https://unpkg.com"]),
permissions=ResourcePermissions(clipboard_write={}),
),
@@ -224,17 +249,17 @@ def app_html() -> str:
return Path("./dist/index.html").read_text()
# Tool with UI β clients render an iframe alongside the result
-@mcp.tool(ui=ToolUI(resource_uri="ui://my-app/view.html"))
+@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
async def list_users() -> list[dict]:
return [{"id": "1", "name": "Alice"}]
# App-only tool β visible to the UI but hidden from the model
-@mcp.tool(ui=ToolUI(resource_uri="ui://my-app/view.html", visibility=["app"]))
+@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html", visibility=["app"]))
async def delete_user(id: str) -> dict:
return {"deleted": True}
```
-The `ui=` parameter accepts either a typed model (`ToolUI`, `ResourceUI`) or a raw dict for forward compatibility. It merges into `meta["ui"]` β alongside any other metadata you set.
+The `app=` parameter accepts `True` (enable with defaults), an `AppConfig` instance, or a raw dict for forward compatibility. It merges into `meta["ui"]` β alongside any other metadata you set.
**`ui://` resources** automatically get the correct MIME type (`text/html;profile=mcp-app`) unless you override it explicitly.
@@ -242,9 +267,9 @@ The `ui=` parameter accepts either a typed model (`ToolUI`, `ResourceUI`) or a r
```python
from fastmcp import Context
-from fastmcp.server.apps import ToolUI, UI_EXTENSION_ID
+from fastmcp.server.apps import AppConfig, UI_EXTENSION_ID
-@mcp.tool(ui=ToolUI(resource_uri="ui://dashboard"))
+@mcp.tool(app=AppConfig(resource_uri="ui://dashboard"))
async def dashboard(ctx: Context) -> dict:
data = compute_dashboard()
if ctx.client_supports_extension(UI_EXTENSION_ID):
@@ -253,11 +278,10 @@ async def dashboard(ctx: Context) -> dict:
```
**Key details:**
-- `ToolUI` fields: `resource_uri`, `visibility`, `csp`, `permissions`, `domain`, `prefers_border` (all optional except for typical usage of `resource_uri`)
-- `ResourceUI` fields: `csp`, `permissions`, `domain`, `prefers_border` β metadata for the resource itself when it's a UI bundle
+- `AppConfig` fields: `resource_uri`, `visibility`, `csp`, `permissions`, `domain`, `prefers_border` (all optional). On resources, `resource_uri` and `visibility` are validated as not-applicable and will raise `ValueError` if set.
- `csp` accepts a `ResourceCSP` model with structured domain lists: `connect_domains`, `resource_domains`, `frame_domains`, `base_uri_domains`
- `permissions` accepts a `ResourcePermissions` model: `camera`, `microphone`, `geolocation`, `clipboard_write` (each set to `{}` to request)
-- Both models use `extra="allow"` for forward compatibility with future spec additions
+- `AppConfig` uses `extra="allow"` for forward compatibility with future spec additions
- Models use Pydantic aliases for wire format (`resourceUri`, `prefersBorder`, `connectDomains`, `clipboardWrite`)
- Resource metadata (including CSP/permissions) is propagated to `resources/read` response content items so hosts can read it when rendering the iframe
- `ctx.client_supports_extension(id)` is a general-purpose method β works for any extension, not just MCP Apps
diff --git a/docs/integrations/azure.mdx b/docs/integrations/azure.mdx
index a5b316d88..4376a38ce 100644
--- a/docs/integrations/azure.mdx
+++ b/docs/integrations/azure.mdx
@@ -326,3 +326,135 @@ mcp = FastMCP(name="Azure MI App", auth=auth)
For Azure Government, pass `base_authority="login.microsoftonline.us"` to `AzureJWTVerifier`.
+
+## On-Behalf-Of (OBO)
+
+
+
+The On-Behalf-Of (OBO) flow allows your FastMCP server to call downstream Microsoft APIsβlike Microsoft Graphβusing the authenticated user's identity. When a user authenticates to your MCP server, you receive a token for your API. OBO exchanges that token for a new token that can call other services, maintaining the user's identity and permissions throughout the chain.
+
+This pattern is useful when your tools need to access user-specific data from Microsoft services: reading emails, accessing calendar events, querying SharePoint, or any other Graph API operation that requires user context.
+
+
+OBO features require the `azure` extra:
+
+```bash
+pip install 'fastmcp[azure]'
+```
+
+
+### Azure Portal Setup
+
+OBO requires additional configuration in your Azure App registration beyond basic authentication.
+
+
+
+ In your App registration, navigate to **API permissions** and add the Microsoft Graph permissions your tools will need.
+
+ - Click **Add a permission** β **Microsoft Graph** β **Delegated permissions**
+ - Select the permissions required for your use case (e.g., `Mail.Read`, `Calendars.Read`, `User.Read`)
+ - Repeat for any other APIs you need to call
+
+
+ Only add delegated permissions for OBO. Application permissions bypass user context entirely and are inappropriate for the OBO flow.
+
+
+
+
+ OBO requires admin consent for the permissions you've added. In the **API permissions** page, click **Grant admin consent for [Your Organization]**.
+
+ Without admin consent, OBO token exchanges will fail with an `AADSTS65001` error indicating the user or administrator hasn't consented to use the application.
+
+
+ For development, you can grant consent for just your own account. For production, an Azure AD administrator must grant tenant-wide consent.
+
+
+
+
+### Configure AzureProvider for OBO
+
+The `additional_authorize_scopes` parameter tells Azure which downstream API permissions to include during the initial authorization. These scopes establish what your server can request through OBO later.
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.azure import AzureProvider
+
+auth_provider = AzureProvider(
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+ tenant_id="your-tenant-id",
+ base_url="http://localhost:8000",
+ required_scopes=["mcp-access"], # Your API scope
+ # Include Graph scopes for OBO
+ additional_authorize_scopes=[
+ "https://graph.microsoft.com/Mail.Read",
+ "https://graph.microsoft.com/User.Read",
+ "offline_access", # Enables refresh tokens
+ ],
+)
+
+mcp = FastMCP(name="Graph-Enabled Server", auth=auth_provider)
+```
+
+Scopes listed in `additional_authorize_scopes` are requested during the initial OAuth flow but aren't validated on incoming tokens. They establish permission for your server to later exchange the user's token for downstream API access.
+
+
+Use fully-qualified scope URIs for downstream APIs (e.g., `https://graph.microsoft.com/Mail.Read`). Short forms like `Mail.Read` work for authorization requests, but fully-qualified URIs are clearer and avoid ambiguity.
+
+
+### EntraOBOToken Dependency
+
+The `EntraOBOToken` dependency handles the complete OBO flow automatically. Declare it as a parameter default with the scopes you need, and FastMCP exchanges the user's token for a downstream API token before your function runs.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.azure import AzureProvider, EntraOBOToken
+import httpx
+
+auth_provider = AzureProvider(
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+ tenant_id="your-tenant-id",
+ base_url="http://localhost:8000",
+ required_scopes=["mcp-access"],
+ additional_authorize_scopes=[
+ "https://graph.microsoft.com/Mail.Read",
+ "https://graph.microsoft.com/User.Read",
+ ],
+)
+
+mcp = FastMCP(name="Email Reader", auth=auth_provider)
+
+@mcp.tool
+async def get_recent_emails(
+ count: int = 10,
+ graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"]),
+) -> list[dict]:
+ """Get the user's recent emails from Microsoft Graph."""
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ f"https://graph.microsoft.com/v1.0/me/messages?$top={count}",
+ headers={"Authorization": f"Bearer {graph_token}"},
+ )
+ response.raise_for_status()
+ data = response.json()
+
+ return [
+ {"subject": msg["subject"], "from": msg["from"]["emailAddress"]["address"]}
+ for msg in data.get("value", [])
+ ]
+```
+
+The `graph_token` parameter receives a ready-to-use access token for Microsoft Graph. FastMCP handles the OBO exchange transparentlyβyour function just uses the token to call the API.
+
+
+**Scope alignment is critical.** The scopes passed to `EntraOBOToken` must be a subset of the scopes in `additional_authorize_scopes`. If you request a scope during OBO that wasn't included in the initial authorization, the exchange will fail.
+
+
+
+For advanced OBO scenarios, use `CurrentAccessToken()` to get the user's token, then construct an `azure.identity.aio.OnBehalfOfCredential` directly with your Azure credentials.
+
+
+
+For a complete working example of Azure OBO with FastMCP, see [Pamela Fox's blog post on OBO flow for Entra-based MCP servers](https://blog.pamelafox.org/2026/01/using-on-behalf-of-flow-for-entra-based.html).
+
diff --git a/docs/python-sdk/fastmcp-cli-generate.mdx b/docs/python-sdk/fastmcp-cli-generate.mdx
index 28bd3ea7f..027894e38 100644
--- a/docs/python-sdk/fastmcp-cli-generate.mdx
+++ b/docs/python-sdk/fastmcp-cli-generate.mdx
@@ -6,7 +6,7 @@ sidebarTitle: generate
# `fastmcp.cli.generate`
-Generate a standalone CLI script from an MCP server's capabilities.
+Generate a standalone CLI script and agent skill from an MCP server.
## Functions
@@ -33,7 +33,17 @@ generate_cli_script(server_name: str, server_spec: str, transport_code: str, ext
Generate the full CLI script source code.
-### `generate_cli_command`
+### `generate_skill_content`
+
+```python
+generate_skill_content(server_name: str, cli_filename: str, tools: list[mcp.types.Tool]) -> str
+```
+
+
+Generate a SKILL.md file for a generated CLI script.
+
+
+### `generate_cli_command`
```python
generate_cli_command(server_spec: Annotated[str, cyclopts.Parameter(help='Server URL, Python file, MCPConfig JSON, discovered name, or .js file')], output: Annotated[str, cyclopts.Parameter(help='Output file path (default: cli.py)')] = 'cli.py') -> None
@@ -43,7 +53,8 @@ generate_cli_command(server_spec: Annotated[str, cyclopts.Parameter(help='Server
Generate a standalone CLI script from an MCP server.
Connects to the server, reads its tools/resources/prompts, and writes
-a Python script that can invoke them directly.
+a Python script that can invoke them directly. Also generates a SKILL.md
+agent skill file unless --no-skill is passed.
**Examples:**
@@ -51,4 +62,5 @@ fastmcp generate-cli weather
fastmcp generate-cli weather my_cli.py
fastmcp generate-cli http://localhost:8000/mcp
fastmcp generate-cli server.py output.py -f
+fastmcp generate-cli weather --no-skill
diff --git a/docs/python-sdk/fastmcp-client-auth-oauth.mdx b/docs/python-sdk/fastmcp-client-auth-oauth.mdx
index b9d06beb7..15ce6e8af 100644
--- a/docs/python-sdk/fastmcp-client-auth-oauth.mdx
+++ b/docs/python-sdk/fastmcp-client-auth-oauth.mdx
@@ -73,7 +73,7 @@ a browser for user authorization and running a local callback server.
**Methods:**
-#### `redirect_handler`
+#### `redirect_handler`
```python
redirect_handler(self, authorization_url: str) -> None
@@ -82,7 +82,7 @@ redirect_handler(self, authorization_url: str) -> None
Open browser for authorization, with pre-flight check for invalid client.
-#### `callback_handler`
+#### `callback_handler`
```python
callback_handler(self) -> tuple[str, str | None]
@@ -91,7 +91,7 @@ callback_handler(self) -> tuple[str, str | None]
Handle OAuth callback and return (auth_code, state).
-#### `async_auth_flow`
+#### `async_auth_flow`
```python
async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]
diff --git a/docs/python-sdk/fastmcp-server-apps.mdx b/docs/python-sdk/fastmcp-server-apps.mdx
index c72052339..ad006b492 100644
--- a/docs/python-sdk/fastmcp-server-apps.mdx
+++ b/docs/python-sdk/fastmcp-server-apps.mdx
@@ -15,17 +15,17 @@ UI metadata for clients that support interactive app rendering.
## Functions
-### `ui_to_meta_dict`
+### `app_config_to_meta_dict`
```python
-ui_to_meta_dict(ui: ToolUI | ResourceUI | dict[str, Any]) -> dict[str, Any]
+app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any]
```
-Convert a UI model or dict to the wire-format dict for ``meta["ui"]``.
+Convert an AppConfig or dict to the wire-format dict for ``meta["ui"]``.
-### `resolve_ui_mime_type`
+### `resolve_ui_mime_type`
```python
resolve_ui_mime_type(uri: str, explicit_mime_type: str | None) -> str | None
@@ -70,18 +70,17 @@ iframe. Hosts MAY honour these; apps should use JS feature detection
as a fallback.
-### `ToolUI`
+### `AppConfig`
-Typed ``_meta.ui`` for tools β links a tool to its UI resource.
+Configuration for MCP App tools and resources.
+
+Controls how a tool or resource participates in the MCP Apps extension.
+On tools, ``resource_uri`` and ``visibility`` specify which UI resource
+to render and where the tool appears. On resources, those fields must
+be left unset (the resource itself is the UI).
All fields use ``exclude_none`` serialization so only explicitly-set
values appear on the wire. Aliases match the MCP Apps wire format
(camelCase).
-
-### `ResourceUI`
-
-
-Typed ``_meta.ui`` for resources β rendering hints for UI-capable clients.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
index 2e403a611..e5773c6fd 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
@@ -12,9 +12,38 @@ This provider implements Azure/Microsoft Entra ID OAuth authentication
using the OAuth Proxy pattern for non-DCR OAuth flows.
+## Functions
+
+### `EntraOBOToken`
+
+```python
+EntraOBOToken(scopes: list[str]) -> str
+```
+
+
+Exchange the user's Entra token for a downstream API token via OBO.
+
+This dependency performs a Microsoft Entra On-Behalf-Of (OBO) token exchange,
+allowing your MCP server to call downstream APIs (like Microsoft Graph) on
+behalf of the authenticated user.
+
+**Args:**
+- `scopes`: The scopes to request for the downstream API. For Microsoft Graph,
+use scopes like ["https\://graph.microsoft.com/Mail.Read"] or
+["https\://graph.microsoft.com/.default"].
+
+**Returns:**
+- A dependency that resolves to the downstream API access token string
+
+**Raises:**
+- `ImportError`: If fastmcp[azure] is not installed
+- `RuntimeError`: If no access token is available, provider is not Azure,
+or OBO exchange fails
+
+
## Classes
-### `AzureProvider`
+### `AzureProvider`
Azure (Microsoft Entra) OAuth provider for FastMCP.
@@ -49,7 +78,7 @@ Setup:
**Methods:**
-#### `authorize`
+#### `authorize`
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@@ -69,7 +98,29 @@ scopes to determine the resource/audience instead of a separate parameter.
- Authorization URL to redirect the user to Azure AD
-### `AzureJWTVerifier`
+#### `create_obo_credential`
+
+```python
+create_obo_credential(self, user_assertion: str) -> OnBehalfOfCredential
+```
+
+Create an OnBehalfOfCredential for OBO token exchange.
+
+Uses the AzureProvider's configuration (client_id, client_secret,
+tenant_id, authority) to create a credential that can exchange the
+user's token for downstream API tokens.
+
+**Args:**
+- `user_assertion`: The user's access token to exchange via OBO.
+
+**Returns:**
+- A configured OnBehalfOfCredential ready for get_token() calls.
+
+**Raises:**
+- `ImportError`: If azure-identity is not installed (requires fastmcp[azure]).
+
+
+### `AzureJWTVerifier`
JWT verifier pre-configured for Azure AD / Microsoft Entra ID.
@@ -106,7 +157,7 @@ Example::
**Methods:**
-#### `scopes_supported`
+#### `scopes_supported`
```python
scopes_supported(self) -> list[str]
diff --git a/docs/python-sdk/fastmcp-server-context.mdx b/docs/python-sdk/fastmcp-server-context.mdx
index 060f0c4d5..19d67e5d6 100644
--- a/docs/python-sdk/fastmcp-server-context.mdx
+++ b/docs/python-sdk/fastmcp-server-context.mdx
@@ -465,6 +465,12 @@ in the step for manual execution.
- `mask_error_details`: If True, mask detailed error messages from tool
execution. When None (default), uses the global settings value.
Tools can raise ToolError to bypass masking.
+- `tool_concurrency`: Controls parallel execution of tools\:
+- None (default)\: Sequential execution (one at a time)
+- 0\: Unlimited parallel execution
+- N > 0\: Execute at most N tools concurrently
+If any tool has sequential=True, all tools execute sequentially
+regardless of this setting.
**Returns:**
- SampleStep containing:
@@ -475,7 +481,7 @@ Tools can raise ToolError to bypass masking.
- - .text: The text content (if any)
-#### `sample`
+#### `sample`
```python
sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT]
@@ -484,7 +490,7 @@ sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[
Overload: With result_type, returns SamplingResult[ResultT].
-#### `sample`
+#### `sample`
```python
sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[str]
@@ -493,7 +499,7 @@ sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[
Overload: Without result_type, returns SamplingResult[str].
-#### `sample`
+#### `sample`
```python
sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT] | SamplingResult[str]
@@ -527,6 +533,12 @@ response is validated against this type.
- `mask_error_details`: If True, mask detailed error messages from tool
execution. When None (default), uses the global settings value.
Tools can raise ToolError to bypass masking.
+- `tool_concurrency`: Controls parallel execution of tools\:
+- None (default)\: Sequential execution (one at a time)
+- 0\: Unlimited parallel execution
+- N > 0\: Execute at most N tools concurrently
+If any tool has sequential=True, all tools execute sequentially
+regardless of this setting.
**Returns:**
- SamplingResult[T] containing:
@@ -535,43 +547,43 @@ Tools can raise ToolError to bypass masking.
- - .history: All messages exchanged during sampling
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: None) -> AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: type[T]) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: list[str]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: dict[str, dict[str, str]]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: list[list[str]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: list[dict[str, dict[str, str]]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: type[T] | list[str] | dict[str, dict[str, str]] | list[list[str]] | list[dict[str, dict[str, str]]] | None = None) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
@@ -600,7 +612,7 @@ type or dataclass or BaseModel. If it is a primitive type, an
object schema with a single "value" field will be generated.
-#### `set_state`
+#### `set_state`
```python
set_state(self, key: str, value: Any) -> None
@@ -613,7 +625,7 @@ The key is automatically prefixed with the session identifier.
State expires after 1 day to prevent unbounded memory growth.
-#### `get_state`
+#### `get_state`
```python
get_state(self, key: str) -> Any
@@ -624,7 +636,7 @@ Get a value from the session-scoped state store.
Returns None if the key is not found.
-#### `delete_state`
+#### `delete_state`
```python
delete_state(self, key: str) -> None
@@ -633,7 +645,7 @@ delete_state(self, key: str) -> None
Delete a value from the session-scoped state store.
-#### `enable_components`
+#### `enable_components`
```python
enable_components(self) -> None
@@ -657,7 +669,7 @@ ResourceListChangedNotification, and PromptListChangedNotification.
- `match_all`: If True, matches all components regardless of other criteria.
-#### `disable_components`
+#### `disable_components`
```python
disable_components(self) -> None
@@ -681,7 +693,7 @@ ResourceListChangedNotification, and PromptListChangedNotification.
- `match_all`: If True, matches all components regardless of other criteria.
-#### `reset_visibility`
+#### `reset_visibility`
```python
reset_visibility(self) -> None
diff --git a/docs/python-sdk/fastmcp-server-dependencies.mdx b/docs/python-sdk/fastmcp-server-dependencies.mdx
index b066c8c8e..496a19e8c 100644
--- a/docs/python-sdk/fastmcp-server-dependencies.mdx
+++ b/docs/python-sdk/fastmcp-server-dependencies.mdx
@@ -15,7 +15,7 @@ CurrentWorker) and background task execution require fastmcp[tasks].
## Functions
-### `get_task_context`
+### `get_task_context`
```python
get_task_context() -> TaskContextInfo | None
@@ -31,7 +31,7 @@ Returns None if not running in a task context (e.g., foreground execution).
- TaskContextInfo with task_id and session_id, or None if not in a task.
-### `register_task_session`
+### `register_task_session`
```python
register_task_session(session_id: str, session: ServerSession) -> None
@@ -49,7 +49,7 @@ client disconnects.
- `session`: The ServerSession instance
-### `get_task_session`
+### `get_task_session`
```python
get_task_session(session_id: str) -> ServerSession | None
@@ -65,7 +65,7 @@ Get a registered session by ID if still alive.
- The ServerSession if found and alive, None otherwise
-### `is_docket_available`
+### `is_docket_available`
```python
is_docket_available() -> bool
@@ -75,7 +75,7 @@ is_docket_available() -> bool
Check if pydocket is installed.
-### `require_docket`
+### `require_docket`
```python
require_docket(feature: str) -> None
@@ -89,7 +89,7 @@ Raise ImportError with install instructions if docket not available.
"CurrentDocket()"). Will be included in the error message.
-### `transform_context_annotations`
+### `transform_context_annotations`
```python
transform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any]
@@ -115,7 +115,7 @@ allows them to have defaults in any order.
- Function with modified signature (same function object, updated __signature__)
-### `get_context`
+### `get_context`
```python
get_context() -> Context
@@ -125,7 +125,7 @@ get_context() -> Context
Get the current FastMCP Context instance directly.
-### `get_server`
+### `get_server`
```python
get_server() -> FastMCP
@@ -141,7 +141,7 @@ Get the current FastMCP server instance directly.
- `RuntimeError`: If no server in context
-### `get_http_request`
+### `get_http_request`
```python
get_http_request() -> Request
@@ -153,7 +153,7 @@ Get the current HTTP request.
Tries MCP SDK's request_ctx first, then falls back to FastMCP's HTTP context.
-### `get_http_headers`
+### `get_http_headers`
```python
get_http_headers(include_all: bool = False) -> dict[str, str]
@@ -169,7 +169,7 @@ By default, strips problematic headers like `content-length` that cause issues
if forwarded to downstream clients. If `include_all` is True, all headers are returned.
-### `get_access_token`
+### `get_access_token`
```python
get_access_token() -> AccessToken | None
@@ -187,7 +187,7 @@ request is available.
- The access token if an authenticated user is available, None otherwise.
-### `without_injected_parameters`
+### `without_injected_parameters`
```python
without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]
@@ -212,7 +212,7 @@ Handles:
- Async wrapper function without injected parameters
-### `resolve_dependencies`
+### `resolve_dependencies`
```python
resolve_dependencies(fn: Callable[..., Any], arguments: dict[str, Any]) -> AsyncGenerator[dict[str, Any], None]
@@ -238,7 +238,7 @@ time, so all injection goes through the unified DI system.
which will be filtered out)
-### `CurrentContext`
+### `CurrentContext`
```python
CurrentContext() -> Context
@@ -257,7 +257,7 @@ current MCP operation (tool/resource/prompt call).
- `RuntimeError`: If no active context found (during resolution)
-### `CurrentDocket`
+### `CurrentDocket`
```python
CurrentDocket() -> Docket
@@ -277,7 +277,7 @@ automatically creates for background task scheduling.
- `ImportError`: If fastmcp[tasks] not installed
-### `CurrentWorker`
+### `CurrentWorker`
```python
CurrentWorker() -> Worker
@@ -297,7 +297,7 @@ automatically creates for background task processing.
- `ImportError`: If fastmcp[tasks] not installed
-### `CurrentFastMCP`
+### `CurrentFastMCP`
```python
CurrentFastMCP() -> FastMCP
@@ -315,7 +315,7 @@ This dependency provides access to the active FastMCP server.
- `RuntimeError`: If no server in context (during resolution)
-### `CurrentRequest`
+### `CurrentRequest`
```python
CurrentRequest() -> Request
@@ -335,7 +335,7 @@ current HTTP request. Only available when running over HTTP transports
- `RuntimeError`: If no HTTP request in context (e.g., STDIO transport)
-### `CurrentHeaders`
+### `CurrentHeaders`
```python
CurrentHeaders() -> dict[str, str]
@@ -352,7 +352,7 @@ safe to use in code that might run over any transport.
- A dependency that resolves to a dictionary of header name -> value
-### `CurrentAccessToken`
+### `CurrentAccessToken`
```python
CurrentAccessToken() -> AccessToken
@@ -371,9 +371,32 @@ authenticated request. Raises an error if no authentication is present.
- `RuntimeError`: If no authenticated user (use get_access_token() for optional)
+### `TokenClaim`
+
+```python
+TokenClaim(name: str) -> str
+```
+
+
+Get a specific claim from the access token.
+
+This dependency extracts a single claim value from the current access token.
+It's useful for getting user identifiers, roles, or other token claims
+without needing the full token object.
+
+**Args:**
+- `name`: The name of the claim to extract (e.g., "oid", "sub", "email")
+
+**Returns:**
+- A dependency that resolves to the claim value as a string
+
+**Raises:**
+- `RuntimeError`: If no access token is available or claim is missing
+
+
## Classes
-### `TaskContextInfo`
+### `TaskContextInfo`
Information about the current background task context.
@@ -382,7 +405,7 @@ Returned by ``get_task_context()`` when running inside a Docket worker.
Contains identifiers needed to communicate with the MCP session.
-### `ProgressLike`
+### `ProgressLike`
Protocol for progress tracking interface.
@@ -393,7 +416,7 @@ and Docket's Progress (worker context).
**Methods:**
-#### `current`
+#### `current`
```python
current(self) -> int | None
@@ -402,7 +425,7 @@ current(self) -> int | None
Current progress value.
-#### `total`
+#### `total`
```python
total(self) -> int
@@ -411,7 +434,7 @@ total(self) -> int
Total/target progress value.
-#### `message`
+#### `message`
```python
message(self) -> str | None
@@ -420,7 +443,7 @@ message(self) -> str | None
Current progress message.
-#### `set_total`
+#### `set_total`
```python
set_total(self, total: int) -> None
@@ -429,7 +452,7 @@ set_total(self, total: int) -> None
Set the total/target value for progress tracking.
-#### `increment`
+#### `increment`
```python
increment(self, amount: int = 1) -> None
@@ -438,7 +461,7 @@ increment(self, amount: int = 1) -> None
Atomically increment the current progress value.
-#### `set_message`
+#### `set_message`
```python
set_message(self, message: str | None) -> None
@@ -447,7 +470,7 @@ set_message(self, message: str | None) -> None
Update the progress status message.
-### `InMemoryProgress`
+### `InMemoryProgress`
In-memory progress tracker for immediate tool execution.
@@ -459,25 +482,25 @@ progress doesn't need to be observable across processes.
**Methods:**
-#### `current`
+#### `current`
```python
current(self) -> int | None
```
-#### `total`
+#### `total`
```python
total(self) -> int
```
-#### `message`
+#### `message`
```python
message(self) -> str | None
```
-#### `set_total`
+#### `set_total`
```python
set_total(self, total: int) -> None
@@ -486,7 +509,7 @@ set_total(self, total: int) -> None
Set the total/target value for progress tracking.
-#### `increment`
+#### `increment`
```python
increment(self, amount: int = 1) -> None
@@ -495,7 +518,7 @@ increment(self, amount: int = 1) -> None
Atomically increment the current progress value.
-#### `set_message`
+#### `set_message`
```python
set_message(self, message: str | None) -> None
@@ -504,7 +527,7 @@ set_message(self, message: str | None) -> None
Update the progress status message.
-### `Progress`
+### `Progress`
FastMCP Progress dependency that works in both server and worker contexts.
diff --git a/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx b/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx
index fd501251e..bfb3ccea1 100644
--- a/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx
@@ -37,19 +37,19 @@ Add a tool to this provider's storage.
Accepts either a Tool object or a decorated function with __fastmcp__ metadata.
-#### `tool`
+#### `tool`
```python
tool(self: LocalProvider, name_or_fn: AnyFunction) -> FunctionTool
```
-#### `tool`
+#### `tool`
```python
tool(self: LocalProvider, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool]
```
-#### `tool`
+#### `tool`
```python
tool(self: LocalProvider, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]
diff --git a/docs/python-sdk/fastmcp-server-sampling-run.mdx b/docs/python-sdk/fastmcp-server-sampling-run.mdx
index a251946ff..a28542f28 100644
--- a/docs/python-sdk/fastmcp-server-sampling-run.mdx
+++ b/docs/python-sdk/fastmcp-server-sampling-run.mdx
@@ -10,7 +10,7 @@ Sampling types and helper functions for FastMCP servers.
## Functions
-### `determine_handler_mode`
+### `determine_handler_mode`
```python
determine_handler_mode(context: Context, needs_tools: bool) -> bool
@@ -30,7 +30,7 @@ Determine whether to use fallback handler or client for sampling.
- `ValueError`: If client lacks required capability and no fallback configured.
-### `call_sampling_handler`
+### `call_sampling_handler`
```python
call_sampling_handler(context: Context, messages: list[SamplingMessage]) -> CreateMessageResult | CreateMessageResultWithTools
@@ -44,10 +44,10 @@ sampling_handler is set via determine_handler_mode(). The checks below are
safeguards against internal misuse.
-### `execute_tools`
+### `execute_tools`
```python
-execute_tools(tool_calls: list[ToolUseContent], tool_map: dict[str, SamplingTool], mask_error_details: bool = False) -> list[ToolResultContent]
+execute_tools(tool_calls: list[ToolUseContent], tool_map: dict[str, SamplingTool], mask_error_details: bool = False, tool_concurrency: int | None = None) -> list[ToolResultContent]
```
@@ -60,12 +60,18 @@ Execute tool calls and return results.
When masked, only generic error messages are returned to the LLM.
Tools can explicitly raise ToolError to bypass masking when they want
to provide specific error messages to the LLM.
+- `tool_concurrency`: Controls parallel execution of tools\:
+- None (default)\: Sequential execution (one at a time)
+- 0\: Unlimited parallel execution
+- N > 0\: Execute at most N tools concurrently
+If any tool has sequential=True, all tools execute sequentially
+regardless of this setting.
**Returns:**
-- List of tool result content blocks.
+- List of tool result content blocks in the same order as tool_calls.
-### `prepare_messages`
+### `prepare_messages`
```python
prepare_messages(messages: str | Sequence[str | SamplingMessage]) -> list[SamplingMessage]
@@ -75,7 +81,7 @@ prepare_messages(messages: str | Sequence[str | SamplingMessage]) -> list[Sampli
Convert various message formats to a list of SamplingMessage objects.
-### `prepare_tools`
+### `prepare_tools`
```python
prepare_tools(tools: Sequence[SamplingTool | Callable[..., Any]] | None) -> list[SamplingTool] | None
@@ -85,7 +91,7 @@ prepare_tools(tools: Sequence[SamplingTool | Callable[..., Any]] | None) -> list
Convert tools to SamplingTool objects.
-### `extract_tool_calls`
+### `extract_tool_calls`
```python
extract_tool_calls(response: CreateMessageResult | CreateMessageResultWithTools) -> list[ToolUseContent]
@@ -95,7 +101,7 @@ extract_tool_calls(response: CreateMessageResult | CreateMessageResultWithTools)
Extract tool calls from a response.
-### `create_final_response_tool`
+### `create_final_response_tool`
```python
create_final_response_tool(result_type: type) -> SamplingTool
@@ -108,7 +114,7 @@ This tool is used to capture structured responses from the LLM.
The tool's schema is derived from the result_type.
-### `sample_step_impl`
+### `sample_step_impl`
```python
sample_step_impl(context: Context, messages: str | Sequence[str | SamplingMessage]) -> SampleStep
@@ -121,7 +127,7 @@ Make a single LLM sampling call. This is a stateless function that makes
exactly one LLM call and optionally executes any requested tools.
-### `sample_impl`
+### `sample_impl`
```python
sample_impl(context: Context, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT]
@@ -137,7 +143,7 @@ provides a final text response.
## Classes
-### `SamplingResult`
+### `SamplingResult`
Result of a sampling operation.
@@ -148,7 +154,7 @@ Result of a sampling operation.
- `history`: All messages exchanged during sampling.
-### `SampleStep`
+### `SampleStep`
Result of a single sampling call.
@@ -158,7 +164,7 @@ Represents what the LLM returned in this step plus the message history.
**Methods:**
-#### `is_tool_use`
+#### `is_tool_use`
```python
is_tool_use(self) -> bool
@@ -167,7 +173,7 @@ is_tool_use(self) -> bool
True if the LLM is requesting tool execution.
-#### `text`
+#### `text`
```python
text(self) -> str | None
@@ -176,7 +182,7 @@ text(self) -> str | None
Extract text from the response, if available.
-#### `tool_calls`
+#### `tool_calls`
```python
tool_calls(self) -> list[ToolUseContent]
diff --git a/docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx b/docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx
index 1231624f5..15941b0dc 100644
--- a/docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx
+++ b/docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx
@@ -37,7 +37,7 @@ Create a SamplingTool explicitly when you need custom name/description:
**Methods:**
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any] | None = None) -> Any
@@ -52,7 +52,7 @@ Execute the tool with the given arguments.
- The result of executing the tool function.
-#### `from_function`
+#### `from_function`
```python
from_function(cls, fn: Callable[..., Any]) -> SamplingTool
@@ -67,6 +67,10 @@ the tool's parameters. Type hints are used to determine parameter types.
- `fn`: The function to create a tool from.
- `name`: Optional name override. Defaults to the function's name.
- `description`: Optional description override. Defaults to the function's docstring.
+- `sequential`: If True, this tool requires sequential execution and prevents
+parallel execution of all tools in the batch. Set to True for tools
+with shared state, file writes, or other operations that cannot run
+concurrently. Defaults to False.
**Returns:**
- A SamplingTool wrapping the function.
diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx
index 004fc53c8..56bfd7696 100644
--- a/docs/python-sdk/fastmcp-server-server.mdx
+++ b/docs/python-sdk/fastmcp-server-server.mdx
@@ -10,7 +10,7 @@ FastMCP - A more ergonomic interface for MCP servers.
## Functions
-### `default_lifespan`
+### `default_lifespan`
```python
default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]
@@ -26,7 +26,7 @@ Default lifespan context manager that does nothing.
- An empty dictionary as the lifespan result.
-### `create_proxy`
+### `create_proxy`
```python
create_proxy(target: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
@@ -54,65 +54,65 @@ use `FastMCPProxy` or `ProxyProvider` directly from `fastmcp.server.providers.pr
## Classes
-### `StateValue`
+### `StateValue`
Wrapper for stored context state values.
-### `FastMCP`
+### `FastMCP`
**Methods:**
-#### `settings`
+#### `settings`
```python
settings(self) -> Settings
```
-#### `name`
+#### `name`
```python
name(self) -> str
```
-#### `instructions`
+#### `instructions`
```python
instructions(self) -> str | None
```
-#### `instructions`
+#### `instructions`
```python
instructions(self, value: str | None) -> None
```
-#### `version`
+#### `version`
```python
version(self) -> str | None
```
-#### `website_url`
+#### `website_url`
```python
website_url(self) -> str | None
```
-#### `icons`
+#### `icons`
```python
icons(self) -> list[mcp.types.Icon]
```
-#### `add_middleware`
+#### `add_middleware`
```python
add_middleware(self, middleware: Middleware) -> None
```
-#### `add_provider`
+#### `add_provider`
```python
add_provider(self, provider: Provider) -> None
@@ -132,7 +132,7 @@ always take precedence over providers.
- Prompts become "namespace_promptname"
-#### `get_tasks`
+#### `get_tasks`
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@@ -144,7 +144,7 @@ Overrides AggregateProvider.get_tasks() to apply server-level transforms
after aggregation. AggregateProvider handles provider-level namespacing.
-#### `add_transform`
+#### `add_transform`
```python
add_transform(self, transform: Transform) -> None
@@ -159,7 +159,7 @@ They transform tools, resources, and prompts from ALL providers.
- `transform`: The transform to add.
-#### `add_tool_transformation`
+#### `add_tool_transformation`
```python
add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfig) -> None
@@ -171,7 +171,7 @@ Add a tool transformation.
Use ``add_transform(ToolTransform({...}))`` instead.
-#### `remove_tool_transformation`
+#### `remove_tool_transformation`
```python
remove_tool_transformation(self, _tool_name: str) -> None
@@ -183,7 +183,7 @@ Remove a tool transformation.
Tool transformations are now immutable. Use enable/disable controls instead.
-#### `list_tools`
+#### `list_tools`
```python
list_tools(self) -> Sequence[Tool]
@@ -196,7 +196,7 @@ and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
-#### `get_tool`
+#### `get_tool`
```python
get_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None
@@ -216,7 +216,7 @@ session transforms can override provider-level disables.
- The tool if found and enabled, None otherwise.
-#### `list_resources`
+#### `list_resources`
```python
list_resources(self) -> Sequence[Resource]
@@ -229,7 +229,7 @@ and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
-#### `get_resource`
+#### `get_resource`
```python
get_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | None
@@ -248,7 +248,7 @@ transforms (including session-level) have been applied.
- The resource if found and enabled, None otherwise.
-#### `list_resource_templates`
+#### `list_resource_templates`
```python
list_resource_templates(self) -> Sequence[ResourceTemplate]
@@ -261,7 +261,7 @@ auth filtering, and middleware execution. Returns all versions (no deduplication
Protocol handlers deduplicate for MCP wire format.
-#### `get_resource_template`
+#### `get_resource_template`
```python
get_resource_template(self, uri: str, version: VersionSpec | None = None) -> ResourceTemplate | None
@@ -280,7 +280,7 @@ all transforms (including session-level) have been applied.
- The template if found and enabled, None otherwise.
-#### `list_prompts`
+#### `list_prompts`
```python
list_prompts(self) -> Sequence[Prompt]
@@ -293,7 +293,7 @@ and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
-#### `get_prompt`
+#### `get_prompt`
```python
get_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None
@@ -312,19 +312,19 @@ transforms (including session-level) have been applied.
- The prompt if found and enabled, None otherwise.
-#### `call_tool`
+#### `call_tool`
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult
```
-#### `call_tool`
+#### `call_tool`
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult
```
-#### `call_tool`
+#### `call_tool`
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult | mcp.types.CreateTaskResult
@@ -354,19 +354,19 @@ return ToolResult.
- `ValidationError`: If arguments fail validation
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self, uri: str) -> ResourceResult
```
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self, uri: str) -> mcp.types.CreateTaskResult
```
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self, uri: str) -> ResourceResult | mcp.types.CreateTaskResult
@@ -395,19 +395,19 @@ return ResourceResult.
- `ResourceError`: If resource read fails
-#### `render_prompt`
+#### `render_prompt`
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult
```
-#### `render_prompt`
+#### `render_prompt`
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult
```
-#### `render_prompt`
+#### `render_prompt`
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult | mcp.types.CreateTaskResult
@@ -437,7 +437,7 @@ return PromptResult.
- `PromptError`: If prompt rendering fails
-#### `add_tool`
+#### `add_tool`
```python
add_tool(self, tool: Tool | Callable[..., Any]) -> Tool
@@ -455,7 +455,7 @@ with the Context type annotation. See the @tool decorator for examples.
- The tool instance that was added to the server.
-#### `remove_tool`
+#### `remove_tool`
```python
remove_tool(self, name: str, version: str | None = None) -> None
@@ -471,19 +471,19 @@ Remove tool(s) from the server.
- `NotFoundError`: If no matching tool is found.
-#### `tool`
+#### `tool`
```python
tool(self, name_or_fn: AnyFunction) -> FunctionTool
```
-#### `tool`
+#### `tool`
```python
tool(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool]
```
-#### `tool`
+#### `tool`
```python
tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]
@@ -539,7 +539,7 @@ server.tool(my_function, name="custom_name")
```
-#### `add_resource`
+#### `add_resource`
```python
add_resource(self, resource: Resource | Callable[..., Any]) -> Resource | ResourceTemplate
@@ -554,7 +554,7 @@ Add a resource to the server.
- The resource instance that was added to the server.
-#### `add_template`
+#### `add_template`
```python
add_template(self, template: ResourceTemplate) -> ResourceTemplate
@@ -569,7 +569,7 @@ Add a resource template to the server.
- The template instance that was added to the server.
-#### `resource`
+#### `resource`
```python
resource(self, uri: str) -> Callable[[AnyFunction], Resource | ResourceTemplate | AnyFunction]
@@ -628,7 +628,7 @@ async def get_weather(city: str) -> str:
```
-#### `add_prompt`
+#### `add_prompt`
```python
add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt
@@ -643,19 +643,19 @@ Add a prompt to the server.
- The prompt instance that was added to the server.
-#### `prompt`
+#### `prompt`
```python
prompt(self, name_or_fn: AnyFunction) -> FunctionPrompt
```
-#### `prompt`
+#### `prompt`
```python
prompt(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionPrompt]
```
-#### `prompt`
+#### `prompt`
```python
prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt]
@@ -732,7 +732,7 @@ Decorator to register a prompt.
```
-#### `mount`
+#### `mount`
```python
mount(self, server: FastMCP[LifespanResultT], namespace: str | None = None, as_proxy: bool | None = None, tool_names: dict[str, str] | None = None, prefix: str | None = None) -> None
@@ -779,7 +779,7 @@ mounted server.
- `prefix`: Deprecated. Use namespace instead.
-#### `import_server`
+#### `import_server`
```python
import_server(self, server: FastMCP[LifespanResultT], prefix: str | None = None) -> None
@@ -820,7 +820,7 @@ templates, and prompts are imported with their original names.
objects are imported with their original names.
-#### `from_openapi`
+#### `from_openapi`
```python
from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient | None = None, name: str = 'OpenAPI Server', route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, **settings: Any) -> Self
@@ -845,7 +845,7 @@ server URL from the OpenAPI spec with a 30-second timeout.
- A FastMCP server with an OpenAPIProvider attached.
-#### `from_fastapi`
+#### `from_fastapi`
```python
from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> Self
@@ -869,7 +869,7 @@ Use this to configure timeout and other client settings.
- A FastMCP server with an OpenAPIProvider attached.
-#### `as_proxy`
+#### `as_proxy`
```python
as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
@@ -887,7 +887,7 @@ instance or any value accepted as the `transport` argument of
`fastmcp.client.Client` constructor.
-#### `generate_name`
+#### `generate_name`
```python
generate_name(cls, name: str | None = None) -> str
diff --git a/docs/python-sdk/fastmcp-tools-function_tool.mdx b/docs/python-sdk/fastmcp-tools-function_tool.mdx
index bd66818a0..4f910a354 100644
--- a/docs/python-sdk/fastmcp-tools-function_tool.mdx
+++ b/docs/python-sdk/fastmcp-tools-function_tool.mdx
@@ -10,7 +10,7 @@ Standalone @tool decorator for FastMCP.
## Functions
-### `tool`
+### `tool`
```python
tool(name_or_fn: str | Callable[..., Any] | None = None) -> Any
@@ -37,11 +37,11 @@ Protocol for functions decorated with @tool.
Metadata attached to functions by the @tool decorator.
-### `FunctionTool`
+### `FunctionTool`
**Methods:**
-#### `to_mcp_tool`
+#### `to_mcp_tool`
```python
to_mcp_tool(self, **overrides: Any) -> mcp.types.Tool
@@ -52,7 +52,7 @@ Convert the FastMCP tool to an MCP tool.
Extends the base implementation to add task execution mode if enabled.
-#### `from_function`
+#### `from_function`
```python
from_function(cls, fn: Callable[..., Any]) -> FunctionTool
@@ -68,7 +68,7 @@ individual parameters must not be passed.
Cannot be used together with metadata parameter.
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@@ -77,7 +77,7 @@ run(self, arguments: dict[str, Any]) -> ToolResult
Run the tool with arguments.
-#### `register_with_docket`
+#### `register_with_docket`
```python
register_with_docket(self, docket: Docket) -> None
@@ -89,7 +89,7 @@ FunctionTool registers the underlying function, which has the user's
Depends parameters for docket to resolve.
-#### `add_to_docket`
+#### `add_to_docket`
```python
add_to_docket(self, docket: Docket, arguments: dict[str, Any], **kwargs: Any) -> Execution
diff --git a/docs/servers/dependency-injection.mdx b/docs/servers/dependency-injection.mdx
index d986bc52a..27dd3fd0c 100644
--- a/docs/servers/dependency-injection.mdx
+++ b/docs/servers/dependency-injection.mdx
@@ -237,6 +237,37 @@ The `AccessToken` object provides:
- **`expires_at`**: Token expiration timestamp (if available)
- **`claims`**: Dictionary of all token claims (JWT claims or provider-specific data)
+### Token Claims
+
+When you need just one specific value from the tokenβlike a user ID or tenant identifierβ`TokenClaim()` extracts it directly without needing the full token object.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.dependencies import TokenClaim
+
+mcp = FastMCP("Demo")
+
+
+@mcp.tool
+async def add_expense(
+ amount: float,
+ user_id: str = TokenClaim("oid"), # Azure object ID
+) -> dict:
+ await db.insert({"user_id": user_id, "amount": amount})
+ return {"status": "created", "user_id": user_id}
+```
+
+`TokenClaim()` raises a `RuntimeError` if the claim doesn't exist, listing available claims to help with debugging.
+
+Common claims vary by identity provider:
+
+| Provider | User ID Claim | Email Claim | Name Claim |
+|----------|--------------|-------------|------------|
+| Azure/Entra | `oid` | `email` | `name` |
+| GitHub | `sub` | `email` | `name` |
+| Google | `sub` | `email` | `name` |
+| Auth0 | `sub` | `email` | `name` |
+
### Background Task Dependencies
diff --git a/docs/servers/sampling.mdx b/docs/servers/sampling.mdx
index f15d50aa3..8ea479eb0 100644
--- a/docs/servers/sampling.mdx
+++ b/docs/servers/sampling.mdx
@@ -289,6 +289,45 @@ def search(query: str) -> str:
`ToolError` messages always pass through to the LLM, making it the escape hatch for errors you want the LLM to see and handle.
+### Concurrent Tool Execution
+
+By default, tools execute sequentially β one at a time, in order. When your tools are independent (no shared state between them), you can execute them in parallel with `tool_concurrency`:
+
+```python
+result = await ctx.sample(
+ messages="Research these three topics",
+ tools=[search, fetch_url],
+ tool_concurrency=0, # Unlimited parallel execution
+)
+```
+
+The `tool_concurrency` parameter controls how many tools run at once:
+
+- **`None`** (default): Sequential execution
+- **`0`**: Unlimited parallel execution
+- **`N > 0`**: Execute at most N tools concurrently
+
+For tools that must not run concurrently (file writes, shared state mutations, etc.), mark them as `sequential` when creating the `SamplingTool`:
+
+```python
+from fastmcp.server.sampling import SamplingTool
+
+db_writer = SamplingTool.from_function(
+ write_to_db,
+ sequential=True, # Forces all tools in the batch to run sequentially
+)
+
+result = await ctx.sample(
+ messages="Process this data",
+ tools=[search, db_writer],
+ tool_concurrency=0, # Would be parallel, but db_writer forces sequential
+)
+```
+
+
+When any tool in a batch has `sequential=True`, the entire batch executes sequentially regardless of `tool_concurrency`. This is a conservative guarantee β if one tool needs ordering, all tools in that batch respect it.
+
+
### Client Requirements
@@ -463,6 +502,10 @@ tool_result = ToolResultContent(
If True, mask detailed error messages from tool execution. When None (default), uses the global `settings.mask_error_details` value. Tools can raise `ToolError` to bypass masking and provide specific error messages to the LLM.
+
+ Controls parallel execution of tools. `None` (default) for sequential, `0` for unlimited parallel, or a positive integer for bounded concurrency. If any tool has `sequential=True`, all tools execute sequentially regardless.
+
+
@@ -511,6 +554,10 @@ tool_result = ToolResultContent(
If True, mask detailed error messages from tool execution.
+
+
+ Controls parallel execution of tools. `None` (default) for sequential, `0` for unlimited parallel, or a positive integer for bounded concurrency.
+
diff --git a/examples/apps/qr_server/README.md b/examples/apps/qr_server/README.md
index f2c9ebf1d..2cfe806b1 100644
--- a/examples/apps/qr_server/README.md
+++ b/examples/apps/qr_server/README.md
@@ -4,7 +4,7 @@ An MCP App server that generates QR codes with an interactive viewer UI. Ported
## What it demonstrates
-- Linking a tool to a `ui://` resource via `ToolUI`
+- Linking a tool to a `ui://` resource via `AppConfig`
- Serving embedded HTML with the `@modelcontextprotocol/ext-apps` JS SDK from CDN
- Declaring CSP resource domains via `ResourceCSP`
- Returning `ImageContent` (base64 PNG) from a tool
diff --git a/examples/apps/qr_server/qr_server.py b/examples/apps/qr_server/qr_server.py
index 8478cd557..7a3d5ee64 100644
--- a/examples/apps/qr_server/qr_server.py
+++ b/examples/apps/qr_server/qr_server.py
@@ -1,7 +1,7 @@
"""QR Code MCP App Server β generates QR codes with an interactive view UI.
Demonstrates MCP Apps with FastMCP:
-- Tool linked to a ui:// resource via ToolUI
+- Tool linked to a ui:// resource via AppConfig
- HTML resource with CSP metadata for CDN-loaded dependencies
- Embedded HTML using the @modelcontextprotocol/ext-apps JS SDK
- ImageContent return type for binary data
@@ -26,7 +26,7 @@ import qrcode # type: ignore[import-untyped]
from mcp import types
from fastmcp import FastMCP
-from fastmcp.server.apps import ResourceCSP, ResourceUI, ToolUI
+from fastmcp.server.apps import AppConfig, ResourceCSP
from fastmcp.tools import ToolResult
VIEW_URI: str = "ui://qr-server/view.html"
@@ -104,7 +104,7 @@ EMBEDDED_VIEW_HTML: str = """\