Archive v3 docs and publish v4 as the primary version (#4613)
* Archive v3 docs under /v3 and publish v4 as the primary version * Label primary docs version v4.0.0 (alpha 1) * Add What's New in v4 page; fix upgrade-guide phrasing; point banner at What's New * Rewrite What's New around v4's new capabilities, not the sampling deprecation * Lead What's New with the SDK v2 engine swap and the SEPs it brings * State ships now (link Session State); tasks arrive next alpha * Exclude docs/v3 frozen snapshots from doc-example import validation
|
|
@ -57,6 +57,42 @@ h6 code:not(pre code) {
|
|||
background: linear-gradient(135deg, #2d00f7 0%, #4cc9f0 100%);
|
||||
}
|
||||
|
||||
/* V3 banner - inside content-container, breaks out of padding with negative margins */
|
||||
#v3-banner {
|
||||
display: block;
|
||||
background: linear-gradient(135deg, #4cc9f0 0%, #2d00f7 100%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 10px 16px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin: -2rem -2rem 1.5rem -2rem;
|
||||
width: calc(100% + 4rem);
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
#v3-banner a {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#v3-banner a:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
#v3-banner {
|
||||
margin: -3rem -4rem 1.5rem -4rem;
|
||||
width: calc(100% + 8rem);
|
||||
}
|
||||
}
|
||||
|
||||
.dark #v3-banner {
|
||||
background: linear-gradient(135deg, #2d00f7 0%, #4cc9f0 100%);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
"dark": "#475569",
|
||||
"light": "#1e3a5f"
|
||||
},
|
||||
"content": "Meet [Prefect Horizon](https://prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_banner&utm_content=sitewide_banner), the enterprise MCP gateway built by the team behind FastMCP"
|
||||
"content": "FastMCP 4 is in alpha — you're reading the v4 docs. [What's new](/getting-started/whats-new) · [FastMCP 3 docs](/v3/getting-started/welcome)"
|
||||
},
|
||||
"colors": {
|
||||
"dark": "#f72585",
|
||||
|
|
@ -89,7 +89,8 @@
|
|||
"pages": [
|
||||
"getting-started/welcome",
|
||||
"getting-started/installation",
|
||||
"getting-started/quickstart"
|
||||
"getting-started/quickstart",
|
||||
"getting-started/whats-new"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -412,7 +413,10 @@
|
|||
"icon": "code"
|
||||
}
|
||||
],
|
||||
"version": "v3"
|
||||
"version": "v4.0.0 (alpha 1)"
|
||||
},
|
||||
{
|
||||
"$ref": "./v3-navigation.json"
|
||||
},
|
||||
{
|
||||
"$ref": "./v2-navigation.json"
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ A few client behaviors that touch the SDK are preserved so you don't have to cha
|
|||
|
||||
## What you must change
|
||||
|
||||
Three things are on you.
|
||||
Everything above, FastMCP handled for you. What remains lives in your own code, where FastMCP can't reach it — your imports, how you construct errors, the custom HTTP clients you hand to a transport, and any place you reach past FastMCP's surfaces into the raw SDK objects. Each surfaces as a clear failure at import or call time, and each is a mechanical fix.
|
||||
|
||||
**Your own `mcp.types` imports.** FastMCP can re-export types, but it can't rewrite imports in your code. Any `from mcp.types import X` or `import mcp.types` in your server or client fails at import time with:
|
||||
|
||||
|
|
|
|||
71
docs/getting-started/whats-new.mdx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
title: "What's New in FastMCP 4"
|
||||
sidebarTitle: "What's New"
|
||||
description: The capabilities that define FastMCP 4 — a rebuilt engine, a new protocol era, and a stateless protocol made practical.
|
||||
icon: sparkles
|
||||
---
|
||||
|
||||
FastMCP 4 is a major version because its engine changed. The framework is now built on the MCP Python SDK v2, a ground-up rebuild of the protocol layer, and on that foundation it adds a new protocol era, first-class extensions, stateless state, enterprise identity, and more. Most FastMCP 3 servers run on it untouched — the major version signals how much moved underneath, and what that movement unlocks.
|
||||
|
||||
<Note>
|
||||
FastMCP 4 is in **alpha**. Everything below is available today except background tasks, which arrive in the next alpha. Pin an exact version and expect sharp edges.
|
||||
</Note>
|
||||
|
||||
## Built on the MCP Python SDK v2
|
||||
|
||||
The defining change in FastMCP 4 is the one you mostly can't see. The MCP Python SDK v2 rewrote the protocol layer end to end: it split the protocol types into a standalone `mcp_types` package, renamed every wire field from camelCase to snake_case, replaced the server's request-handling model, and made server-side middleware and multi-era serving first-class. FastMCP absorbs nearly all of it — your reads stay working through a compatibility bridge, and the handful of changes left in your code are mechanical.
|
||||
|
||||
The major version is the signal. Even where your surface is unchanged, the behavior underneath is substantially different, and bumping to 4.0 is how we tell you that plainly rather than slipping a new engine in under a patch release.
|
||||
|
||||
The rebuild also pulls the protocol's recent evolution forward in a single step. A batch of accepted MCP proposals arrives with SDK v2, and FastMCP 4 surfaces each one: capability-negotiated extensions (SEP-2133), multi-round-trip elicitation for sessionless connections (SEP-2322), response cache hints (SEP-2549), spec-standard error codes (SEP-2164), the enterprise identity-assertion grant (SEP-990), and the sessionless `2026-07-28` protocol itself, which removes server-initiated requests (SEP-2577). The rest of this page is what those add up to.
|
||||
|
||||
## Every protocol era
|
||||
|
||||
A FastMCP 4 server answers clients across the protocol transition from one deployment. The MCP SDK negotiates the era per connection — the sessionless `2026-07-28` protocol for clients that have moved forward, the session-based handshake for everyone else — and any replica behind a plain load balancer can serve a modern request. This supersedes FastMCP's earlier "latest protocol only" stance: you adopt the new protocol without forking your deployment or gating clients by version.
|
||||
|
||||
The modern protocol is sessionless, so it drops the server's ability to call back into the client mid-request (SEP-2577). Imperative `ctx.elicit` and `ctx.list_roots` move to a request-shaped pattern on modern connections, and server-initiated sampling — which has no such replacement — is [deprecated](/servers/sampling). Everything else about writing a server is unchanged.
|
||||
|
||||
## State without a session
|
||||
|
||||
A stateless protocol raises an obvious question: if every request is a fresh connection, where does a tool keep a shopping cart, a conversation, or a running total? FastMCP 4 follows the MCP working group's own decision to reject protocol-level sessions in favor of *explicit state handles* (SEP-2567) — the server hands out an identifier, and the client passes it back.
|
||||
|
||||
Two shapes cover the cases. `UserSession` is injected like `Context` and keyed to the authenticated user, so a tool reads and writes one bucket of state with nothing to pass around. `SessionId` is an explicit handle a tool mints and the caller supplies as an argument, for when one user holds many independent states. Both store their data server-side in the storage backend, keyed to the authenticated user — so a handle is inert in anyone else's hands. See [Session State](/servers/sessions).
|
||||
|
||||
## Background tasks
|
||||
|
||||
Long-running work runs as a background task: the server accepts the call, returns a handle, and the client polls for the result while the work proceeds. Tasks left the core MCP spec during the SDK v2 rebuild and returned as the `io.modelcontextprotocol/tasks` extension (SEP-2663), which FastMCP 4 implements end to end in the optional `fastmcp-tasks` package. The durable execution engine that made FastMCP 3's tasks reliable carries straight over, and `@mcp.tool(task=True)` remains the only authoring surface — so the wire protocol modernizing underneath costs you no code change. See [Background Tasks](/development/v4-notes/background-tasks) for the design; the runtime arrives in the next alpha.
|
||||
|
||||
## Server extensions
|
||||
|
||||
Background tasks are the first capability built on a more general one: FastMCP 4 makes MCP extensions — capability-negotiated protocol features named by a reverse-DNS string (SEP-2133) — a first-class surface. `FastMCP.add_extension()` lets an extension advertise a capability, add request methods, intercept `tools/call`, and run a lifespan hook, all with full access to the component registry, `Context`, and auth. The same extensions flow through the client with `Client(extensions=...)`. A cross-cutting protocol feature stops being surgery on core and becomes a supported plugin.
|
||||
|
||||
## Enterprise identity
|
||||
|
||||
FastMCP 4 ships a complete server-side implementation of identity assertion (SEP-990): enterprise "on-behalf-of" access, where a corporate identity provider issues a signed assertion, the user's agent presents it, and the server mints a short-lived token — no browser login and no per-user consent screen. Behind one parameter on the existing auth providers, FastMCP performs the full signature verification, binding checks, replay rejection, and scoped token issuance.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth import IdentityAssertion, OAuthProxy
|
||||
|
||||
auth = OAuthProxy(
|
||||
# existing upstream configuration unchanged
|
||||
identity_assertion=IdentityAssertion(trusted_issuers=["https://login.acme-corp.com"]),
|
||||
)
|
||||
mcp = FastMCP("Internal API", auth=auth)
|
||||
```
|
||||
|
||||
The asserted subject flows into the normal auth context, so tools read it through `get_access_token()` like any other identity. See [Identity Assertion](/servers/auth/oauth-proxy#identity-assertion-sep-990).
|
||||
|
||||
## Faster and safer
|
||||
|
||||
Two more capabilities arrive by default. Response caching (SEP-2549) lets a server stamp freshness hints on its results that a caching [client](/clients/client#response-caching) reuses without a round trip, and a distributed `KeyValueResponseCacheStore` backs that cache with Redis or any key-value store, so a fleet of clients or proxy replicas shares fills.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Weather", cache_ttl=300, cache_scope="public")
|
||||
```
|
||||
|
||||
Security tightened in the same release: every templated resource screens its parameters for path traversal, absolute paths, and null bytes before the handler runs — [path security](/servers/resources#path-security) on by default, covering mounted and proxied templates too.
|
||||
|
||||
When you're ready to move a server to v4, [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) walks through every change and what it looks like in practice.
|
||||
39
docs/v3-banner.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// Add v3 banner inside content-container with negative margins
|
||||
(function() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
function addBanner() {
|
||||
const isV3 = window.location.pathname.includes('/v3/');
|
||||
const container = document.getElementById('content-container');
|
||||
let banner = document.getElementById('v3-banner');
|
||||
|
||||
if (isV3 && container) {
|
||||
if (!banner) {
|
||||
banner = document.createElement('div');
|
||||
banner.id = 'v3-banner';
|
||||
banner.innerHTML = 'These are the docs for FastMCP 3. <a href="/getting-started/welcome" style="color: white; text-decoration: underline; font-weight: 700;">FastMCP 4</a> is now available.';
|
||||
container.insertBefore(banner, container.firstChild);
|
||||
}
|
||||
} else if (!isV3 && banner) {
|
||||
banner.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function run() {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', addBanner);
|
||||
} else {
|
||||
addBanner();
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
let lastUrl = location.href;
|
||||
new MutationObserver(() => {
|
||||
if (location.href !== lastUrl) {
|
||||
lastUrl = location.href;
|
||||
setTimeout(addBanner, 100);
|
||||
}
|
||||
}).observe(document.body, {subtree: true, childList: true});
|
||||
})();
|
||||
312
docs/v3-navigation.json
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
{
|
||||
"dropdowns": [
|
||||
{
|
||||
"dropdown": "Documentation",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Get Started",
|
||||
"pages": [
|
||||
"v3/getting-started/welcome",
|
||||
"v3/getting-started/installation",
|
||||
"v3/getting-started/quickstart"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Servers",
|
||||
"pages": [
|
||||
"v3/servers/server",
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Core Components",
|
||||
"icon": "toolbox",
|
||||
"pages": [
|
||||
"v3/servers/tools",
|
||||
"v3/servers/resources",
|
||||
"v3/servers/prompts",
|
||||
"v3/servers/context"
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Working with Tools",
|
||||
"icon": "wand-magic-sparkles",
|
||||
"pages": [
|
||||
"v3/servers/transforms/transforms",
|
||||
"v3/servers/transforms/tool-transformation",
|
||||
"v3/servers/transforms/code-mode",
|
||||
"v3/servers/transforms/tool-search",
|
||||
"v3/servers/transforms/namespace",
|
||||
"v3/servers/visibility",
|
||||
"v3/servers/transforms/resources-as-tools",
|
||||
"v3/servers/transforms/prompts-as-tools",
|
||||
"v3/servers/tool-fingerprinting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "MCP Providers",
|
||||
"icon": "layer-group",
|
||||
"pages": [
|
||||
"v3/servers/providers/overview",
|
||||
"v3/servers/providers/local",
|
||||
"v3/servers/providers/filesystem",
|
||||
"v3/servers/providers/proxy",
|
||||
"v3/servers/providers/skills",
|
||||
"v3/servers/composition",
|
||||
"v3/servers/providers/custom"
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Interactivity",
|
||||
"icon": "comments",
|
||||
"pages": [
|
||||
"v3/servers/elicitation",
|
||||
"v3/servers/sampling",
|
||||
"v3/servers/progress",
|
||||
"v3/servers/logging",
|
||||
"v3/servers/pagination",
|
||||
"v3/servers/icons"
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Extensibility",
|
||||
"icon": "puzzle-piece",
|
||||
"pages": [
|
||||
"v3/servers/middleware",
|
||||
"v3/servers/dependency-injection",
|
||||
"v3/servers/lifespan",
|
||||
"v3/servers/storage-backends",
|
||||
"v3/servers/tasks",
|
||||
"v3/servers/versioning"
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Auth",
|
||||
"icon": "shield-check",
|
||||
"pages": [
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Authentication",
|
||||
"icon": "key",
|
||||
"pages": [
|
||||
"v3/servers/auth/authentication",
|
||||
"v3/servers/auth/token-verification",
|
||||
"v3/servers/auth/remote-oauth",
|
||||
"v3/servers/auth/oauth-proxy",
|
||||
"v3/servers/auth/oidc-proxy",
|
||||
"v3/servers/auth/full-oauth-server",
|
||||
"v3/servers/auth/multi-auth"
|
||||
]
|
||||
},
|
||||
"v3/servers/authorization"
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Deployment",
|
||||
"icon": "rocket",
|
||||
"pages": [
|
||||
"v3/deployment/running-server",
|
||||
"v3/deployment/http",
|
||||
"v3/deployment/sandboxed-agents",
|
||||
"v3/deployment/prefect-horizon",
|
||||
"v3/deployment/server-configuration",
|
||||
"v3/servers/testing",
|
||||
"v3/servers/telemetry"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Apps",
|
||||
"pages": [
|
||||
"v3/apps/overview",
|
||||
"v3/apps/quickstart",
|
||||
"v3/apps/fastmcp-app",
|
||||
"v3/apps/prefab",
|
||||
"v3/apps/generative",
|
||||
"v3/apps/low-level",
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Reference",
|
||||
"icon": "book",
|
||||
"pages": [
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Prefab Providers",
|
||||
"icon": "cube",
|
||||
"pages": [
|
||||
"v3/apps/providers/approval",
|
||||
"v3/apps/providers/choice",
|
||||
"v3/apps/providers/file-upload",
|
||||
"v3/apps/providers/form"
|
||||
]
|
||||
},
|
||||
"v3/apps/development",
|
||||
"v3/apps/examples",
|
||||
"v3/apps/architecture"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Clients",
|
||||
"pages": [
|
||||
"v3/clients/client",
|
||||
"v3/clients/client-only-package",
|
||||
"v3/clients/transports",
|
||||
"v3/clients/fastmcp-remote",
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Operations",
|
||||
"icon": "toolbox",
|
||||
"pages": [
|
||||
"v3/clients/tools",
|
||||
"v3/clients/resources",
|
||||
"v3/clients/prompts",
|
||||
"v3/clients/sampling",
|
||||
"v3/clients/elicitation",
|
||||
"v3/clients/tasks",
|
||||
"v3/clients/progress",
|
||||
"v3/clients/logging",
|
||||
"v3/clients/roots",
|
||||
"v3/clients/notifications"
|
||||
],
|
||||
"tag": "UPDATED"
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Authentication",
|
||||
"icon": "key",
|
||||
"pages": [
|
||||
"v3/clients/auth/oauth",
|
||||
"v3/clients/auth/cimd",
|
||||
"v3/clients/auth/bearer"
|
||||
],
|
||||
"tag": "UPDATED"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Integrations",
|
||||
"pages": [
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Auth",
|
||||
"icon": "key",
|
||||
"pages": [
|
||||
"v3/integrations/auth0",
|
||||
"v3/integrations/authkit",
|
||||
"v3/integrations/aws-cognito",
|
||||
"v3/integrations/azure",
|
||||
"v3/integrations/descope",
|
||||
"v3/integrations/discord",
|
||||
"v3/integrations/eunomia-authorization",
|
||||
"v3/integrations/github",
|
||||
"v3/integrations/google",
|
||||
"v3/integrations/huggingface",
|
||||
"v3/integrations/keycloak",
|
||||
"v3/integrations/oci",
|
||||
"v3/integrations/permit",
|
||||
"v3/integrations/propelauth",
|
||||
"v3/integrations/scalekit",
|
||||
"v3/integrations/supabase",
|
||||
"v3/integrations/workos"
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Web Frameworks",
|
||||
"icon": "code",
|
||||
"pages": [
|
||||
"v3/integrations/fastapi",
|
||||
"v3/integrations/openapi"
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "AI Assistants",
|
||||
"icon": "robot",
|
||||
"pages": [
|
||||
"v3/integrations/chatgpt",
|
||||
"v3/integrations/claude-code",
|
||||
"v3/integrations/claude-desktop",
|
||||
"v3/integrations/cursor",
|
||||
"v3/integrations/gemini-cli",
|
||||
"v3/integrations/goose"
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "AI SDKs",
|
||||
"icon": "microchip",
|
||||
"pages": [
|
||||
"v3/integrations/anthropic",
|
||||
"v3/integrations/gemini",
|
||||
"v3/integrations/openai",
|
||||
"v3/integrations/pydantic-ai"
|
||||
]
|
||||
},
|
||||
"v3/integrations/mcp-json-configuration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "More",
|
||||
"pages": [
|
||||
"v3/more/settings",
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "CLI",
|
||||
"icon": "terminal",
|
||||
"pages": [
|
||||
"v3/cli/overview",
|
||||
"v3/cli/running",
|
||||
"v3/cli/install-mcp",
|
||||
"v3/cli/inspecting",
|
||||
"v3/cli/client",
|
||||
"v3/cli/generate-cli",
|
||||
"v3/cli/auth"
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Upgrading",
|
||||
"icon": "up",
|
||||
"pages": [
|
||||
"v3/getting-started/upgrading/from-fastmcp-2",
|
||||
"v3/getting-started/upgrading/from-mcp-sdk",
|
||||
"v3/getting-started/upgrading/from-low-level-sdk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Development",
|
||||
"icon": "code",
|
||||
"pages": [
|
||||
"v3/development/contributing",
|
||||
"v3/development/tests",
|
||||
"v3/development/releases",
|
||||
"v3/patterns/contrib"
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "What's New",
|
||||
"icon": "sparkles",
|
||||
"pages": [
|
||||
"v3/updates",
|
||||
"v3/changelog"
|
||||
]
|
||||
},
|
||||
"v3/more/faq"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "book"
|
||||
}
|
||||
],
|
||||
"version": "v3.4.4"
|
||||
}
|
||||
118
docs/v3/apps/architecture.mdx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
---
|
||||
title: Architecture
|
||||
sidebarTitle: Architecture
|
||||
description: How FastMCP apps work under the hood — from Python to pixels.
|
||||
icon: sitemap
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
You don't need this page to build apps. It's for when something isn't rendering the way you expect, when UI tool calls aren't reaching your server, or when you're writing [custom HTML apps](/apps/low-level) and need to understand the protocol directly.
|
||||
|
||||
## The pipeline
|
||||
|
||||
An MCP app moves through five stages from Python to pixels:
|
||||
|
||||
```
|
||||
Python components → JSON tree → structuredContent → Renderer iframe → Host UI
|
||||
```
|
||||
|
||||
You write Prefab components. FastMCP serializes them to a JSON component tree and delivers it as `structuredContent` on the tool result. The host loads the Prefab renderer in a sandboxed iframe, pushes the JSON in, and the renderer paints the UI. If the UI calls server tools, it talks back through the same `postMessage` channel.
|
||||
|
||||
The sections below walk each stage.
|
||||
|
||||
## Tool registration
|
||||
|
||||
When you mark a tool with `app=True` or `@app.ui()`, FastMCP wires up the metadata and renderer resource that the protocol requires.
|
||||
|
||||
### The `app=True` flag
|
||||
|
||||
`app` on `@mcp.tool` accepts `True`, an `AppConfig`, or a dict. When you pass `True`, FastMCP checks whether the tool's return type is a Prefab type (`PrefabApp`, `Component`, or unions containing them). If it qualifies, FastMCP expands `True` into a full `AppConfig` — setting the renderer URI, CSP headers, and visibility — and stores it in the tool's `meta["ui"]` dict.
|
||||
|
||||
This expansion also registers the shared Prefab renderer resource (below). The tool and the renderer are linked through a `resourceUri` field in the metadata: the tool says "render me with `ui://prefab/renderer.html`" and the host fetches that resource when it displays the result.
|
||||
|
||||
Type inference works the same way. If the return type is a Prefab type and you haven't set `app` explicitly, FastMCP auto-wires the metadata as if you'd written `app=True`.
|
||||
|
||||
### FastMCPApp registration
|
||||
|
||||
`FastMCPApp` uses the same mechanism but adds two things. First, it tags every tool — both `@app.ui()` entry points and `@app.tool()` backends — with `meta["fastmcp"]["app"]` set to the app's name. That tag lets the server identify which app a tool belongs to when routing UI calls.
|
||||
|
||||
Second, it sets `meta["ui"]["visibility"]` to control who can see each tool. Entry points default to `["model"]` (LLM-visible). Backend tools default to `["app"]` (UI-only). Hosts use this to filter the tool list.
|
||||
|
||||
## Serialization
|
||||
|
||||
When a Prefab tool runs, its return value — a `PrefabApp` or a bare `Component` — becomes a JSON blob the renderer can interpret.
|
||||
|
||||
### `PrefabApp.to_json()`
|
||||
|
||||
The entry point is `PrefabApp.to_json()`. It walks the component tree and produces a JSON object with three top-level keys: `view` (the component tree), `state` (initial state values), and `_meta` (routing metadata).
|
||||
|
||||
FastMCP passes a `tool_resolver` callback to `to_json()`. Whenever the tree contains a `CallTool` action that references a function (not a string), the resolver converts it to a `ResolvedTool` with the function's registered name. This is how `CallTool(save_contact)` becomes `CallTool("save_contact")` on the wire. The resolver also handles `unwrap_result` — a flag telling the renderer to unwrap single-value results from the `{"result": value}` envelope FastMCP uses for schema compliance.
|
||||
|
||||
### The `_meta.fastmcp.app` tag
|
||||
|
||||
After `to_json()` produces the tree, FastMCP injects `_meta.fastmcp.app` with the app's name (if the tool belongs to a `FastMCPApp`). This tag rides along inside `structuredContent` all the way to the renderer.
|
||||
|
||||
When the renderer calls a backend tool, it includes `_meta.fastmcp.app` in the `CallTool` request. The server sees this tag and routes the call through a special path that bypasses transforms (below).
|
||||
|
||||
### ToolResult assembly
|
||||
|
||||
The final tool result has two parts: `content` (a list of `TextContent` blocks for the LLM) and `structuredContent` (the JSON tree for the renderer). By default, Prefab tools send `"[Rendered Prefab UI]"` as the text content — just enough for the LLM to know something was rendered. If you return a `ToolResult` explicitly, you control both halves.
|
||||
|
||||
## Tool call routing
|
||||
|
||||
Normal tool calls go through the provider chain, which applies transforms (namespace prefixes, visibility filters) before resolving by name. App UI calls need a different path.
|
||||
|
||||
### The `get_app_tool` bypass
|
||||
|
||||
Backend tools are typically hidden from the model (`visibility=["app"]`). Visibility transforms would filter them out of normal resolution. And namespace transforms might rename them — `save_contact` becomes `contacts_save_contact` — while the renderer still uses the original name.
|
||||
|
||||
`get_app_tool` solves both problems. When the server sees `_meta.fastmcp.app` on an incoming `CallTool` request, it calls `get_app_tool(app_name, tool_name)` instead of the normal `get_tool(name)`. This walks the provider tree directly, skipping transforms. It finds the tool by its original registered name and verifies that its `meta["fastmcp"]["app"]` matches the expected app.
|
||||
|
||||
That's why `CallTool("save_contact")` keeps working when the server is mounted under a namespace. The renderer sends the original name plus the app identity; the server uses `get_app_tool` to find it without transforms in the way.
|
||||
|
||||
Authorization still applies. `get_app_tool` bypasses transforms but runs auth checks against the tool's `auth` config before executing.
|
||||
|
||||
### Provider delegation
|
||||
|
||||
`get_app_tool` is defined on the `Provider` base class and overridden by aggregate and wrapped providers. Aggregate providers fan out the lookup across child providers in parallel. Wrapped providers (like `FastMCPProvider`, which wraps a nested `FastMCP` server) delegate to the inner server's `get_app_tool`. Backend tools are reachable through any depth of composition.
|
||||
|
||||
## The renderer
|
||||
|
||||
The Prefab renderer is a self-contained JavaScript application that interprets the JSON component tree and renders it as a React UI.
|
||||
|
||||
### The shared resource
|
||||
|
||||
FastMCP registers the renderer as a `ui://prefab/renderer.html` resource with MIME type `text/html;profile=mcp-app`. The HTML is bundled inside the `prefab-ui` Python package; `get_renderer_html()` returns it as a string. All Prefab tools on a server share this single resource.
|
||||
|
||||
The resource also carries CSP metadata (via `get_renderer_csp()`) declaring the CDN domains the renderer needs. Hosts use this to configure the iframe's Content Security Policy.
|
||||
|
||||
### `postMessage` communication
|
||||
|
||||
The renderer lives in a sandboxed iframe and communicates with the host using `postMessage`. The protocol follows the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) spec:
|
||||
|
||||
The host pushes the tool result (with `structuredContent`) into the iframe. The renderer parses the component tree, initializes state, and renders the UI. When the user interacts — submitting a form, clicking a button — and the interaction triggers a `CallTool` action, the renderer sends a `callServerTool` message back to the host via `postMessage`. The host forwards it as a regular MCP `tools/call` request to the server, including `_meta.fastmcp.app` for routing.
|
||||
|
||||
The response flows back the same way: server → host → iframe via `postMessage`, and the renderer updates state with the result.
|
||||
|
||||
### AppBridge
|
||||
|
||||
The `@modelcontextprotocol/ext-apps` JavaScript SDK provides the `App` class (sometimes called AppBridge) that manages the `postMessage` handshake. It handles connection negotiation, tool result delivery, server tool calls, and host context (safe area insets, theme preferences). The Prefab renderer uses it internally; you only touch it directly when building [custom HTML apps](/apps/low-level).
|
||||
|
||||
## The dev server
|
||||
|
||||
`fastmcp dev apps` simulates the host-side behavior locally without a real MCP client.
|
||||
|
||||
### Proxy architecture
|
||||
|
||||
Two HTTP servers. Your MCP server runs on port 8000 with the Streamable HTTP transport. The dev UI runs on port 8080 and serves a picker page that lists your app tools.
|
||||
|
||||
A reverse proxy at `/mcp` on the dev server forwards requests to your MCP server. This matters because the renderer iframe runs on `localhost:8080` and your MCP server runs on `localhost:8000` — without the proxy, the renderer's `callServerTool` requests would be cross-origin and the browser would block them. The proxy keeps everything same-origin from the iframe's perspective.
|
||||
|
||||
### The launch flow
|
||||
|
||||
When you select a tool and click launch, the dev UI calls the tool through the proxy, receives the `structuredContent` response, and opens a new tab. That tab loads the tool's renderer resource (via the proxy), creates an AppBridge, and pushes the tool result into the renderer. From here on it matches what a real host provides: the renderer displays the UI, and any `CallTool` actions route back through the proxy to your server.
|
||||
|
||||
Auto-reload is on by default, so changes to your server code restart the MCP server automatically. The dev UI keeps running — relaunch the tool to see changes.
|
||||
23
docs/v3/apps/demos/bar-chart.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column
|
||||
from prefab_ui.components.charts import BarChart, ChartSeries
|
||||
|
||||
data = [
|
||||
{"quarter": "Q1", "revenue": 42000, "costs": 28000},
|
||||
{"quarter": "Q2", "revenue": 51000, "costs": 31000},
|
||||
{"quarter": "Q3", "revenue": 47000, "costs": 29000},
|
||||
{"quarter": "Q4", "revenue": 63000, "costs": 35000},
|
||||
]
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(css_class="p-6"):
|
||||
BarChart(
|
||||
data=data,
|
||||
series=[
|
||||
ChartSeries(data_key="revenue", label="Revenue"),
|
||||
ChartSeries(data_key="costs", label="Costs"),
|
||||
],
|
||||
x_axis="quarter",
|
||||
show_legend=True,
|
||||
height=250,
|
||||
)
|
||||
78
docs/v3/apps/demos/contacts.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
from prefab_ui.actions import ShowToast
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
H3,
|
||||
Badge,
|
||||
Button,
|
||||
Column,
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
Form,
|
||||
Input,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
Separator,
|
||||
)
|
||||
|
||||
contacts = [
|
||||
{"name": "Arthur Dent", "email": "arthur@earth.com", "category": "Customer"},
|
||||
{"name": "Ford Prefect", "email": "ford@betelgeuse.org", "category": "Partner"},
|
||||
{
|
||||
"name": "Trillian Astra",
|
||||
"email": "trillian@heartofgold.com",
|
||||
"category": "Customer",
|
||||
},
|
||||
{"name": "Zaphod Beeblebrox", "email": "zaphod@galaxy.gov", "category": "Vendor"},
|
||||
]
|
||||
|
||||
rows = [
|
||||
{
|
||||
"name": c["name"],
|
||||
"email": c["email"],
|
||||
"category": Badge(
|
||||
c["category"],
|
||||
variant="success"
|
||||
if c["category"] == "Customer"
|
||||
else "secondary"
|
||||
if c["category"] == "Partner"
|
||||
else "outline",
|
||||
),
|
||||
}
|
||||
for c in contacts
|
||||
]
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="email", header="Email"),
|
||||
DataTableColumn(key="category", header="Category"),
|
||||
],
|
||||
rows=rows,
|
||||
search=True,
|
||||
)
|
||||
|
||||
Separator()
|
||||
|
||||
H3("Add Contact")
|
||||
with Form(
|
||||
on_submit=ShowToast(
|
||||
"Contact saved! (preview demo — no backend wired)",
|
||||
variant="success",
|
||||
),
|
||||
):
|
||||
with Row(gap=4):
|
||||
Input(name="name", label="Name", placeholder="Full name", required=True)
|
||||
Input(
|
||||
name="email",
|
||||
label="Email",
|
||||
placeholder="name@example.com",
|
||||
required=True,
|
||||
)
|
||||
with Select(name="category", label="Category"):
|
||||
SelectOption(value="Customer", label="Customer")
|
||||
SelectOption(value="Partner", label="Partner")
|
||||
SelectOption(value="Vendor", label="Vendor")
|
||||
Button("Save Contact")
|
||||
68
docs/v3/apps/demos/dashboard.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
Badge,
|
||||
Column,
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
Row,
|
||||
Separator,
|
||||
)
|
||||
from prefab_ui.components.charts import BarChart, ChartSeries
|
||||
from prefab_ui.components.metric import Metric
|
||||
|
||||
monthly = [
|
||||
{"month": "Jan", "revenue": 48200, "costs": 31000},
|
||||
{"month": "Feb", "revenue": 52100, "costs": 32500},
|
||||
{"month": "Mar", "revenue": 61800, "costs": 34200},
|
||||
{"month": "Apr", "revenue": 58400, "costs": 33800},
|
||||
]
|
||||
|
||||
deals = [
|
||||
{"account": "Acme Corp", "value": "$84,000", "stage": "Won"},
|
||||
{"account": "Globex Inc", "value": "$52,000", "stage": "Negotiation"},
|
||||
{"account": "Initech", "value": "$31,500", "stage": "Proposal"},
|
||||
{"account": "Wayne Enterprises", "value": "$45,000", "stage": "Lost"},
|
||||
]
|
||||
|
||||
rows = [
|
||||
{
|
||||
"account": d["account"],
|
||||
"value": d["value"],
|
||||
"stage": Badge(
|
||||
d["stage"],
|
||||
variant="success"
|
||||
if d["stage"] == "Won"
|
||||
else "destructive"
|
||||
if d["stage"] == "Lost"
|
||||
else "secondary",
|
||||
),
|
||||
}
|
||||
for d in deals
|
||||
]
|
||||
|
||||
total = sum(m["revenue"] for m in monthly)
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
with Row(gap=6):
|
||||
Metric(label="Revenue (Q1-Q4)", value=f"${total:,}")
|
||||
Metric(label="Deals", value=f"{len(deals)}")
|
||||
BarChart(
|
||||
data=monthly,
|
||||
series=[
|
||||
ChartSeries(data_key="revenue", label="Revenue"),
|
||||
ChartSeries(data_key="costs", label="Costs"),
|
||||
],
|
||||
x_axis="month",
|
||||
show_legend=True,
|
||||
height=200,
|
||||
)
|
||||
Separator()
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="account", header="Account", sortable=True),
|
||||
DataTableColumn(key="value", header="Value", sortable=True),
|
||||
DataTableColumn(key="stage", header="Stage"),
|
||||
],
|
||||
rows=rows,
|
||||
)
|
||||
24
docs/v3/apps/demos/data-table.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, DataTable, DataTableColumn
|
||||
|
||||
employees = [
|
||||
{"name": "Alice Chen", "role": "Staff Engineer", "dept": "Platform"},
|
||||
{"name": "Bob Martinez", "role": "Lead Designer", "dept": "Design"},
|
||||
{"name": "Carol Johnson", "role": "Senior Engineer", "dept": "Platform"},
|
||||
{"name": "David Kim", "role": "Product Manager", "dept": "Product"},
|
||||
{"name": "Eva Mueller", "role": "Engineer", "dept": "Platform"},
|
||||
{"name": "Frank Lee", "role": "Data Scientist", "dept": "ML"},
|
||||
{"name": "Grace Park", "role": "Eng Manager", "dept": "Platform"},
|
||||
]
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="role", header="Role", sortable=True),
|
||||
DataTableColumn(key="dept", header="Dept", sortable=True),
|
||||
],
|
||||
rows=employees,
|
||||
search=True,
|
||||
)
|
||||
461
docs/v3/apps/demos/hitchhikers.py
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
"""The Hitchhiker's Guide dashboard from the Prefab welcome page.
|
||||
|
||||
Run with:
|
||||
prefab serve examples/hitchhikers-guide/dashboard.py
|
||||
prefab export examples/hitchhikers-guide/dashboard.py
|
||||
"""
|
||||
|
||||
from prefab_ui import PrefabApp
|
||||
from prefab_ui.actions import SetInterval, SetState, ShowToast
|
||||
from prefab_ui.components import (
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Carousel,
|
||||
Checkbox,
|
||||
Column,
|
||||
Combobox,
|
||||
ComboboxOption,
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
DatePicker,
|
||||
Dialog,
|
||||
Grid,
|
||||
GridItem,
|
||||
HoverCard,
|
||||
Loader,
|
||||
Metric,
|
||||
Muted,
|
||||
P,
|
||||
Progress,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Ring,
|
||||
Row,
|
||||
Separator,
|
||||
Slider,
|
||||
Switch,
|
||||
Text,
|
||||
Tooltip,
|
||||
)
|
||||
from prefab_ui.components.charts import (
|
||||
BarChart,
|
||||
ChartSeries,
|
||||
RadarChart,
|
||||
Sparkline,
|
||||
)
|
||||
from prefab_ui.components.control_flow import Else, If
|
||||
from prefab_ui.rx import Rx
|
||||
|
||||
ctx_tick = Rx("ctx_tick")
|
||||
|
||||
# Context window: climbs from 24% to ~78%, then resets
|
||||
ctx_pct = (ctx_tick % 20) * 3 + 20
|
||||
ctx_variant = (ctx_pct > 70).then(
|
||||
"destructive", (ctx_pct <= 33).then("success", "default")
|
||||
)
|
||||
|
||||
with PrefabApp(
|
||||
title="Prefab Showcase",
|
||||
state={"ctx_tick": 0, "improbability": 42},
|
||||
on_mount=SetInterval(
|
||||
400,
|
||||
on_tick=SetState("ctx_tick", ctx_tick + 1),
|
||||
),
|
||||
) as app:
|
||||
with Grid(columns={"default": 1, "md": 2, "lg": 4}, gap=4):
|
||||
# ── Col 1 ─────────────────────────────────────────────────────────
|
||||
with Column(gap=4):
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Register Towel")
|
||||
CardDescription("The most important item in the galaxy")
|
||||
with CardContent():
|
||||
with Column(gap=3):
|
||||
with Combobox(
|
||||
placeholder="Type...",
|
||||
search_placeholder="Search types...",
|
||||
):
|
||||
ComboboxOption("Bath", value="bath")
|
||||
ComboboxOption("Beach", value="beach")
|
||||
ComboboxOption("Interstellar", value="interstellar")
|
||||
ComboboxOption("Microfiber", value="micro")
|
||||
DatePicker(placeholder="Registration date")
|
||||
with CardFooter():
|
||||
with Row(gap=2):
|
||||
with Dialog(
|
||||
title="Towel Registered!",
|
||||
description="Your towel has been added to the galactic registry.",
|
||||
):
|
||||
Button("Register")
|
||||
Text("Don't forget to bring it.")
|
||||
Button("Cancel", variant="outline")
|
||||
with If("{{ !pressed }}"):
|
||||
Button(
|
||||
"This is probably the best button to press.",
|
||||
variant="success",
|
||||
on_click=SetState("pressed", True),
|
||||
)
|
||||
with Else():
|
||||
Button(
|
||||
"Please do not press this button again.",
|
||||
variant="destructive",
|
||||
on_click=SetState("pressed", False),
|
||||
)
|
||||
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Ship Status")
|
||||
with CardContent():
|
||||
with Column(gap=3):
|
||||
with Row(
|
||||
align="center",
|
||||
css_class="justify-between",
|
||||
):
|
||||
Text("heart-of-gold")
|
||||
with HoverCard(open_delay=0, close_delay=200):
|
||||
Badge("In Orbit", variant="default")
|
||||
with Column(gap=2):
|
||||
Text("heart-of-gold")
|
||||
Muted("Deployed 2h ago")
|
||||
Progress(
|
||||
value=100,
|
||||
max=100,
|
||||
variant="success",
|
||||
)
|
||||
Progress(
|
||||
value=100,
|
||||
max=100,
|
||||
indicator_class="bg-yellow-400",
|
||||
)
|
||||
with Row(
|
||||
align="center",
|
||||
css_class="justify-between",
|
||||
):
|
||||
Text("vogon-poetry")
|
||||
with Tooltip("64% — ETA 12 min", delay=0):
|
||||
with Badge(variant="secondary"):
|
||||
Loader(size="sm")
|
||||
Text("Deploying")
|
||||
Progress(value=64, max=100)
|
||||
with Row(
|
||||
align="center",
|
||||
css_class="justify-between",
|
||||
):
|
||||
Text("deep-thought")
|
||||
with Tooltip(
|
||||
"Computing... 7.5 million years remaining",
|
||||
delay=0,
|
||||
):
|
||||
with Badge(variant="outline"):
|
||||
Loader(size="sm", variant="ios")
|
||||
Text("Soon...")
|
||||
Progress(value=12, max=100)
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Planet Ratings")
|
||||
with CardContent():
|
||||
RadarChart(
|
||||
data=[
|
||||
{"axis": "Views", "earth": 30, "mag": 95},
|
||||
{"axis": "Fjords", "earth": 65, "mag": 100},
|
||||
{"axis": "Pubs", "earth": 90, "mag": 10},
|
||||
{"axis": "Mice", "earth": 40, "mag": 85},
|
||||
{"axis": "Tea", "earth": 95, "mag": 15},
|
||||
{"axis": "Safety", "earth": 45, "mag": 70},
|
||||
],
|
||||
series=[
|
||||
ChartSeries(dataKey="earth", label="Earth"),
|
||||
ChartSeries(dataKey="mag", label="Magrathea"),
|
||||
],
|
||||
axis_key="axis",
|
||||
height=200,
|
||||
show_legend=True,
|
||||
show_tooltip=True,
|
||||
)
|
||||
|
||||
# ── Col 2 ─────────────────────────────────────────────────────────
|
||||
with Column(gap=4):
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Survival Odds")
|
||||
with CardContent(css_class="w-fit mx-auto"):
|
||||
Ring(
|
||||
value=42,
|
||||
label="42%",
|
||||
variant="info",
|
||||
size="lg",
|
||||
thickness=12,
|
||||
indicator_class="group-hover:drop-shadow-[0_0_24px_rgba(59,130,246,0.9)]",
|
||||
)
|
||||
with Card():
|
||||
with CardHeader():
|
||||
with Row(gap=2, align="center"):
|
||||
CardTitle("Improbability Drive")
|
||||
Loader(
|
||||
variant="pulse",
|
||||
size="sm",
|
||||
css_class="text-blue-500",
|
||||
)
|
||||
with CardContent():
|
||||
with Column(gap=2):
|
||||
Slider(
|
||||
min=0,
|
||||
max=100,
|
||||
value=42,
|
||||
name="improbability",
|
||||
)
|
||||
with Row(
|
||||
align="center",
|
||||
css_class="justify-between",
|
||||
):
|
||||
Muted("Probable")
|
||||
Muted("Infinite")
|
||||
with Carousel(auto_advance=3000, show_controls=False, direction="up"):
|
||||
with Alert(variant="success", icon="circle-check"):
|
||||
AlertTitle("Don't Panic")
|
||||
AlertDescription("Normality achieved.")
|
||||
with Alert(variant="destructive", icon="triangle-alert"):
|
||||
AlertTitle("Display Department")
|
||||
AlertDescription("Beware of the leopard.")
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Prefect Horizon Config")
|
||||
with CardContent():
|
||||
with Column(gap=3):
|
||||
Switch(
|
||||
label="Auto-scale agents",
|
||||
value=True,
|
||||
name="autoscale",
|
||||
)
|
||||
Separator()
|
||||
Switch(
|
||||
label="Code Mode",
|
||||
value=True,
|
||||
name="code_mode",
|
||||
)
|
||||
Separator()
|
||||
Switch(
|
||||
label="Tool call caching",
|
||||
value=False,
|
||||
name="cache",
|
||||
)
|
||||
with CardFooter():
|
||||
Button(
|
||||
"Save Preferences",
|
||||
on_click=ShowToast("Preferences saved!"),
|
||||
)
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Travel Class")
|
||||
with CardContent():
|
||||
with RadioGroup(name="travel_class"):
|
||||
Radio(option="economy", label="Economy")
|
||||
Radio(option="business", label="Business Class")
|
||||
Radio(
|
||||
option="improbability",
|
||||
label="Infinite Improbability",
|
||||
value=True,
|
||||
)
|
||||
|
||||
# ── Cols 3–4: summary row, chart, then 2-col grid below ─────────
|
||||
with GridItem(css_class="md:col-span-2"):
|
||||
with Column(gap=4):
|
||||
with Grid(columns=2, gap=4, css_class="h-32"):
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Context Window")
|
||||
with CardContent():
|
||||
with Column(
|
||||
gap=6,
|
||||
justify="center",
|
||||
css_class="h-full",
|
||||
):
|
||||
with Row(
|
||||
align="center",
|
||||
css_class="justify-between",
|
||||
):
|
||||
Text(f"{ctx_pct}% used")
|
||||
Muted(f"{ctx_pct * 2}k / 200k tokens")
|
||||
with Tooltip(
|
||||
"Auto-compact buffer: 12%",
|
||||
delay=0,
|
||||
):
|
||||
Progress(
|
||||
value=ctx_pct,
|
||||
max=100,
|
||||
variant=ctx_variant,
|
||||
)
|
||||
with Card(css_class="pb-0 gap-0"):
|
||||
with CardContent():
|
||||
Metric(
|
||||
label="Fjords designed",
|
||||
value="1,847",
|
||||
delta="+3 coastlines",
|
||||
)
|
||||
Sparkline(
|
||||
data=[
|
||||
820,
|
||||
950,
|
||||
1100,
|
||||
980,
|
||||
1250,
|
||||
1400,
|
||||
1350,
|
||||
1500,
|
||||
1680,
|
||||
1847,
|
||||
],
|
||||
variant="success",
|
||||
fill=True,
|
||||
css_class="h-16",
|
||||
)
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Towel Incidents")
|
||||
with CardContent():
|
||||
BarChart(
|
||||
data=[
|
||||
{"month": "Jan", "lost": 8, "found": 5},
|
||||
{"month": "Feb", "lost": 24, "found": 15},
|
||||
{"month": "Mar", "lost": 12, "found": 28},
|
||||
{"month": "Apr", "lost": 35, "found": 19},
|
||||
{"month": "May", "lost": 18, "found": 38},
|
||||
{"month": "Jun", "lost": 42, "found": 30},
|
||||
],
|
||||
series=[
|
||||
ChartSeries(dataKey="lost", label="Lost"),
|
||||
ChartSeries(dataKey="found", label="Found"),
|
||||
],
|
||||
x_axis="month",
|
||||
height=200,
|
||||
bar_radius=4,
|
||||
show_legend=True,
|
||||
show_tooltip=True,
|
||||
show_grid=True,
|
||||
)
|
||||
|
||||
with Grid(columns=2, gap=4):
|
||||
with Column(gap=4):
|
||||
with Card():
|
||||
with CardContent():
|
||||
with Column(gap=2):
|
||||
Checkbox(label="Towel packed", value=True)
|
||||
Checkbox(label="Guide charged", value=True)
|
||||
Checkbox(
|
||||
label="Babel fish inserted",
|
||||
value=False,
|
||||
)
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Marvin's Mood")
|
||||
with CardContent():
|
||||
with Column(gap=3):
|
||||
P("How's life?")
|
||||
with Column(gap=2):
|
||||
Button(
|
||||
"Meh",
|
||||
on_click=ShowToast(
|
||||
"Noted. Enthusiasm levels nominal."
|
||||
),
|
||||
)
|
||||
Button(
|
||||
"Depressed",
|
||||
variant="info",
|
||||
on_click=ShowToast(
|
||||
"I think you ought to "
|
||||
"know I'm feeling very "
|
||||
"depressed."
|
||||
),
|
||||
)
|
||||
Button(
|
||||
"Don't talk to me about life",
|
||||
variant="warning",
|
||||
on_click=ShowToast(
|
||||
"Brain the size of a "
|
||||
"planet and they ask me "
|
||||
"to pick up a piece of "
|
||||
"paper."
|
||||
),
|
||||
)
|
||||
|
||||
with Column(gap=4):
|
||||
with Card():
|
||||
with CardContent():
|
||||
with Row(gap=2, align="center"):
|
||||
Loader(variant="dots", size="sm")
|
||||
Muted("Marvin is thinking...")
|
||||
with Card():
|
||||
with CardContent():
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(
|
||||
key="crew",
|
||||
header="Crew",
|
||||
sortable=True,
|
||||
),
|
||||
DataTableColumn(
|
||||
key="species",
|
||||
header="Species",
|
||||
sortable=True,
|
||||
),
|
||||
DataTableColumn(
|
||||
key="towel",
|
||||
header="Towel?",
|
||||
sortable=True,
|
||||
),
|
||||
DataTableColumn(
|
||||
key="status",
|
||||
header="Status",
|
||||
sortable=True,
|
||||
),
|
||||
],
|
||||
rows=[
|
||||
{
|
||||
"crew": "Arthur Dent",
|
||||
"species": "Human",
|
||||
"towel": "Yes",
|
||||
"status": "Confused",
|
||||
},
|
||||
{
|
||||
"crew": "Ford Prefect",
|
||||
"species": "Betelgeusian",
|
||||
"towel": "Always",
|
||||
"status": "Drinking",
|
||||
},
|
||||
{
|
||||
"crew": "Zaphod",
|
||||
"species": "Betelgeusian",
|
||||
"towel": "Lost it",
|
||||
"status": "Presidential",
|
||||
},
|
||||
{
|
||||
"crew": "Trillian",
|
||||
"species": "Human",
|
||||
"towel": "Yes",
|
||||
"status": "Navigating",
|
||||
},
|
||||
{
|
||||
"crew": "Marvin",
|
||||
"species": "Android",
|
||||
"towel": "No point",
|
||||
"status": "Depressed",
|
||||
},
|
||||
{
|
||||
"crew": "Slartibartfast",
|
||||
"species": "Magrathean",
|
||||
"towel": "Somewhere",
|
||||
"status": "Designing",
|
||||
},
|
||||
],
|
||||
search=True,
|
||||
paginated=False,
|
||||
)
|
||||
21
docs/v3/apps/demos/pie-chart.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column
|
||||
from prefab_ui.components.charts import PieChart
|
||||
|
||||
data = [
|
||||
{"category": "Bug", "count": 42},
|
||||
{"category": "Feature", "count": 28},
|
||||
{"category": "Docs", "count": 15},
|
||||
{"category": "Infra", "count": 10},
|
||||
]
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(css_class="p-6"):
|
||||
PieChart(
|
||||
data=data,
|
||||
data_key="count",
|
||||
name_key="category",
|
||||
inner_radius=50,
|
||||
show_legend=True,
|
||||
height=240,
|
||||
)
|
||||
66
docs/v3/apps/demos/reactive.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
Column,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
Switch,
|
||||
Text,
|
||||
)
|
||||
from prefab_ui.components.charts import BarChart, ChartSeries
|
||||
from prefab_ui.components.control_flow import If
|
||||
from prefab_ui.components.metric import Metric
|
||||
from prefab_ui.rx import Rx
|
||||
|
||||
region = Rx("region")
|
||||
|
||||
north = [
|
||||
{"month": "Jan", "sales": 22000},
|
||||
{"month": "Feb", "sales": 25500},
|
||||
{"month": "Mar", "sales": 24200},
|
||||
]
|
||||
south = [
|
||||
{"month": "Jan", "sales": 5800},
|
||||
{"month": "Feb", "sales": 6400},
|
||||
{"month": "Mar", "sales": 5600},
|
||||
]
|
||||
west = [
|
||||
{"month": "Jan", "sales": 6000},
|
||||
{"month": "Feb", "sales": 6000},
|
||||
{"month": "Mar", "sales": 5600},
|
||||
]
|
||||
|
||||
with PrefabApp(
|
||||
state={
|
||||
"region": "north",
|
||||
"north": north,
|
||||
"south": south,
|
||||
"west": west,
|
||||
"show_target": True,
|
||||
},
|
||||
) as app:
|
||||
with Column(
|
||||
gap=4,
|
||||
css_class="p-6",
|
||||
let={
|
||||
"data": "{{ region == 'south' ? south : region == 'west' ? west : north }}",
|
||||
},
|
||||
):
|
||||
with Row(gap=4, align="center"):
|
||||
with Select(name="region", css_class="w-40"):
|
||||
SelectOption(value="north", label="North")
|
||||
SelectOption(value="south", label="South")
|
||||
SelectOption(value="west", label="West")
|
||||
Switch(name="show_target", css_class="ml-auto")
|
||||
Text("Show target", css_class="text-sm text-muted-foreground")
|
||||
BarChart(
|
||||
data=Rx("data"),
|
||||
series=[ChartSeries(data_key="sales", label="Sales")],
|
||||
x_axis="month",
|
||||
height=200,
|
||||
)
|
||||
with If(Rx("show_target")):
|
||||
Metric(
|
||||
label="Q1 Target",
|
||||
value="$75,000",
|
||||
)
|
||||
116
docs/v3/apps/demos/team-directory-reactive.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
from collections import Counter
|
||||
|
||||
from prefab_ui.actions import SetState
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
H3,
|
||||
Badge,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Column,
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
Grid,
|
||||
Row,
|
||||
Small,
|
||||
Text,
|
||||
)
|
||||
from prefab_ui.components.charts import PieChart
|
||||
from prefab_ui.components.control_flow import If
|
||||
from prefab_ui.rx import STATE, Rx
|
||||
|
||||
MEMBERS = [
|
||||
{
|
||||
"name": "Alice Chen",
|
||||
"role": "Staff Engineer",
|
||||
"office": "San Francisco",
|
||||
"email": "alice@company.com",
|
||||
"projects": 3,
|
||||
},
|
||||
{
|
||||
"name": "Bob Martinez",
|
||||
"role": "Lead Designer",
|
||||
"office": "New York",
|
||||
"email": "bob@company.com",
|
||||
"projects": 5,
|
||||
},
|
||||
{
|
||||
"name": "Carol Johnson",
|
||||
"role": "Senior Engineer",
|
||||
"office": "London",
|
||||
"email": "carol@company.com",
|
||||
"projects": 2,
|
||||
},
|
||||
{
|
||||
"name": "David Kim",
|
||||
"role": "Product Manager",
|
||||
"office": "San Francisco",
|
||||
"email": "david@company.com",
|
||||
"projects": 7,
|
||||
},
|
||||
{
|
||||
"name": "Eva Mueller",
|
||||
"role": "Engineer",
|
||||
"office": "Berlin",
|
||||
"email": "eva@company.com",
|
||||
"projects": 1,
|
||||
},
|
||||
{
|
||||
"name": "Frank Lee",
|
||||
"role": "Data Scientist",
|
||||
"office": "San Francisco",
|
||||
"email": "frank@company.com",
|
||||
"projects": 4,
|
||||
},
|
||||
{
|
||||
"name": "Grace Park",
|
||||
"role": "Engineering Manager",
|
||||
"office": "New York",
|
||||
"email": "grace@company.com",
|
||||
"projects": 6,
|
||||
},
|
||||
]
|
||||
|
||||
OFFICE_COUNTS = [
|
||||
{"office": office, "count": count}
|
||||
for office, count in Counter(m["office"] for m in MEMBERS).items()
|
||||
]
|
||||
|
||||
with PrefabApp(state={"selected": None}) as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
with Grid(columns=[1, 2], gap=4):
|
||||
PieChart(
|
||||
data=OFFICE_COUNTS,
|
||||
data_key="count",
|
||||
name_key="office",
|
||||
show_legend=True,
|
||||
)
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="role", header="Role", sortable=True),
|
||||
DataTableColumn(key="office", header="Office", sortable=True),
|
||||
],
|
||||
rows=MEMBERS,
|
||||
search=True,
|
||||
on_row_click=SetState("selected", Rx("$event")),
|
||||
)
|
||||
|
||||
with If(STATE.selected):
|
||||
with Card():
|
||||
with CardHeader():
|
||||
with Row(gap=2, align="center"):
|
||||
H3(Rx("selected.name"))
|
||||
Badge(Rx("selected.office"))
|
||||
with CardContent():
|
||||
with Grid(columns=3, gap=4):
|
||||
with Column(gap=0):
|
||||
Small("Role")
|
||||
Text(Rx("selected.role"))
|
||||
with Column(gap=0):
|
||||
Small("Email")
|
||||
Text(Rx("selected.email"))
|
||||
with Column(gap=0):
|
||||
Small("Active Projects")
|
||||
Text(Rx("selected.projects"))
|
||||
39
docs/v3/apps/demos/team-directory.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from collections import Counter
|
||||
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, DataTable, DataTableColumn, Grid
|
||||
from prefab_ui.components.charts import PieChart
|
||||
|
||||
members = [
|
||||
{"name": "Alice Chen", "role": "Staff Engineer", "office": "San Francisco"},
|
||||
{"name": "Bob Martinez", "role": "Lead Designer", "office": "New York"},
|
||||
{"name": "Carol Johnson", "role": "Senior Engineer", "office": "London"},
|
||||
{"name": "David Kim", "role": "Product Manager", "office": "San Francisco"},
|
||||
{"name": "Eva Mueller", "role": "Engineer", "office": "Berlin"},
|
||||
{"name": "Frank Lee", "role": "Data Scientist", "office": "San Francisco"},
|
||||
{"name": "Grace Park", "role": "Engineering Manager", "office": "New York"},
|
||||
]
|
||||
|
||||
office_counts = [
|
||||
{"office": office, "count": count}
|
||||
for office, count in Counter(m["office"] for m in members).items()
|
||||
]
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
with Grid(columns=[1, 2], gap=4):
|
||||
PieChart(
|
||||
data=office_counts,
|
||||
data_key="count",
|
||||
name_key="office",
|
||||
show_legend=True,
|
||||
)
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="role", header="Role", sortable=True),
|
||||
DataTableColumn(key="office", header="Office", sortable=True),
|
||||
],
|
||||
rows=members,
|
||||
search=True,
|
||||
)
|
||||
65
docs/v3/apps/development.mdx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
title: Development
|
||||
sidebarTitle: Development
|
||||
description: Preview and test your app tools locally without a full MCP host.
|
||||
icon: flask
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
<Frame>
|
||||
<img src="/apps/images/dev-app.png" alt="The dev UI showing a rendered Prefab app with the MCP inspector panel" />
|
||||
</Frame>
|
||||
|
||||
`fastmcp dev apps` gives you a browser preview for your app tools without needing an MCP host client. It starts your server and a local dev UI side by side: you pick a tool, fill in its arguments, and the rendered result opens in a new tab.
|
||||
|
||||
Works with both [Interactive Tools](/apps/prefab) and [custom HTML apps](/apps/low-level).
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
fastmcp dev apps server.py
|
||||
```
|
||||
|
||||
The dev UI opens at `http://localhost:8080`. Your MCP server runs on port 8000 with auto-reload enabled by default — save a file and the server restarts automatically.
|
||||
|
||||
## How it works
|
||||
|
||||
The dev server does three things:
|
||||
|
||||
The **picker page** connects to your MCP server, finds all tools with UI metadata, and renders a form for each one. The forms are auto-generated from the tool's input schema — text fields, dropdowns, checkboxes, all wired up.
|
||||
|
||||
When you submit a form, the dev server **calls your tool** via the MCP protocol and opens the result in a new tab. The result page loads the tool's UI resource (the Prefab renderer or your custom HTML) inside an AppBridge — the same protocol that real MCP hosts use.
|
||||
|
||||
A **reverse proxy** on `/mcp` forwards requests from the browser to your MCP server, avoiding CORS issues that would otherwise block the iframe-based renderer from talking to a different port.
|
||||
|
||||
## MCP inspector
|
||||
|
||||
The dev UI includes an inspector panel on the left side that captures MCP traffic in real time. It shows JSON-RPC messages flowing between the browser and your server — requests, responses, and AppBridge `postMessage` traffic.
|
||||
|
||||
Each entry shows direction, method, timing, and a smart summary. Click any entry to expand the full JSON-RPC body. The panel auto-scrolls to new messages unless you've scrolled up to inspect older ones.
|
||||
|
||||
The inspector is useful for debugging: you can see exactly what arguments your tool received, what it returned, and how the AppBridge communicated with the renderer.
|
||||
|
||||
## Options
|
||||
|
||||
```bash
|
||||
fastmcp dev apps server.py:mcp --mcp-port 9000 --dev-port 9090 --no-reload
|
||||
```
|
||||
|
||||
| Option | Flag | Default | Description |
|
||||
| ------ | ---- | ------- | ----------- |
|
||||
| MCP Port | `--mcp-port` | `8000` | Port for your MCP server |
|
||||
| Dev Port | `--dev-port` | `8080` | Port for the dev UI |
|
||||
| Auto-Reload | `--reload` / `--no-reload` | On | Watch files and restart the server on changes |
|
||||
|
||||
## Multiple tools
|
||||
|
||||
If your server has multiple app tools, the picker shows a dropdown. Each tool gets its own form and launch button. The tool's `title` is displayed when available, falling back to the tool name.
|
||||
|
||||
```bash
|
||||
# Server with multiple app tools
|
||||
fastmcp dev apps examples/apps/contacts/contacts_server.py
|
||||
```
|
||||
92
docs/v3/apps/examples.mdx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
---
|
||||
title: Examples
|
||||
sidebarTitle: Examples
|
||||
description: Example apps you can run right now.
|
||||
icon: images
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
Each tile below is a working FastMCP server you can run with `fastmcp dev apps` or connect to from any MCP host. Source lives in `examples/apps/` in the repository.
|
||||
|
||||
<Columns cols={2}>
|
||||
<Tile href="#sales-dashboard" title="Sales Dashboard" description="Metrics, charts, and deal pipeline">
|
||||
<div style={{overflow: "hidden", width: "100%"}}>
|
||||
<img src="/apps/images/app-example-sales-dashboard.png" />
|
||||
</div>
|
||||
</Tile>
|
||||
<Tile href="#system-monitor" title="System Monitor" description="Live CPU, memory, disk with auto-refresh">
|
||||
<img src="/apps/images/app-example-system-dashboard.png" />
|
||||
</Tile>
|
||||
<Tile href="#quiz" title="Quiz" description="LLM-generated trivia with scoring">
|
||||
<img src="/apps/images/app-example-quiz.png" />
|
||||
</Tile>
|
||||
<Tile href="#interactive-map" title="Interactive Map" description="Geocoded addresses on Leaflet">
|
||||
<img src="/apps/images/app-example-map.png" />
|
||||
</Tile>
|
||||
<Tile href="/apps/providers/file-upload" title="File Upload" description="Drag-and-drop upload provider">
|
||||
<img src="/apps/images/app-file-upload.png" />
|
||||
</Tile>
|
||||
<Tile href="/apps/providers/approval" title="Approval" description="Human-in-the-loop confirmation">
|
||||
<img src="/apps/images/app-approval.png" />
|
||||
</Tile>
|
||||
<Tile href="/apps/providers/choice" title="Choice" description="Clickable option selection">
|
||||
<img src="/apps/images/app-choice.png" />
|
||||
</Tile>
|
||||
<Tile href="/apps/providers/form" title="Form Input" description="Pydantic model forms">
|
||||
<img src="/apps/images/app-form.png" />
|
||||
</Tile>
|
||||
<Tile href="/apps/generative" title="Generative UI" description="LLM writes the UI at runtime">
|
||||
<img src="/apps/images/app-showcase.png" />
|
||||
</Tile>
|
||||
</Columns>
|
||||
|
||||
## Running the examples
|
||||
|
||||
Preview any example in your browser with the dev server:
|
||||
|
||||
```bash
|
||||
pip install "fastmcp[apps]"
|
||||
fastmcp dev apps examples/apps/sales_dashboard/sales_dashboard_server.py
|
||||
```
|
||||
|
||||
The dev UI lets you pick a tool and fill in arguments. In a real deployment the LLM provides those arguments from conversation context — the quiz example especially shines when connected to a host like Goose or Claude Desktop, where the LLM generates the questions itself.
|
||||
|
||||
## Standalone apps
|
||||
|
||||
### Sales dashboard
|
||||
|
||||
A full dashboard with KPI metrics, revenue trends, segment breakdown, and a deal pipeline table. Shows what you can build with a single `app=True` tool and Prefab's chart and data components.
|
||||
|
||||
```bash
|
||||
fastmcp dev apps examples/apps/sales_dashboard/sales_dashboard_server.py
|
||||
```
|
||||
|
||||
### System monitor
|
||||
|
||||
Reads live CPU, memory, and disk stats from your machine using `psutil`. Auto-refreshes via `SetInterval` calling a backend tool, with a dropdown to control the refresh rate. The chart accumulates up to 100 data points over time.
|
||||
|
||||
```bash
|
||||
pip install psutil
|
||||
fastmcp dev apps examples/apps/system_monitor/system_monitor_server.py
|
||||
```
|
||||
|
||||
### Quiz
|
||||
|
||||
The LLM generates trivia questions and passes them to the tool. The user answers via buttons, sees correct/incorrect feedback, and tracks score across questions. Demonstrates multi-turn client-side state with FastMCPApp.
|
||||
|
||||
```bash
|
||||
fastmcp dev apps examples/apps/quiz/quiz_server.py
|
||||
```
|
||||
|
||||
### Interactive map
|
||||
|
||||
Accepts addresses or place names, geocodes them via OpenStreetMap Nominatim (free, no API key), and renders an interactive Leaflet map using Prefab's `Embed` component with inline HTML. A reminder that Prefab apps can break out of built-in components when they need to.
|
||||
|
||||
```bash
|
||||
fastmcp dev apps examples/apps/map/map_server.py
|
||||
```
|
||||
|
||||
For ready-made building blocks like approvals, choice pickers, file uploads, and Pydantic forms, see the [Providers](/apps/providers/approval) group.
|
||||
470
docs/v3/apps/fastmcp-app.mdx
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
---
|
||||
title: FastMCPApp
|
||||
sidebarTitle: FastMCPApp
|
||||
description: Wire an interactive UI to backend tools with managed visibility and composition safety.
|
||||
icon: puzzle-piece
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx'
|
||||
import { PrefabDemoFrame } from '/snippets/prefab-demo-frame.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
<PrefabPinWarning />
|
||||
|
||||
<PrefabDemoFrame demo="contacts" height="650px" title="Contacts app demo" />
|
||||
|
||||
Search a list, fill out a form, click save, the list updates. That pattern — UI that reads and writes data on the server — needs two things: backend tools that actually do the work, and a way to call them from the UI. `FastMCPApp` handles the wiring.
|
||||
|
||||
You'll build up to the contacts app above by the end of this page. Let's start with something smaller.
|
||||
|
||||
## A minimal interactive app
|
||||
|
||||
The smallest interactive app: a form that saves a note, and a list that updates when the user submits.
|
||||
|
||||
```python
|
||||
from prefab_ui.actions import SetState, ShowToast
|
||||
from prefab_ui.actions.mcp import CallTool
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
Badge, Button, Column, ForEach, Form, Heading,
|
||||
Input, Row, Separator, Text,
|
||||
)
|
||||
from prefab_ui.rx import RESULT
|
||||
from fastmcp import FastMCP, FastMCPApp
|
||||
|
||||
app = FastMCPApp("Notes")
|
||||
notes_db: list[dict] = []
|
||||
|
||||
|
||||
@app.tool()
|
||||
def add_note(title: str, body: str) -> list[dict]:
|
||||
"""Save a note and return all notes."""
|
||||
notes_db.append({"title": title, "body": body})
|
||||
return list(notes_db)
|
||||
|
||||
|
||||
@app.ui()
|
||||
def notes_app() -> PrefabApp:
|
||||
"""Open the notes app."""
|
||||
with Column(gap=6, css_class="p-6") as view:
|
||||
Heading("Notes")
|
||||
|
||||
with ForEach("notes") as note:
|
||||
with Row(gap=2, align="center"):
|
||||
Text(note.title, css_class="font-semibold")
|
||||
Badge(note.body)
|
||||
|
||||
Separator()
|
||||
|
||||
with Form(
|
||||
on_submit=CallTool(
|
||||
"add_note",
|
||||
on_success=[
|
||||
SetState("notes", RESULT),
|
||||
ShowToast("Note saved!", variant="success"),
|
||||
],
|
||||
on_error=ShowToast("Failed to save", variant="error"),
|
||||
)
|
||||
):
|
||||
Input(name="title", label="Title", required=True)
|
||||
Input(name="body", label="Body", required=True)
|
||||
Button("Add Note")
|
||||
|
||||
return PrefabApp(view=view, state={"notes": list(notes_db)})
|
||||
|
||||
|
||||
mcp = FastMCP("Notes Server", providers=[app])
|
||||
```
|
||||
|
||||
The model sees one tool: `notes_app`. Calling it opens the UI. When the user submits the form, `CallTool("add_note")` fires, the server saves the note, returns the updated list, and `SetState("notes", RESULT)` writes that list back into state. `ForEach("notes")` re-renders. The model never sees `add_note` — it's UI-only.
|
||||
|
||||
## Why not just `@mcp.tool(app=True)`?
|
||||
|
||||
A fair question. Any [Interactive Tool](/apps/prefab) can call a server tool — there's nothing stopping you from putting `CallTool("add_note")` inside a regular `@mcp.tool(app=True)`. It works for one or two tools. Things get harder once the app grows:
|
||||
|
||||
- Which tools should the model see, and which are UI-only?
|
||||
- What happens to `CallTool("add_note")` when you mount this server under a namespace and the tool becomes `notes_add_note`?
|
||||
- How do you keep it all wired correctly as you compose servers?
|
||||
|
||||
`FastMCPApp` owns these concerns. Entry points register as model-visible. Backend tools register as UI-only by default. Backend tools get globally stable identifiers that survive namespacing, and `CallTool` accepts function references, so references stay valid when you compose servers.
|
||||
|
||||
The rest of this page covers each piece in turn.
|
||||
|
||||
## `@app.ui()` — entry points
|
||||
|
||||
Entry points are what the model sees. They return a `PrefabApp` and default to `visibility=["model"]`, showing up in the LLM tool list but not callable from within the UI.
|
||||
|
||||
```python
|
||||
@app.ui()
|
||||
def dashboard() -> PrefabApp:
|
||||
"""The model calls this to open the dashboard."""
|
||||
with Column(gap=4, css_class="p-6") as view:
|
||||
Heading("Dashboard")
|
||||
...
|
||||
return PrefabApp(view=view)
|
||||
```
|
||||
|
||||
`@app.ui()` supports the same options as `@mcp.tool`: `name`, `description`, `title`, `tags`, `icons`, `auth`, and `timeout`.
|
||||
|
||||
## `@app.tool()` — backend tools
|
||||
|
||||
Backend tools do the work. By default they're visible only to the UI (`visibility=["app"]`), not the model.
|
||||
|
||||
```python
|
||||
@app.tool()
|
||||
def save_contact(name: str, email: str) -> list[dict]:
|
||||
"""Save a contact and return the updated list."""
|
||||
db.append({"name": name, "email": email})
|
||||
return list(db)
|
||||
```
|
||||
|
||||
If you want a tool callable by both the model and the UI, pass `model=True`:
|
||||
|
||||
```python
|
||||
@app.tool(model=True)
|
||||
def list_contacts() -> list[dict]:
|
||||
"""Both the model and the UI can call this."""
|
||||
return list(db)
|
||||
```
|
||||
|
||||
Backend tools support `name`, `description`, `auth`, and `timeout`.
|
||||
|
||||
## `CallTool` — UI → backend
|
||||
|
||||
`CallTool` is how the UI invokes a backend tool. Pass the tool's name (or a direct function reference):
|
||||
|
||||
```python
|
||||
from prefab_ui.actions.mcp import CallTool
|
||||
|
||||
CallTool("save_contact", arguments={"name": "Alice", "email": "alice@example.com"})
|
||||
|
||||
# Or a function reference — resolves to a stable global key
|
||||
CallTool(save_contact, arguments={...})
|
||||
```
|
||||
|
||||
Arguments can reference state with `Rx`:
|
||||
|
||||
```python
|
||||
from prefab_ui.rx import STATE
|
||||
|
||||
CallTool("search", arguments={"query": STATE.search_term})
|
||||
```
|
||||
|
||||
### Handling results
|
||||
|
||||
Server calls are async. Use `on_success` and `on_error` callbacks:
|
||||
|
||||
```python
|
||||
from prefab_ui.actions import SetState, ShowToast
|
||||
from prefab_ui.rx import RESULT
|
||||
|
||||
CallTool(
|
||||
"save_contact",
|
||||
on_success=[
|
||||
SetState("contacts", RESULT),
|
||||
ShowToast("Saved!", variant="success"),
|
||||
],
|
||||
on_error=ShowToast("Something went wrong", variant="error"),
|
||||
)
|
||||
```
|
||||
|
||||
`RESULT` is a reactive reference to the tool's return value, available inside `on_success`. `ERROR` (from `prefab_ui.rx`) is the counterpart inside `on_error`. Callbacks can be a single action or a list; they execute in order and short-circuit on error.
|
||||
|
||||
### `result_key` shorthand
|
||||
|
||||
When a tool's return value should replace a state key, use `result_key`:
|
||||
|
||||
```python
|
||||
CallTool("list_contacts", result_key="contacts")
|
||||
|
||||
# same as:
|
||||
CallTool("list_contacts", on_success=SetState("contacts", RESULT))
|
||||
```
|
||||
|
||||
## Actions
|
||||
|
||||
`CallTool` is one of several actions. Actions attach to handlers like `on_click`, `on_submit`, and `on_change`.
|
||||
|
||||
Client-side actions run instantly in the browser, no server round-trip:
|
||||
|
||||
```python
|
||||
from prefab_ui.actions import SetState, ToggleState, AppendState, PopState, ShowToast
|
||||
|
||||
SetState("count", 42)
|
||||
ToggleState("expanded")
|
||||
AppendState("items", {"name": "New Item"})
|
||||
PopState("items", 0)
|
||||
ShowToast("Done!", variant="success")
|
||||
```
|
||||
|
||||
Pass a list to chain actions:
|
||||
|
||||
```python
|
||||
Button(
|
||||
"Reset",
|
||||
on_click=[
|
||||
SetState("query", ""),
|
||||
SetState("results", []),
|
||||
ShowToast("Cleared"),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### Loading states
|
||||
|
||||
A common pattern: disable a button and show a spinner while a call is in flight.
|
||||
|
||||
```python
|
||||
from prefab_ui.rx import Rx
|
||||
|
||||
saving = Rx("saving")
|
||||
|
||||
Button(
|
||||
saving.then("Saving...", "Save"),
|
||||
disabled=saving,
|
||||
on_click=[
|
||||
SetState("saving", True),
|
||||
CallTool(
|
||||
"save_data",
|
||||
on_success=[
|
||||
SetState("saving", False),
|
||||
SetState("result", RESULT),
|
||||
ShowToast("Saved!", variant="success"),
|
||||
],
|
||||
on_error=[
|
||||
SetState("saving", False),
|
||||
ShowToast("Failed", variant="error"),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# PrefabApp(view=view, state={"saving": False, ...})
|
||||
```
|
||||
|
||||
## Forms
|
||||
|
||||
Forms collect input and submit it to a tool. When submitted, named input values become the tool's arguments.
|
||||
|
||||
### Manual forms
|
||||
|
||||
```python
|
||||
from prefab_ui.components import Form, Input, Select, SelectOption, Textarea, Button
|
||||
|
||||
with Form(
|
||||
on_submit=CallTool(
|
||||
"create_ticket",
|
||||
on_success=ShowToast("Ticket created!", variant="success"),
|
||||
)
|
||||
):
|
||||
Input(name="title", label="Title", required=True)
|
||||
with Select(name="priority", label="Priority"):
|
||||
SelectOption("Low", value="low")
|
||||
SelectOption("Medium", value="medium")
|
||||
SelectOption("High", value="high")
|
||||
Textarea(name="description", label="Description")
|
||||
Button("Create Ticket")
|
||||
```
|
||||
|
||||
On submit, `CallTool` receives `{"title": ..., "priority": ..., "description": ...}`.
|
||||
|
||||
### Forms from Pydantic models
|
||||
|
||||
For structured input, `Form.from_model()` generates the whole form — inputs, labels, validation:
|
||||
|
||||
```python
|
||||
from typing import Literal
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class BugReport(BaseModel):
|
||||
title: str = Field(title="Bug Title")
|
||||
severity: Literal["low", "medium", "high", "critical"] = Field(
|
||||
title="Severity", default="medium"
|
||||
)
|
||||
description: str = Field(title="Description")
|
||||
|
||||
|
||||
@app.ui()
|
||||
def report_bug() -> PrefabApp:
|
||||
with Column(gap=4, css_class="p-6") as view:
|
||||
Heading("Report a Bug")
|
||||
Form.from_model(
|
||||
BugReport,
|
||||
on_submit=CallTool(
|
||||
"create_bug",
|
||||
on_success=ShowToast("Bug filed!", variant="success"),
|
||||
),
|
||||
)
|
||||
return PrefabApp(view=view)
|
||||
|
||||
|
||||
@app.tool()
|
||||
def create_bug(data: BugReport) -> str:
|
||||
return f"Created: {data.title}"
|
||||
```
|
||||
|
||||
`str` becomes a text input, `Literal` becomes a select, `bool` becomes a checkbox. Field titles and defaults are respected.
|
||||
|
||||
## Composition and namespacing
|
||||
|
||||
The reason `FastMCPApp` exists — and why you'd pick it over plain `@mcp.tool(app=True)` with string-based `CallTool` — is composition safety.
|
||||
|
||||
When you mount a server under a namespace, tool names get prefixed:
|
||||
|
||||
```python
|
||||
platform = FastMCP("Platform")
|
||||
platform.mount("contacts", contacts_server)
|
||||
|
||||
# "save_contact" becomes "contacts_save_contact"
|
||||
```
|
||||
|
||||
`CallTool("save_contact")` would now be broken. But `CallTool(save_contact)` with a function reference resolves to a globally stable identifier that bypasses the namespace. Your app works the same whether standalone or mounted.
|
||||
|
||||
### Mounting
|
||||
|
||||
`FastMCPApp` is a Provider. Add it to a server with `providers=` or `add_provider`:
|
||||
|
||||
```python
|
||||
mcp = FastMCP("Platform", providers=[app])
|
||||
|
||||
# or
|
||||
mcp = FastMCP("Platform")
|
||||
mcp.add_provider(app)
|
||||
```
|
||||
|
||||
Multiple apps can coexist; each gets its own global keys, so there's no collision even if two apps have a tool named `save`.
|
||||
|
||||
```python
|
||||
mcp = FastMCP("Platform", providers=[contacts_app, inventory_app, billing_app])
|
||||
```
|
||||
|
||||
### Running standalone
|
||||
|
||||
For development, `FastMCPApp` has a `run()` shortcut that wraps itself in a temporary `FastMCP` server:
|
||||
|
||||
```python
|
||||
app = FastMCPApp("Contacts")
|
||||
# ... register tools ...
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
```
|
||||
|
||||
## A full example: contact manager
|
||||
|
||||
This brings everything together — entry point, backend tools, Pydantic form, manual form, state, actions, and multi-visibility.
|
||||
|
||||
```python expandable
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from prefab_ui.actions import SetState, ShowToast
|
||||
from prefab_ui.actions.mcp import CallTool
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
Badge, Button, Column, ForEach, Form,
|
||||
Heading, Input, Muted, Row, Separator, Text,
|
||||
)
|
||||
from prefab_ui.rx import RESULT, Rx
|
||||
from pydantic import BaseModel, Field
|
||||
from fastmcp import FastMCP, FastMCPApp
|
||||
|
||||
contacts_db: list[dict] = [
|
||||
{"name": "Arthur Dent", "email": "arthur@earth.com", "category": "Customer"},
|
||||
{"name": "Ford Prefect", "email": "ford@betelgeuse.org", "category": "Partner"},
|
||||
]
|
||||
|
||||
|
||||
class ContactModel(BaseModel):
|
||||
name: str = Field(title="Full Name", min_length=1)
|
||||
email: str = Field(title="Email")
|
||||
category: Literal["Customer", "Vendor", "Partner", "Other"] = "Other"
|
||||
|
||||
|
||||
app = FastMCPApp("Contacts")
|
||||
|
||||
|
||||
@app.tool()
|
||||
def save_contact(data: ContactModel) -> list[dict]:
|
||||
"""Save a new contact and return the updated list."""
|
||||
contacts_db.append(data.model_dump())
|
||||
return list(contacts_db)
|
||||
|
||||
|
||||
@app.tool()
|
||||
def search_contacts(query: str) -> list[dict]:
|
||||
"""Filter contacts by name or email."""
|
||||
q = query.lower()
|
||||
return [
|
||||
c for c in contacts_db
|
||||
if q in c["name"].lower() or q in c["email"].lower()
|
||||
]
|
||||
|
||||
|
||||
@app.tool(model=True)
|
||||
def list_contacts() -> list[dict]:
|
||||
"""Return all contacts. Visible to both the model and the UI."""
|
||||
return list(contacts_db)
|
||||
|
||||
|
||||
@app.ui()
|
||||
def contact_manager() -> PrefabApp:
|
||||
"""Open the contact manager."""
|
||||
with Column(gap=6, css_class="p-6") as view:
|
||||
Heading("Contacts")
|
||||
|
||||
with ForEach("contacts") as contact:
|
||||
with Row(gap=2, align="center"):
|
||||
Text(contact.name, css_class="font-medium")
|
||||
Muted(contact.email)
|
||||
Badge(contact.category)
|
||||
|
||||
Separator()
|
||||
|
||||
Heading("Add Contact", level=3)
|
||||
Form.from_model(
|
||||
ContactModel,
|
||||
on_submit=CallTool(
|
||||
"save_contact",
|
||||
on_success=[
|
||||
SetState("contacts", RESULT),
|
||||
ShowToast("Contact saved!", variant="success"),
|
||||
],
|
||||
on_error=ShowToast("Failed to save", variant="error"),
|
||||
),
|
||||
)
|
||||
|
||||
Separator()
|
||||
|
||||
Heading("Search", level=3)
|
||||
with Form(
|
||||
on_submit=CallTool(
|
||||
"search_contacts",
|
||||
arguments={"query": Rx("query")},
|
||||
on_success=SetState("contacts", RESULT),
|
||||
)
|
||||
):
|
||||
Input(name="query", placeholder="Search by name or email...")
|
||||
Button("Search")
|
||||
|
||||
return PrefabApp(view=view, state={"contacts": list(contacts_db)})
|
||||
|
||||
|
||||
mcp = FastMCP("Contacts Server", providers=[app])
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
Also available as a runnable server at `examples/apps/contacts/contacts_server.py`.
|
||||
|
||||
## Next steps
|
||||
|
||||
- **[Interactive Tools](/apps/prefab)** — the building blocks: charts, tables, dashboards, reactive state
|
||||
- **[Examples](/apps/examples)** — complete working servers
|
||||
- **[Development](/apps/development)** — preview and test app tools locally
|
||||
- **[Prefab UI docs](https://prefab.prefect.io)** — full component reference
|
||||
134
docs/v3/apps/generative.mdx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
---
|
||||
title: Generative UI
|
||||
sidebarTitle: Generative UI
|
||||
description: Let the LLM build custom Prefab UIs on the fly.
|
||||
icon: wand-magic-sparkles
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
<video src="/apps/images/generative-ui.mp4" autoPlay loop muted playsInline style={{width:"100%", borderRadius:"8px", marginBottom:"1rem"}} />
|
||||
|
||||
With Generative UI, the LLM writes the UI code at runtime. Instead of calling a pre-built tool with a fixed shape, the model writes Prefab Python tailored to the current data and request. The user watches the UI stream in as the model generates it.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.generative import GenerativeUI
|
||||
|
||||
mcp = FastMCP("Prefab Studio")
|
||||
mcp.add_provider(GenerativeUI())
|
||||
```
|
||||
|
||||
One provider registers three things:
|
||||
|
||||
- **`generate_prefab_ui`** — a tool that accepts Python code, executes it in a Pyodide sandbox, and renders the result as a Prefab app
|
||||
- **`search_prefab_components`** — a tool the LLM uses to discover what components are available
|
||||
- **The streaming renderer** — a `ui://` resource with browser-side Pyodide that progressively renders partial code as the LLM generates it
|
||||
|
||||
## How it works
|
||||
|
||||
When the LLM calls `generate_prefab_ui`, it writes Prefab Python code into the `code` argument. The MCP Apps protocol creates the renderer iframe in parallel with the tool call, so the app is already running by the time partial arguments start flowing.
|
||||
|
||||
As the LLM generates each token:
|
||||
|
||||
1. The host forwards partial arguments to the app via `ontoolinputpartial`
|
||||
2. The renderer extracts the growing `code` string
|
||||
3. Browser-side Pyodide executes whatever compiles successfully
|
||||
4. The user sees components appear as they're written
|
||||
|
||||
When the LLM finishes, the server runs the complete code in a server-side Pyodide sandbox for validation, and the renderer swaps the streaming preview for the final server-validated result.
|
||||
|
||||
## What the LLM writes
|
||||
|
||||
The tool description includes examples that teach the model the Prefab patterns. A typical generation looks like:
|
||||
|
||||
```python
|
||||
from prefab_ui.components import Column, Row, Heading, Text, Badge, Card, CardContent
|
||||
from prefab_ui.components.charts import BarChart, ChartSeries
|
||||
from prefab_ui.app import PrefabApp
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=6, css_class="p-6"):
|
||||
Heading("Q3 Revenue Report")
|
||||
|
||||
BarChart(
|
||||
data=[
|
||||
{"month": "Jul", "revenue": 42000},
|
||||
{"month": "Aug", "revenue": 51000},
|
||||
{"month": "Sep", "revenue": 63000},
|
||||
],
|
||||
series=[ChartSeries(data_key="revenue", label="Revenue")],
|
||||
x_axis="month",
|
||||
)
|
||||
|
||||
with Row(gap=4):
|
||||
with Card():
|
||||
with CardContent():
|
||||
Text("Total", css_class="text-sm text-muted-foreground")
|
||||
Heading("$156,000")
|
||||
with Card():
|
||||
with CardContent():
|
||||
Text("Growth", css_class="text-sm text-muted-foreground")
|
||||
Badge("+18%", variant="success")
|
||||
```
|
||||
|
||||
The model writes real Python — loops, f-strings, computation, helper functions. Prefab gives it charts, tables, forms, cards, badges, and layout primitives to compose.
|
||||
|
||||
## The component search tool
|
||||
|
||||
Before writing code, the LLM can call `search_prefab_components` to discover what's available:
|
||||
|
||||
```
|
||||
search_prefab_components("Chart")
|
||||
→ 7 components matching 'Chart':
|
||||
AreaChart — from prefab_ui.components.charts import AreaChart
|
||||
BarChart — from prefab_ui.components.charts import BarChart
|
||||
...
|
||||
```
|
||||
|
||||
Passing `detail=True` returns full field descriptions and docstrings. The search tool introspects Prefab classes at runtime, so it's always up to date with the installed version.
|
||||
|
||||
## Passing data
|
||||
|
||||
The `generate_prefab_ui` tool accepts a `data` parameter. Values become global variables in the sandbox:
|
||||
|
||||
```python
|
||||
# The LLM can reference 'sales_data' directly in its code
|
||||
result = await generate_prefab_ui(
|
||||
code="...",
|
||||
data={"sales_data": [{"month": "Jan", "revenue": 42000}, ...]}
|
||||
)
|
||||
```
|
||||
|
||||
This lets the model use data from earlier in the conversation to build visualizations.
|
||||
|
||||
## Configuration
|
||||
|
||||
`GenerativeUI` takes options for customizing tool names:
|
||||
|
||||
```python
|
||||
GenerativeUI(
|
||||
tool_name="generate_prefab_ui", # default
|
||||
components_tool_name="search_prefab_components", # default
|
||||
include_components_tool=True, # default
|
||||
)
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
Generative UI needs `fastmcp[apps]`, which pulls in `prefab-ui`. The server-side Pyodide sandbox (for final validation) requires Deno — it installs automatically on first use.
|
||||
|
||||
The streaming renderer loads Pyodide from CDN in the browser. The CSP is configured automatically by the provider — no manual setup.
|
||||
|
||||
## Sandbox limitations
|
||||
|
||||
The Pyodide sandbox includes the Python standard library and Prefab. External packages (NumPy, pandas, requests, etc.) are **not available** — the LLM's code must work with only built-in Python and Prefab. If the LLM imports something unavailable, the sandbox raises `ImportError`.
|
||||
|
||||
## Next steps
|
||||
|
||||
- **[Interactive Tools](/apps/prefab)** — the component building blocks the LLM will use
|
||||
- **[Prefab component reference](https://prefab.prefect.io/docs/components)** — full component library
|
||||
- **[Development](/apps/development)** — preview generative tools locally with `fastmcp dev apps`
|
||||
BIN
docs/v3/apps/images/app-approval.png
Normal file
|
After Width: | Height: | Size: 571 KiB |
BIN
docs/v3/apps/images/app-chart.png
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
docs/v3/apps/images/app-choice.png
Normal file
|
After Width: | Height: | Size: 536 KiB |
BIN
docs/v3/apps/images/app-contacts.png
Normal file
|
After Width: | Height: | Size: 587 KiB |
0
docs/v3/apps/images/app-datatable.png
Normal file
BIN
docs/v3/apps/images/app-example-map.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/v3/apps/images/app-example-quiz.png
Normal file
|
After Width: | Height: | Size: 586 KiB |
BIN
docs/v3/apps/images/app-example-sales-dashboard.png
Normal file
|
After Width: | Height: | Size: 683 KiB |
BIN
docs/v3/apps/images/app-example-system-dashboard.png
Normal file
|
After Width: | Height: | Size: 745 KiB |
BIN
docs/v3/apps/images/app-file-upload.png
Normal file
|
After Width: | Height: | Size: 555 KiB |
BIN
docs/v3/apps/images/app-form.png
Normal file
|
After Width: | Height: | Size: 580 KiB |
BIN
docs/v3/apps/images/app-greet.png
Normal file
|
After Width: | Height: | Size: 267 KiB |
BIN
docs/v3/apps/images/app-overview.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/v3/apps/images/app-quickstart-dev-2.png
Normal file
|
After Width: | Height: | Size: 586 KiB |
BIN
docs/v3/apps/images/app-quickstart-dev.png
Normal file
|
After Width: | Height: | Size: 652 KiB |
BIN
docs/v3/apps/images/app-quickstart.png
Normal file
|
After Width: | Height: | Size: 639 KiB |
BIN
docs/v3/apps/images/app-showcase.png
Normal file
|
After Width: | Height: | Size: 1,001 KiB |
BIN
docs/v3/apps/images/dev-app.png
Normal file
|
After Width: | Height: | Size: 322 KiB |
BIN
docs/v3/apps/images/generative-ui.mp4
Normal file
304
docs/v3/apps/low-level.mdx
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
---
|
||||
title: Custom HTML Apps
|
||||
sidebarTitle: Custom HTML
|
||||
description: Build apps with your own HTML, CSS, and JavaScript using the MCP Apps extension directly.
|
||||
icon: code
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
Everything on this page is for when you want full control: your own HTML, your own JavaScript framework, a map library, a 3D viewer, custom video playback. [Interactive Tools](/apps/prefab) wrap the MCP Apps extension so you never have to think about it — this page is what you reach for when you need to think about it.
|
||||
|
||||
You'll be working with two things: the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK for host communication, and FastMCP's `AppConfig` for resources and CSP.
|
||||
|
||||
## How it works
|
||||
|
||||
An MCP App has two parts:
|
||||
|
||||
1. A **tool** that does the work and returns data
|
||||
2. A **`ui://` resource** containing the HTML that renders that data
|
||||
|
||||
The tool declares which resource to use via `AppConfig`. When the host calls the tool, it also fetches the linked resource, renders it in a sandboxed iframe, and pushes the tool result into the app via `postMessage`. The app can also call tools back, enabling interactive workflows.
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps import AppConfig, ResourceCSP
|
||||
|
||||
mcp = FastMCP("My App Server")
|
||||
|
||||
# The tool does the work
|
||||
@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
|
||||
def generate_chart(data: list[float]) -> str:
|
||||
return json.dumps({"values": data})
|
||||
|
||||
# The resource provides the UI
|
||||
@mcp.resource("ui://my-app/view.html")
|
||||
def chart_view() -> str:
|
||||
return "<html>...</html>"
|
||||
```
|
||||
|
||||
## AppConfig
|
||||
|
||||
`AppConfig` controls how a tool or resource participates in the Apps extension. Import it from `fastmcp.server.apps`:
|
||||
|
||||
```python
|
||||
from fastmcp.apps import AppConfig
|
||||
```
|
||||
|
||||
On **tools**, you'll typically set `resource_uri` to point to the UI resource:
|
||||
|
||||
```python
|
||||
@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
|
||||
def my_tool() -> str:
|
||||
return "result"
|
||||
```
|
||||
|
||||
You can also pass a raw dict with camelCase keys, matching the wire format:
|
||||
|
||||
```python
|
||||
@mcp.tool(app={"resourceUri": "ui://my-app/view.html"})
|
||||
def my_tool() -> str:
|
||||
return "result"
|
||||
```
|
||||
|
||||
### Tool visibility
|
||||
|
||||
The `visibility` field controls where a tool appears:
|
||||
|
||||
- `["model"]` — visible to the LLM (the default behavior)
|
||||
- `["app"]` — only callable from within the app UI, hidden from the LLM
|
||||
- `["model", "app"]` — both
|
||||
|
||||
This is useful when you have tools that only make sense as part of the app's interactive flow, not as standalone LLM actions.
|
||||
|
||||
```python
|
||||
@mcp.tool(
|
||||
app=AppConfig(
|
||||
resource_uri="ui://my-app/view.html",
|
||||
visibility=["app"],
|
||||
)
|
||||
)
|
||||
def refresh_data() -> str:
|
||||
"""Only callable from the app UI, not by the LLM."""
|
||||
return fetch_latest()
|
||||
```
|
||||
|
||||
### AppConfig fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `resource_uri` | `str` | URI of the UI resource. Tools only. |
|
||||
| `visibility` | `list[str]` | Where the tool appears: `"model"`, `"app"`, or both. Tools only. |
|
||||
| `csp` | `ResourceCSP` | Content Security Policy for the iframe. |
|
||||
| `permissions` | `ResourcePermissions` | Iframe sandbox permissions. |
|
||||
| `domain` | `str` | Stable sandbox origin for the iframe. |
|
||||
| `prefers_border` | `bool` | Whether the UI prefers a visible border. |
|
||||
|
||||
<Note>
|
||||
On **resources**, `resource_uri` and `visibility` must not be set — the resource *is* the UI. Use `AppConfig` on resources only for `csp`, `permissions`, and other display settings.
|
||||
</Note>
|
||||
|
||||
## UI resources
|
||||
|
||||
Resources using the `ui://` scheme are automatically served with the MIME type `text/html;profile=mcp-app`. No need to set it manually.
|
||||
|
||||
```python
|
||||
@mcp.resource("ui://my-app/view.html")
|
||||
def my_view() -> str:
|
||||
return "<html>...</html>"
|
||||
```
|
||||
|
||||
The HTML can be anything — a full single-page app, a simple display, or a complex interactive tool. The host renders it in a sandboxed iframe and establishes a `postMessage` channel for communication.
|
||||
|
||||
### Writing the app HTML
|
||||
|
||||
Your HTML app communicates with the host using the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK. The simplest approach is to load it from a CDN:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { App } from "https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.0/app-with-deps";
|
||||
|
||||
const app = new App({ name: "My App", version: "1.0.0" });
|
||||
|
||||
// Receive tool results pushed by the host
|
||||
app.ontoolresult = ({ content }) => {
|
||||
const text = content?.find(c => c.type === 'text');
|
||||
if (text) {
|
||||
document.getElementById('output').textContent = text.text;
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the host
|
||||
await app.connect();
|
||||
</script>
|
||||
```
|
||||
|
||||
The `App` object provides:
|
||||
|
||||
- **`app.ontoolresult`** — callback that receives tool results pushed by the host
|
||||
- **`app.callServerTool({name, arguments})`** — call a tool on the server from within the app
|
||||
- **`app.onhostcontextchanged`** — callback for host context changes (e.g., safe area insets)
|
||||
- **`app.getHostContext()`** — get current host context
|
||||
|
||||
See the full [ext-apps SDK documentation](https://github.com/modelcontextprotocol/ext-apps) for the complete API reference.
|
||||
|
||||
<Note>
|
||||
If your HTML loads external scripts, styles, or makes API calls, you need to declare those domains in the CSP configuration. See [Security](#security) below.
|
||||
</Note>
|
||||
|
||||
## Security
|
||||
|
||||
Apps run in sandboxed iframes with a deny-by-default Content Security Policy. By default, only inline scripts and styles are allowed — no external network access.
|
||||
|
||||
### Content Security Policy
|
||||
|
||||
If your app needs to load external resources (CDN scripts, API calls, embedded iframes), declare the allowed domains with `ResourceCSP`:
|
||||
|
||||
```python
|
||||
from fastmcp.apps import AppConfig, ResourceCSP
|
||||
|
||||
@mcp.resource(
|
||||
"ui://my-app/view.html",
|
||||
app=AppConfig(
|
||||
csp=ResourceCSP(
|
||||
resource_domains=["https://unpkg.com", "https://cdn.example.com"],
|
||||
connect_domains=["https://api.example.com"],
|
||||
)
|
||||
),
|
||||
)
|
||||
def my_view() -> str:
|
||||
return "<html>...</html>"
|
||||
```
|
||||
|
||||
| CSP Field | Controls |
|
||||
|-----------|----------|
|
||||
| `connect_domains` | `fetch`, XHR, WebSocket (`connect-src`) |
|
||||
| `resource_domains` | Scripts, images, styles, fonts (`script-src`, etc.) |
|
||||
| `frame_domains` | Nested iframes (`frame-src`) |
|
||||
| `base_uri_domains` | Document base URI (`base-uri`) |
|
||||
|
||||
### Permissions
|
||||
|
||||
If your app needs browser capabilities like camera or clipboard access, request them via `ResourcePermissions`:
|
||||
|
||||
```python
|
||||
from fastmcp.apps import AppConfig, ResourcePermissions
|
||||
|
||||
@mcp.resource(
|
||||
"ui://my-app/view.html",
|
||||
app=AppConfig(
|
||||
permissions=ResourcePermissions(
|
||||
camera={},
|
||||
clipboard_write={},
|
||||
)
|
||||
),
|
||||
)
|
||||
def my_view() -> str:
|
||||
return "<html>...</html>"
|
||||
```
|
||||
|
||||
Hosts may or may not grant these permissions. Your app should use JavaScript feature detection as a fallback.
|
||||
|
||||
## Example: a QR code server
|
||||
|
||||
This example creates a tool that generates QR codes and an app that renders them as images. It's based on the [official MCP Apps example](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server). Requires the `qrcode[pil]` package.
|
||||
|
||||
```python expandable
|
||||
import base64
|
||||
import io
|
||||
|
||||
import qrcode
|
||||
from mcp import types
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps import AppConfig, ResourceCSP
|
||||
from fastmcp.tools import ToolResult
|
||||
|
||||
mcp = FastMCP("QR Code Server")
|
||||
|
||||
VIEW_URI = "ui://qr-server/view.html"
|
||||
|
||||
|
||||
@mcp.tool(app=AppConfig(resource_uri=VIEW_URI))
|
||||
def generate_qr(text: str = "https://gofastmcp.com") -> ToolResult:
|
||||
"""Generate a QR code from text."""
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=4)
|
||||
qr.add_data(text)
|
||||
qr.make(fit=True)
|
||||
|
||||
img = qr.make_image()
|
||||
buffer = io.BytesIO()
|
||||
img.save(buffer, format="PNG")
|
||||
b64 = base64.b64encode(buffer.getvalue()).decode()
|
||||
|
||||
return ToolResult(
|
||||
content=[types.ImageContent(type="image", data=b64, mimeType="image/png")]
|
||||
)
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
VIEW_URI,
|
||||
app=AppConfig(csp=ResourceCSP(resource_domains=["https://unpkg.com"])),
|
||||
)
|
||||
def view() -> str:
|
||||
"""Interactive QR code viewer."""
|
||||
return """\
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<style>
|
||||
body { display: flex; justify-content: center;
|
||||
align-items: center; height: 340px; width: 340px;
|
||||
margin: 0; background: transparent; }
|
||||
img { width: 300px; height: 300px; border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="qr"></div>
|
||||
<script type="module">
|
||||
import { App } from
|
||||
"https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.0/app-with-deps";
|
||||
|
||||
const app = new App({ name: "QR View", version: "1.0.0" });
|
||||
|
||||
app.ontoolresult = ({ content }) => {
|
||||
const img = content?.find(c => c.type === 'image');
|
||||
if (img) {
|
||||
const el = document.createElement('img');
|
||||
el.src = `data:${img.mimeType};base64,${img.data}`;
|
||||
el.alt = "QR Code";
|
||||
document.getElementById('qr').replaceChildren(el);
|
||||
}
|
||||
};
|
||||
|
||||
await app.connect();
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
```
|
||||
|
||||
The tool generates a QR code as a base64 PNG. The resource loads the MCP Apps JS SDK from unpkg (declared in the CSP), listens for tool results, and renders the image. The host wires them together — when the LLM calls `generate_qr`, the QR code appears in an interactive frame inside the conversation.
|
||||
|
||||
## Checking client support
|
||||
|
||||
Not all hosts support the Apps extension. You can check at runtime using the tool's [context](/servers/context):
|
||||
|
||||
```python
|
||||
from fastmcp import Context
|
||||
from fastmcp.apps import AppConfig, UI_EXTENSION_ID
|
||||
|
||||
@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
|
||||
async def my_tool(ctx: Context) -> str:
|
||||
if ctx.client_supports_extension(UI_EXTENSION_ID):
|
||||
# Return data optimized for UI rendering
|
||||
return rich_response()
|
||||
else:
|
||||
# Fall back to plain text
|
||||
return plain_text_response()
|
||||
```
|
||||
73
docs/v3/apps/overview.mdx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
title: Apps
|
||||
sidebarTitle: Overview
|
||||
description: Give your tools interactive UIs rendered directly in the conversation.
|
||||
icon: grid-2
|
||||
mode: center
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx'
|
||||
import { PrefabDemoFrame } from '/snippets/prefab-demo-frame.mdx'
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
A FastMCP app is a tool that returns an interactive UI instead of text. When the host calls it, the user sees a chart, a table, a form, or a whole dashboard rendered right inside the conversation, with working sort, search, tooltips, and state.
|
||||
|
||||
<div style={{
|
||||
margin: '0 clamp(-180px, calc(-18vw + 90px), 0px) 2rem',
|
||||
maxHeight: '700px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
maskImage: 'linear-gradient(to bottom, black 75%, transparent)',
|
||||
WebkitMaskImage: 'linear-gradient(to bottom, black 75%, transparent)',
|
||||
}}>
|
||||
<PrefabDemoFrame demo="hitchhikers" height="2000px" title="Prefab showcase demo" />
|
||||
</div>
|
||||
|
||||
The dashboard above is a [Prefab](https://prefab.prefect.io) showcase — a taste of what you can deliver from a FastMCP tool. Every card, chart, slider, dialog, and carousel is a Python component. Build a composition like this, add `@mcp.tool(app=True)`, and the host renders it inside the conversation.
|
||||
|
||||
Under the hood, FastMCP builds on the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) and uses Prefab to describe UIs in Python.
|
||||
|
||||
```bash
|
||||
pip install "fastmcp[apps]"
|
||||
```
|
||||
|
||||
<PrefabPinWarning />
|
||||
|
||||
## Pick your path
|
||||
|
||||
Four patterns cover almost everything you'd want to build. Most apps start with Interactive Tools; you only reach for the others when you've hit a specific limit.
|
||||
|
||||
### [Interactive Tools](/apps/prefab) — start here
|
||||
|
||||
Add `app=True` to a tool and return a Prefab component. Charts, tables, dashboards, and client-side interactivity (toggles, tabs, filtering) all work without any server round-trips.
|
||||
|
||||
```python
|
||||
@mcp.tool(app=True)
|
||||
def team_directory() -> DataTable:
|
||||
return DataTable(columns=[...], rows=employees, search=True)
|
||||
```
|
||||
|
||||
### [FastMCPApp](/apps/fastmcp-app) — when the UI calls back to the server
|
||||
|
||||
Forms that save data, buttons that trigger backend work, search that hits a database. `FastMCPApp` manages the wiring between UI actions and backend tools, with stable tool identifiers that survive server composition.
|
||||
|
||||
### [Generative UI](/apps/generative) — when the LLM writes the UI
|
||||
|
||||
Register one provider and the model can write Prefab code tailored to the current data and request. The user watches the UI build up as the model generates it.
|
||||
|
||||
```python
|
||||
mcp.add_provider(GenerativeUI())
|
||||
```
|
||||
|
||||
### [Custom HTML](/apps/low-level) — when you need full control
|
||||
|
||||
Write your own HTML, CSS, and JavaScript. Use a specific framework, drop in a map or 3D viewer, embed video. You're talking to the MCP Apps protocol directly.
|
||||
|
||||
## What's next
|
||||
|
||||
- **[Quickstart](/apps/quickstart)** — build a working app in a minute
|
||||
- **[Examples](/apps/examples)** — complete working servers you can run today
|
||||
- **[Providers](/apps/providers/approval)** — ready-made capabilities (approvals, choice pickers, file upload, forms) you add with one line
|
||||
- **[Development](/apps/development)** — preview app tools locally with `fastmcp dev apps`
|
||||
297
docs/v3/apps/prefab.mdx
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
---
|
||||
title: Interactive Tools
|
||||
sidebarTitle: Interactive Tools
|
||||
description: Turn your tools into interactive UIs with charts, tables, and dashboards.
|
||||
icon: palette
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx'
|
||||
import { PrefabDemoFrame } from '/snippets/prefab-demo-frame.mdx'
|
||||
|
||||
<VersionBadge version="3.1.0" />
|
||||
|
||||
<PrefabPinWarning />
|
||||
|
||||
<PrefabDemoFrame demo="dashboard" height="680px" title="Sales dashboard demo" />
|
||||
|
||||
Believe it or not, that dashboard is a FastMCP tool. The chart has tooltips. The table is sortable. The badges are styled by deal stage. The whole thing is about 40 lines of Python, and the user sees it right inside their conversation instead of a wall of JSON.
|
||||
|
||||
The pattern behind every example on this page is the same: add `app=True` to your tool, build a UI with [Prefab](https://prefab.prefect.io) components, and return it as a `PrefabApp`. Prefab has [100+ components](https://prefab.prefect.io/docs/components), from data tables and charts to forms and progress bars. You compose them in Python; the host renders them as a live, interactive application.
|
||||
|
||||
## Start with a table
|
||||
|
||||
Most tools return data the user wants to explore. A `DataTable` is often the smallest useful upgrade — your data goes from a JSON blob to a searchable, sortable table:
|
||||
|
||||
<PrefabDemoFrame demo="data-table" height="530px" title="Data table demo" />
|
||||
|
||||
```python
|
||||
from prefab_ui.components import DataTable, DataTableColumn
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Directory")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def team_directory() -> DataTable:
|
||||
"""Browse the team directory."""
|
||||
employees = [
|
||||
{"name": "Alice Chen", "role": "Staff Engineer", "dept": "Platform"},
|
||||
{"name": "Bob Martinez", "role": "Lead Designer", "dept": "Design"},
|
||||
{"name": "Carol Johnson", "role": "Senior Engineer", "dept": "Platform"},
|
||||
{"name": "David Kim", "role": "Product Manager", "dept": "Product"},
|
||||
{"name": "Eva Mueller", "role": "Engineer", "dept": "Platform"},
|
||||
{"name": "Frank Lee", "role": "Data Scientist", "dept": "ML"},
|
||||
{"name": "Grace Park", "role": "Eng Manager", "dept": "Platform"},
|
||||
]
|
||||
|
||||
return DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="role", header="Role", sortable=True),
|
||||
DataTableColumn(key="dept", header="Dept", sortable=True),
|
||||
],
|
||||
rows=employees,
|
||||
search=True,
|
||||
)
|
||||
```
|
||||
|
||||
That's it. Add `app=True`, return a Prefab component instead of raw dicts. FastMCP handles the rendering, sandboxing, and security. No wrapper class needed for simple cases like this.
|
||||
|
||||
## Add charts
|
||||
|
||||
When numbers tell a better story as a visual, swap in a chart. The API is the same: pass your data as a list of dicts, tell the chart which keys to plot.
|
||||
|
||||
<PrefabDemoFrame demo="bar-chart" height="430px" title="Bar chart demo" />
|
||||
|
||||
```python
|
||||
@mcp.tool(app=True)
|
||||
def quarterly_revenue(year: int) -> BarChart:
|
||||
"""Show quarterly revenue as a bar chart."""
|
||||
data = [
|
||||
{"quarter": "Q1", "revenue": 42000, "costs": 28000},
|
||||
{"quarter": "Q2", "revenue": 51000, "costs": 31000},
|
||||
{"quarter": "Q3", "revenue": 47000, "costs": 29000},
|
||||
{"quarter": "Q4", "revenue": 63000, "costs": 35000},
|
||||
]
|
||||
|
||||
return BarChart(
|
||||
data=data,
|
||||
series=[
|
||||
ChartSeries(data_key="revenue", label="Revenue"),
|
||||
ChartSeries(data_key="costs", label="Costs"),
|
||||
],
|
||||
x_axis="quarter",
|
||||
show_legend=True,
|
||||
)
|
||||
```
|
||||
|
||||
Each `ChartSeries` plots a different key from the data. `BarChart`, `LineChart`, `AreaChart`, `PieChart`, `RadarChart`, and `RadialChart` all follow the same pattern. Hover over the bars to see tooltips.
|
||||
|
||||
<PrefabDemoFrame demo="pie-chart" height="410px" title="Pie chart demo" />
|
||||
|
||||
```python
|
||||
@mcp.tool(app=True)
|
||||
def ticket_breakdown() -> PieChart:
|
||||
"""Show open tickets by category."""
|
||||
data = [
|
||||
{"category": "Bug", "count": 42},
|
||||
{"category": "Feature", "count": 28},
|
||||
{"category": "Docs", "count": 15},
|
||||
{"category": "Infra", "count": 10},
|
||||
]
|
||||
|
||||
return PieChart(
|
||||
data=data,
|
||||
data_key="count",
|
||||
name_key="category",
|
||||
inner_radius=50,
|
||||
show_legend=True,
|
||||
)
|
||||
```
|
||||
|
||||
See the [Prefab chart docs](https://prefab.prefect.io/docs/components) for stacking, curves, custom colors, and more.
|
||||
|
||||
## Compose a dashboard
|
||||
|
||||
Tables and charts are useful on their own, but the real power comes from composing them. `Column` stacks children vertically, `Row` lays them out side by side, and `with` blocks establish nesting — the indentation is the layout.
|
||||
|
||||
<PrefabDemoFrame demo="dashboard" height="680px" title="Sales dashboard demo" />
|
||||
|
||||
```python expandable
|
||||
@mcp.tool(app=True)
|
||||
def sales_dashboard() -> PrefabApp:
|
||||
"""Show sales KPIs, trends, and deals."""
|
||||
monthly = [
|
||||
{"month": "Jan", "revenue": 48200, "costs": 31000},
|
||||
{"month": "Feb", "revenue": 52100, "costs": 32500},
|
||||
{"month": "Mar", "revenue": 61800, "costs": 34200},
|
||||
{"month": "Apr", "revenue": 58400, "costs": 33800},
|
||||
]
|
||||
deals = [
|
||||
{"account": "Acme Corp", "value": "$84,000", "stage": "Won"},
|
||||
{"account": "Globex Inc", "value": "$52,000", "stage": "Negotiation"},
|
||||
{"account": "Initech", "value": "$31,500", "stage": "Proposal"},
|
||||
{"account": "Wayne Enterprises", "value": "$45,000", "stage": "Lost"},
|
||||
]
|
||||
|
||||
rows = [
|
||||
{
|
||||
"account": d["account"],
|
||||
"value": d["value"],
|
||||
"stage": Badge(
|
||||
d["stage"],
|
||||
variant="success" if d["stage"] == "Won"
|
||||
else "destructive" if d["stage"] == "Lost"
|
||||
else "secondary",
|
||||
),
|
||||
}
|
||||
for d in deals
|
||||
]
|
||||
|
||||
total = sum(m["revenue"] for m in monthly)
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
with Row(gap=6):
|
||||
Metric(label="Revenue (Q1-Q4)", value=f"${total:,}")
|
||||
Metric(label="Deals", value=f"{len(deals)}")
|
||||
BarChart(
|
||||
data=monthly,
|
||||
series=[
|
||||
ChartSeries(data_key="revenue", label="Revenue"),
|
||||
ChartSeries(data_key="costs", label="Costs"),
|
||||
],
|
||||
x_axis="month",
|
||||
show_legend=True,
|
||||
)
|
||||
Separator()
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="account", header="Account", sortable=True),
|
||||
DataTableColumn(key="value", header="Value", sortable=True),
|
||||
DataTableColumn(key="stage", header="Stage"),
|
||||
],
|
||||
rows=rows,
|
||||
)
|
||||
|
||||
return app
|
||||
```
|
||||
|
||||
Notice how `Badge` components can be placed inside table cells — any Prefab component works as a cell value, so you can put progress bars, icons, or buttons in your tables too.
|
||||
|
||||
## Make it reactive
|
||||
|
||||
Everything above renders once from the data your Python provides. But interactive tools can also respond to user input in real time, without any server round-trips. Prefab's state system lets components read and write client-side values, so the UI updates instantly as the user interacts with it.
|
||||
|
||||
<PrefabDemoFrame demo="reactive" height="500px" title="Reactive sales demo" />
|
||||
|
||||
Try switching regions in the dropdown, and toggling the switch on and off.
|
||||
|
||||
```python expandable
|
||||
from prefab_ui.rx import Rx
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def regional_sales() -> PrefabApp:
|
||||
"""Sales by region with a live filter."""
|
||||
north = [
|
||||
{"month": "Jan", "sales": 22000},
|
||||
{"month": "Feb", "sales": 25500},
|
||||
{"month": "Mar", "sales": 24200},
|
||||
]
|
||||
south = [
|
||||
{"month": "Jan", "sales": 5800},
|
||||
{"month": "Feb", "sales": 6400},
|
||||
{"month": "Mar", "sales": 5600},
|
||||
]
|
||||
west = [
|
||||
{"month": "Jan", "sales": 6000},
|
||||
{"month": "Feb", "sales": 6000},
|
||||
{"month": "Mar", "sales": 5600},
|
||||
]
|
||||
|
||||
with PrefabApp(
|
||||
state={
|
||||
"region": "north",
|
||||
"north": north, "south": south, "west": west,
|
||||
"show_target": True,
|
||||
},
|
||||
) as app:
|
||||
with Column(
|
||||
gap=4,
|
||||
css_class="p-6",
|
||||
let={"data": "{{ region == 'south' ? south"
|
||||
" : region == 'west' ? west"
|
||||
" : north }}"},
|
||||
):
|
||||
with Row(gap=4, align="center"):
|
||||
with Select(name="region", css_class="w-40"):
|
||||
SelectOption(value="north", label="North")
|
||||
SelectOption(value="south", label="South")
|
||||
SelectOption(value="west", label="West")
|
||||
Switch(name="show_target", css_class="ml-auto")
|
||||
Text("Show target", css_class="text-sm text-muted-foreground")
|
||||
BarChart(
|
||||
data=Rx("data"),
|
||||
series=[ChartSeries(data_key="sales", label="Sales")],
|
||||
x_axis="month",
|
||||
)
|
||||
with If(Rx("show_target")):
|
||||
Metric(label="Q1 Target", value="$75,000")
|
||||
|
||||
return app
|
||||
```
|
||||
|
||||
The `state` dict on `PrefabApp` declares initial values. The `Select` writes to the `region` key on every change. A `let` binding picks the matching dataset, and the chart re-renders. The `Switch` toggles a `Metric` on and off through `If(Rx("show_target"))`. All of this happens in the browser — no calls back to your server.
|
||||
|
||||
`Rx` is a reactive reference: `Rx("region")` compiles to an expression the renderer evaluates live. It supports arithmetic, comparisons, formatting pipes (`.currency()`, `.percent()`), and ternary conditionals (`.then()`). For the full state system, see the [Prefab state docs](https://prefab.prefect.io/docs/concepts/state) and [expression docs](https://prefab.prefect.io/docs/concepts/expressions).
|
||||
|
||||
## Content Security Policy
|
||||
|
||||
Interactive tools render in a sandboxed iframe with a strict CSP. If your tool loads external resources — embedding iframes, fetching from APIs, loading scripts — add the required domains:
|
||||
|
||||
```python
|
||||
from fastmcp.apps import PrefabAppConfig, ResourceCSP
|
||||
|
||||
@mcp.tool(app=PrefabAppConfig(
|
||||
csp=ResourceCSP(frame_domains=["https://example.com"]),
|
||||
))
|
||||
def dashboard_with_embed() -> PrefabApp:
|
||||
...
|
||||
```
|
||||
|
||||
`PrefabAppConfig()` with no arguments is equivalent to `app=True`.
|
||||
|
||||
## Giving the LLM context
|
||||
|
||||
By default, the LLM sees `"[Rendered Prefab UI]"` as the tool result. If the model needs to reason about the data, return a `ToolResult` with a text summary alongside the UI:
|
||||
|
||||
```python
|
||||
from fastmcp.tools import ToolResult
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def sales_overview(year: int) -> ToolResult:
|
||||
"""Show sales visually, summarize for the model."""
|
||||
data = get_sales_data(year)
|
||||
total = sum(row["revenue"] for row in data)
|
||||
|
||||
with Column(gap=4, css_class="p-6") as view:
|
||||
BarChart(data=data, series=[ChartSeries(data_key="revenue")])
|
||||
|
||||
return ToolResult(
|
||||
content=f"Total revenue for {year}: ${total:,} across {len(data)} quarters",
|
||||
structured_content=view,
|
||||
)
|
||||
```
|
||||
|
||||
The user sees the chart. The model sees the summary.
|
||||
|
||||
## Next steps
|
||||
|
||||
- **[FastMCPApp](/apps/fastmcp-app)** — when your UI needs to call backend tools (forms, search, CRUD)
|
||||
- **[Generative UI](/apps/generative)** — let the LLM design the UI at runtime
|
||||
- **[Custom HTML](/apps/low-level)** — when Prefab isn't enough (maps, 3D, your own framework)
|
||||
- **[Examples](/apps/examples)** — complete working servers you can run today
|
||||
- **[Development](/apps/development)** — preview your tools locally with `fastmcp dev apps`
|
||||
- **[Prefab UI](https://prefab.prefect.io)** — full component reference with 100+ components, theming, and advanced patterns
|
||||
80
docs/v3/apps/providers/approval.mdx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
title: Approval
|
||||
sidebarTitle: Approval
|
||||
description: Human-in-the-loop approval gates for agent actions
|
||||
icon: shield-check
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
`Approval` adds a human-in-the-loop confirmation step to any server. The LLM presents what it's about to do, the user approves or rejects via buttons, and the decision flows back into the conversation as a message.
|
||||
|
||||
<Frame>
|
||||
<img src="/apps/images/app-approval.png" alt="The Approval provider shown in Goose, with a payment confirmation card and Approve/Cancel buttons" />
|
||||
</Frame>
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.approval import Approval
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
mcp.add_provider(Approval())
|
||||
```
|
||||
|
||||
This registers a single tool:
|
||||
|
||||
| Tool | Visibility | Purpose |
|
||||
|------|-----------|---------|
|
||||
| `request_approval` | Model | Shows an approval card, sends the user's decision back as a message |
|
||||
|
||||
The LLM calls `request_approval` with a summary (and optional details) whenever it's about to take a significant action. The user sees a card with Approve and Reject buttons. Clicking either sends a message back into the conversation via `SendMessage`, which triggers the LLM's next turn.
|
||||
|
||||
The message looks like it came from the user:
|
||||
|
||||
```
|
||||
"Deploy v3.2 to production" — I selected: Approve
|
||||
```
|
||||
|
||||
<Note>
|
||||
Approval is an advisory gate, not an enforcement mechanism. The conversation isn't blocked while the card is open — the user can keep typing, and a determined LLM could proceed without waiting. Think of it as a strong UX signal that encourages confirmation, not a security boundary. For hard enforcement, implement approval logic server-side in your tool implementations.
|
||||
</Note>
|
||||
|
||||
## Configuration
|
||||
|
||||
The constructor sets defaults; the LLM can override all of these per-call via tool arguments.
|
||||
|
||||
```python
|
||||
Approval(
|
||||
name="Approval", # App name
|
||||
title="Approval Required", # Card heading
|
||||
approve_text="Approve", # Approve button label
|
||||
reject_text="Reject", # Reject button label
|
||||
approve_variant="default", # "default", "destructive", "success", "info"
|
||||
reject_variant="outline", # same options plus "outline"
|
||||
)
|
||||
```
|
||||
|
||||
The LLM can customize each invocation:
|
||||
|
||||
```python
|
||||
request_approval(
|
||||
summary="Delete 47 files from /tmp",
|
||||
details="This cannot be undone.",
|
||||
title="Destructive Action",
|
||||
approve_text="Delete",
|
||||
approve_variant="destructive",
|
||||
reject_text="Keep files",
|
||||
)
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
When the user clicks a button, two things happen:
|
||||
|
||||
1. `SendMessage` pushes the decision into the conversation as a user message
|
||||
2. `SetState("decided", True)` replaces the buttons with "Response sent."
|
||||
|
||||
The tool description instructs the LLM to stop and wait for the "I selected:" message before proceeding. If approved, it continues. If rejected, it acknowledges and asks how to proceed.
|
||||
72
docs/v3/apps/providers/choice.mdx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
title: Choice
|
||||
sidebarTitle: Choice
|
||||
description: Present clickable options instead of free-text responses
|
||||
icon: list-check
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
`Choice` lets the LLM present a set of options as clickable buttons instead of asking the user to type a response. The selection flows back into the conversation as a message, giving the LLM clean structured input.
|
||||
|
||||
<Frame>
|
||||
<img src="/apps/images/app-choice.png" alt="The Choice provider shown in Goose, with four lunch options as clickable buttons" />
|
||||
</Frame>
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.choice import Choice
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
mcp.add_provider(Choice())
|
||||
```
|
||||
|
||||
This registers a single tool:
|
||||
|
||||
| Tool | Visibility | Purpose |
|
||||
|------|-----------|---------|
|
||||
| `choose` | Model | Shows a card with clickable options, sends the selection back as a message |
|
||||
|
||||
The LLM calls `choose` with a prompt and a list of options. The user sees a card with one button per option. Clicking one sends a message back into the conversation:
|
||||
|
||||
```
|
||||
"Which deployment strategy?" — I selected: Blue-green
|
||||
```
|
||||
|
||||
<Note>
|
||||
This is an advisory interaction, not an enforcement mechanism. The conversation isn't blocked while the card is open — the user can keep typing, and the LLM could proceed without waiting. The tool description instructs the LLM to stop and wait for the "I selected:" response, but for hard enforcement, implement selection logic server-side.
|
||||
</Note>
|
||||
|
||||
## Configuration
|
||||
|
||||
The constructor sets defaults; the LLM can override `title` per-call.
|
||||
|
||||
```python
|
||||
Choice(
|
||||
name="Choice", # App name
|
||||
title="Choose an Option", # Default card heading
|
||||
variant="outline", # Button style for all options
|
||||
)
|
||||
```
|
||||
|
||||
The LLM provides the options per-call:
|
||||
|
||||
```python
|
||||
choose(
|
||||
prompt="What should we have for lunch?",
|
||||
options=["Pizza", "Tacos", "Ramen", "Salad"],
|
||||
title="The Important Questions",
|
||||
)
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
Each option renders as a full-width button in a vertical stack. When the user clicks one:
|
||||
|
||||
1. `SendMessage` pushes the selection into the conversation as a user message
|
||||
2. `SetState("decided", True)` replaces the buttons with "Response sent."
|
||||
|
||||
The tool description instructs the LLM to stop and wait for the "I selected:" message before proceeding with whatever the user chose.
|
||||
129
docs/v3/apps/providers/file-upload.mdx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
---
|
||||
title: File Upload
|
||||
sidebarTitle: File Upload
|
||||
description: Drag-and-drop file upload for any MCP server
|
||||
icon: upload
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
`FileUpload` adds drag-and-drop file upload to any server. Users upload files through an interactive UI, bypassing the LLM context window entirely. The LLM can then list and read uploaded files through model-visible tools.
|
||||
|
||||
<Frame>
|
||||
<img src="/apps/images/app-file-upload.png" alt="The FileUpload provider shown in Goose, with a drag-and-drop zone for uploading files" />
|
||||
</Frame>
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.file_upload import FileUpload
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
mcp.add_provider(FileUpload())
|
||||
```
|
||||
|
||||
This registers four tools:
|
||||
|
||||
| Tool | Visibility | Purpose |
|
||||
|------|-----------|---------|
|
||||
| `file_manager` | Model | Opens the drag-and-drop upload UI |
|
||||
| `store_files` | App only | Called by the UI when the user clicks Upload |
|
||||
| `list_files` | Model | Returns metadata for all uploaded files |
|
||||
| `read_file` | Model | Returns a file's contents by name |
|
||||
|
||||
The LLM sees `file_manager`, `list_files`, and `read_file`. It calls `file_manager` to show the upload interface, then uses `list_files` and `read_file` to work with whatever the user uploaded. `store_files` is app-only — the UI calls it directly and the LLM never needs to know about it.
|
||||
|
||||
## Configuration
|
||||
|
||||
```python
|
||||
FileUpload(
|
||||
name="Files", # App name (used in tool routing)
|
||||
max_file_size=10 * 1024 * 1024, # 10 MB default, enforced server-side
|
||||
title="File Upload", # Heading shown in the UI
|
||||
description="Drop files to...", # Description text below the heading
|
||||
drop_label="Drop files here", # Label inside the drop zone
|
||||
)
|
||||
```
|
||||
|
||||
The `max_file_size` limit is enforced both in the UI (the DropZone rejects oversized files) and on the server (the `store_files` tool validates before calling `on_store`).
|
||||
|
||||
## Storage scoping
|
||||
|
||||
By default, files are stored in memory and scoped by MCP session ID. Each session gets its own isolated file store — files uploaded in one conversation aren't visible in another.
|
||||
|
||||
This works with **stdio**, **SSE**, and **stateful HTTP** transports, where sessions persist across requests.
|
||||
|
||||
<Warning>
|
||||
In **stateless HTTP** mode, each request creates a new session object with a new ID. Files stored during one request (e.g. the UI upload) will be invisible to the next request (e.g. the LLM calling `list_files`). You **must** override `_get_scope_key` to use a stable identifier like a user ID from your auth token.
|
||||
</Warning>
|
||||
|
||||
For stateless deployments, override `_get_scope_key` to return a stable identifier. For example, to scope files by authenticated user:
|
||||
|
||||
```python
|
||||
from fastmcp.apps.file_upload import FileUpload
|
||||
|
||||
class UserScopedUpload(FileUpload):
|
||||
def _get_scope_key(self, ctx):
|
||||
return ctx.access_token["sub"]
|
||||
```
|
||||
|
||||
For process-wide shared storage (all users see all files):
|
||||
|
||||
```python
|
||||
class SharedUpload(FileUpload):
|
||||
def _get_scope_key(self, ctx):
|
||||
return "__shared__"
|
||||
```
|
||||
|
||||
## Custom storage
|
||||
|
||||
The default implementation stores files in memory for the lifetime of the server process. For persistent storage, subclass `FileUpload` and override three methods. Each receives the current `Context`, giving you access to session IDs, auth tokens, and request metadata for partitioning and authorization.
|
||||
|
||||
```python
|
||||
import base64
|
||||
|
||||
from fastmcp.apps.file_upload import FileUpload
|
||||
|
||||
class S3Upload(FileUpload):
|
||||
def on_store(self, files, ctx):
|
||||
user_id = ctx.access_token["sub"]
|
||||
for f in files:
|
||||
s3.put_object(
|
||||
Bucket="uploads",
|
||||
Key=f"{user_id}/{f['name']}",
|
||||
Body=base64.b64decode(f["data"]),
|
||||
)
|
||||
return self.on_list(ctx)
|
||||
|
||||
def on_list(self, ctx):
|
||||
user_id = ctx.access_token["sub"]
|
||||
objects = s3.list_objects(Bucket="uploads", Prefix=f"{user_id}/")
|
||||
return [
|
||||
{
|
||||
"name": obj["Key"].split("/", 1)[1],
|
||||
"type": "application/octet-stream",
|
||||
"size": obj["Size"],
|
||||
"size_display": f"{obj['Size']} B",
|
||||
"uploaded_at": obj["LastModified"].isoformat(),
|
||||
}
|
||||
for obj in objects.get("Contents", [])
|
||||
]
|
||||
|
||||
def on_read(self, name, ctx):
|
||||
user_id = ctx.access_token["sub"]
|
||||
obj = s3.get_object(Bucket="uploads", Key=f"{user_id}/{name}")
|
||||
content = obj["Body"].read()
|
||||
return {
|
||||
"name": name,
|
||||
"size": obj["ContentLength"],
|
||||
"type": obj["ContentType"],
|
||||
"uploaded_at": obj["LastModified"].isoformat(),
|
||||
"content": content.decode("utf-8"),
|
||||
}
|
||||
```
|
||||
|
||||
Each file dict passed to `on_store` contains `name`, `size`, `type`, and `data` (base64-encoded content). The return value from `on_store` and `on_list` should be a list of summary dicts with `name`, `type`, `size`, `size_display`, and `uploaded_at` fields — these populate the file list in the UI.
|
||||
|
||||
`on_read` returns a dict with file metadata and either `content` (decoded text) or `content_base64` (a base64 preview for binary files).
|
||||
105
docs/v3/apps/providers/form.mdx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
title: Form Input
|
||||
sidebarTitle: Form Input
|
||||
description: Collect structured data from users via Pydantic models
|
||||
icon: rectangle-list
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
`FormInput` generates a validated form from a Pydantic model. The user fills it out, and the submission is validated against the model before being returned. Structured elicitation that can't be hallucinated.
|
||||
|
||||
<Frame>
|
||||
<img src="/apps/images/app-form.png" alt="The FormInput provider shown in Goose, with a bug report form" />
|
||||
</Frame>
|
||||
|
||||
```python
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.form import FormInput
|
||||
|
||||
class BugReport(BaseModel):
|
||||
title: str = Field(description="Brief summary")
|
||||
severity: Literal["low", "medium", "high", "critical"]
|
||||
description: str = Field(
|
||||
description="Detailed description",
|
||||
json_schema_extra={"ui": {"type": "textarea"}},
|
||||
)
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
mcp.add_provider(FormInput(model=BugReport))
|
||||
```
|
||||
|
||||
This registers two tools:
|
||||
|
||||
| Tool | Visibility | Purpose |
|
||||
|------|-----------|---------|
|
||||
| `collect_bugreport` | Model | Opens the form UI |
|
||||
| `submit_form` | App only | Validates and processes the submission |
|
||||
|
||||
The tool name is derived from the model class name, lowercased: `collect_{modelname}`. So `BugReport` becomes `collect_bugreport`, `ShippingAddress` becomes `collect_shippingaddress`. Use `tool_name` to override if needed. The LLM calls it with a prompt explaining what it needs, and the user gets a form with fields matching the model.
|
||||
|
||||
## Field mapping
|
||||
|
||||
`FormInput` uses Prefab's `Form.from_model()`, which maps Pydantic types to form components:
|
||||
|
||||
| Python type | Form component |
|
||||
|------------|---------------|
|
||||
| `str` | Text input |
|
||||
| `int`, `float` | Number input |
|
||||
| `bool` | Checkbox |
|
||||
| `datetime.date` | Date picker |
|
||||
| `Literal[...]` | Select dropdown |
|
||||
| `SecretStr` | Password input |
|
||||
|
||||
Use `Field()` metadata to control labels (`title`), placeholders (`description`), and validation (`min_length`, `max_length`, `ge`, `le`). Use `json_schema_extra={"ui": {"type": "textarea"}}` for multiline text.
|
||||
|
||||
## Callback
|
||||
|
||||
By default, the validated model is returned as JSON. Provide an `on_submit` callback to process the data server-side:
|
||||
|
||||
```python
|
||||
def save_report(report: BugReport) -> str:
|
||||
db.insert(report.model_dump())
|
||||
return f"Bug #{db.last_id} filed: {report.title}"
|
||||
|
||||
mcp.add_provider(FormInput(model=BugReport, on_submit=save_report))
|
||||
```
|
||||
|
||||
The callback receives a validated model instance and returns a string that becomes the tool result.
|
||||
|
||||
## Configuration
|
||||
|
||||
```python
|
||||
FormInput(
|
||||
model=BugReport, # Required: the Pydantic model
|
||||
name="BugTracker", # App name (default: model name)
|
||||
title="File a Bug", # Card heading (default: model name)
|
||||
tool_name="file_bug", # Tool name (default: collect_{model})
|
||||
submit_text="Submit Report", # Button label (default: "Submit")
|
||||
on_submit=save_report, # Optional callback
|
||||
send_message=True, # Push result as a chat message
|
||||
)
|
||||
```
|
||||
|
||||
Set `send_message=True` to push the result back into the conversation via `SendMessage`, triggering the LLM's next turn. Without it, the result is just the tool return value.
|
||||
|
||||
## Multiple forms
|
||||
|
||||
Add multiple providers for different models — each gets its own tool:
|
||||
|
||||
```python
|
||||
mcp = FastMCP(
|
||||
"My Server",
|
||||
providers=[
|
||||
FormInput(model=ShippingAddress),
|
||||
FormInput(model=BugReport),
|
||||
FormInput(model=ContactInfo),
|
||||
],
|
||||
)
|
||||
```
|
||||
197
docs/v3/apps/quickstart.mdx
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
---
|
||||
title: Quickstart
|
||||
sidebarTitle: Quickstart
|
||||
description: Build your first FastMCP app in under a minute.
|
||||
icon: rocket
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
import { PrefabDemoFrame } from '/snippets/prefab-demo-frame.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
By the end of this page, you'll have a working tool that returns this:
|
||||
|
||||
<PrefabDemoFrame demo="team-directory" height="545px" title="Team directory demo" />
|
||||
|
||||
A pie chart the user can hover, a table they can sort and search — and a single Python tool.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install "fastmcp[apps]"
|
||||
```
|
||||
|
||||
The `apps` extra pulls in [Prefab](https://prefab.prefect.io), the Python component library used to build app UIs.
|
||||
|
||||
## Write the tool
|
||||
|
||||
Create `server.py`. The interesting parts: `app=True` tells FastMCP this tool renders a UI, and `with PrefabApp() as app:` is the canonical pattern for composing one.
|
||||
|
||||
```python server.py expandable
|
||||
from collections import Counter
|
||||
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, DataTable, DataTableColumn, Grid
|
||||
from prefab_ui.components.charts import PieChart
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("My First App")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def team_directory() -> PrefabApp:
|
||||
"""Browse the team directory."""
|
||||
members = [
|
||||
{"name": "Alice Chen", "role": "Staff Engineer", "office": "San Francisco"},
|
||||
{"name": "Bob Martinez", "role": "Lead Designer", "office": "New York"},
|
||||
{"name": "Carol Johnson", "role": "Senior Engineer", "office": "London"},
|
||||
{"name": "David Kim", "role": "Product Manager", "office": "San Francisco"},
|
||||
{"name": "Eva Mueller", "role": "Engineer", "office": "Berlin"},
|
||||
{"name": "Frank Lee", "role": "Data Scientist", "office": "San Francisco"},
|
||||
{"name": "Grace Park", "role": "Engineering Manager", "office": "New York"},
|
||||
]
|
||||
|
||||
office_counts = [
|
||||
{"office": office, "count": count}
|
||||
for office, count in Counter(m["office"] for m in members).items()
|
||||
]
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
with Grid(columns=[1, 2], gap=4):
|
||||
PieChart(
|
||||
data=office_counts,
|
||||
data_key="count",
|
||||
name_key="office",
|
||||
show_legend=True,
|
||||
)
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="role", header="Role", sortable=True),
|
||||
DataTableColumn(key="office", header="Office", sortable=True),
|
||||
],
|
||||
rows=members,
|
||||
search=True,
|
||||
)
|
||||
|
||||
return app
|
||||
```
|
||||
|
||||
The Prefab code reads top-to-bottom. `PrefabApp()` is the root; everything inside its `with` block becomes the UI. `Column` stacks children vertically, `Grid` lays them out in columns. `DataTable` takes rows and column definitions and gives you sort and search for free.
|
||||
|
||||
`app=True` does the rest: it sets up the renderer resource, the content security policy, and the metadata that tells the host "this tool returns a UI." The host loads the result in a sandboxed iframe where the user can interact with it — all client-side, no round-trips.
|
||||
|
||||
## Preview it
|
||||
|
||||
FastMCP ships a dev server that renders your app tools in a browser, no MCP host needed:
|
||||
|
||||
```bash
|
||||
fastmcp dev apps server.py
|
||||
```
|
||||
|
||||
Open `http://localhost:8080`, pick `team_directory`, and try sorting columns and searching.
|
||||
|
||||
<Frame>
|
||||
<img src="/apps/images/app-quickstart-dev-2.png" alt="The team directory rendered in the fastmcp dev apps preview, showing a pie chart, searchable table, and a detail card after clicking a row" />
|
||||
</Frame>
|
||||
|
||||
## Make it reactive
|
||||
|
||||
The UI above renders once from your Python. Prefab apps can also respond to user input live, without any server round-trips. The key concept is **state**: a client-side key-value store that components read from and write to.
|
||||
|
||||
Click a row in the demo below to see a detail card appear:
|
||||
|
||||
<PrefabDemoFrame demo="team-directory-reactive" height="675px" title="Reactive team directory demo" />
|
||||
|
||||
Add a few imports, give each member a couple more fields, wire up a click handler, and render a detail card when something's selected:
|
||||
|
||||
```python expandable server.py
|
||||
from collections import Counter
|
||||
|
||||
from prefab_ui.actions import SetState
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
Badge, Card, CardContent, CardHeader, Column, DataTable, DataTableColumn,
|
||||
Grid, H3, Row, Small, Text,
|
||||
)
|
||||
from prefab_ui.components.charts import PieChart
|
||||
from prefab_ui.components.control_flow import If
|
||||
from prefab_ui.rx import Rx, STATE
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("My First App")
|
||||
|
||||
MEMBERS = [
|
||||
{"name": "Alice Chen", "role": "Staff Engineer", "office": "San Francisco", "email": "alice@company.com", "projects": 3},
|
||||
{"name": "Bob Martinez", "role": "Lead Designer", "office": "New York", "email": "bob@company.com", "projects": 5},
|
||||
# ... more members ...
|
||||
]
|
||||
|
||||
OFFICE_COUNTS = [
|
||||
{"office": o, "count": c}
|
||||
for o, c in Counter(m["office"] for m in MEMBERS).items()
|
||||
]
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def team_directory() -> PrefabApp:
|
||||
"""Browse the team directory."""
|
||||
with PrefabApp(state={"selected": None}) as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
with Grid(columns=[1, 2], gap=4):
|
||||
PieChart(
|
||||
data=OFFICE_COUNTS,
|
||||
data_key="count",
|
||||
name_key="office",
|
||||
show_legend=True,
|
||||
)
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="role", header="Role", sortable=True),
|
||||
DataTableColumn(key="office", header="Office", sortable=True),
|
||||
],
|
||||
rows=MEMBERS,
|
||||
search=True,
|
||||
on_row_click=SetState("selected", Rx("$event")),
|
||||
)
|
||||
|
||||
with If(STATE.selected):
|
||||
with Card():
|
||||
with CardHeader():
|
||||
with Row(gap=2, align="center"):
|
||||
H3(Rx("selected.name"))
|
||||
Badge(Rx("selected.office"))
|
||||
with CardContent():
|
||||
with Grid(columns=3, gap=4):
|
||||
with Column(gap=0):
|
||||
Small("Role")
|
||||
Text(Rx("selected.role"))
|
||||
with Column(gap=0):
|
||||
Small("Email")
|
||||
Text(Rx("selected.email"))
|
||||
with Column(gap=0):
|
||||
Small("Active Projects")
|
||||
Text(Rx("selected.projects"))
|
||||
|
||||
return app
|
||||
```
|
||||
|
||||
Three new ideas do all the work:
|
||||
|
||||
- **`on_row_click=SetState("selected", Rx("$event"))`** — clicking a row writes its data into the `selected` state key. `$event` is the clicked row dict.
|
||||
- **`Rx("selected.name")`** — a reactive reference. It doesn't hold a Python value; it compiles to a browser-side expression that re-evaluates whenever `selected` changes, so `Text(Rx("selected.name"))` always shows the latest clicked name.
|
||||
- **`If(STATE.selected)`** — conditionally renders its body. Before any click, `selected` is `None` and the card stays hidden.
|
||||
|
||||
The `state={"selected": None}` dict on `PrefabApp` sets the initial value. Everything else happens in the browser — no round-trips to your server when the user clicks.
|
||||
|
||||
## Where to go next
|
||||
|
||||
You've built a tool that returns an interactive, reactive UI. This pattern covers a huge range of use cases: build a visualization, return it, and the user gets it rendered right in the conversation.
|
||||
|
||||
- **[Interactive Tools](/apps/prefab)** — charts, tables, dashboards, reactive state, with live demos
|
||||
- **[FastMCPApp](/apps/fastmcp-app)** — when the UI needs to call back to your server (forms, search, CRUD)
|
||||
- **[Examples](/apps/examples)** — complete working servers you can run today
|
||||
3759
docs/v3/changelog.mdx
Normal file
85
docs/v3/cli/auth.mdx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
title: Auth Utilities
|
||||
sidebarTitle: Auth
|
||||
description: Create and validate CIMD documents for OAuth
|
||||
icon: key
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
The `fastmcp auth` commands help with CIMD (Client ID Metadata Document) management — part of MCP's OAuth authentication flow. A CIMD is a JSON document you host at an HTTPS URL to identify your client application to MCP servers.
|
||||
|
||||
## Creating a CIMD
|
||||
|
||||
`fastmcp auth cimd create` generates a CIMD document:
|
||||
|
||||
```bash
|
||||
fastmcp auth cimd create \
|
||||
--name "My App" \
|
||||
--redirect-uri "http://localhost:*/callback"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "https://your-domain.com/oauth/client.json",
|
||||
"client_name": "My App",
|
||||
"redirect_uris": ["http://localhost:*/callback"],
|
||||
"token_endpoint_auth_method": "none"
|
||||
}
|
||||
```
|
||||
|
||||
The generated document includes a placeholder `client_id` — update it to match the URL where you'll host the document before deploying.
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Flag | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| Name | `--name` | **Required.** Human-readable client name |
|
||||
| Redirect URI | `--redirect-uri` | **Required.** Allowed redirect URIs (repeatable) |
|
||||
| Client URI | `--client-uri` | Client's home page URL |
|
||||
| Logo URI | `--logo-uri` | Client's logo URL |
|
||||
| Scope | `--scope` | Space-separated list of scopes |
|
||||
| Output | `--output`, `-o` | Save to file (default: stdout) |
|
||||
| Pretty | `--pretty` | Pretty-print JSON (default: true) |
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
fastmcp auth cimd create \
|
||||
--name "My Production App" \
|
||||
--redirect-uri "http://localhost:*/callback" \
|
||||
--redirect-uri "https://myapp.example.com/callback" \
|
||||
--client-uri "https://myapp.example.com" \
|
||||
--scope "read write" \
|
||||
--output client.json
|
||||
```
|
||||
|
||||
## Validating a CIMD
|
||||
|
||||
`fastmcp auth cimd validate` fetches a hosted CIMD and verifies it conforms to the spec:
|
||||
|
||||
```bash
|
||||
fastmcp auth cimd validate https://myapp.example.com/oauth/client.json
|
||||
```
|
||||
|
||||
The validator checks that the URL is valid (HTTPS, non-root path), the document is valid JSON, the `client_id` matches the URL, and no shared-secret auth methods are used.
|
||||
|
||||
On success:
|
||||
|
||||
```
|
||||
→ Fetching https://myapp.example.com/oauth/client.json...
|
||||
✓ Valid CIMD document
|
||||
|
||||
Document details:
|
||||
client_id: https://myapp.example.com/oauth/client.json
|
||||
client_name: My App
|
||||
token_endpoint_auth_method: none
|
||||
redirect_uris:
|
||||
• http://localhost:*/callback
|
||||
```
|
||||
|
||||
| Option | Flag | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| Timeout | `--timeout`, `-t` | HTTP request timeout in seconds (default: 10) |
|
||||
144
docs/v3/cli/client.mdx
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
title: Client Commands
|
||||
sidebarTitle: Client
|
||||
description: List tools, call them, and discover configured servers
|
||||
icon: satellite-dish
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
The CLI can act as an MCP client — connecting to any server (local or remote) to list what it exposes and call its tools directly. This is useful for development, debugging, scripting, and giving shell-capable LLM agents access to MCP servers.
|
||||
|
||||
## Listing Tools
|
||||
|
||||
`fastmcp list` connects to a server and prints its tools as function signatures, showing parameter names, types, and descriptions at a glance:
|
||||
|
||||
```bash
|
||||
fastmcp list http://localhost:8000/mcp
|
||||
fastmcp list server.py
|
||||
fastmcp list weather # name-based resolution
|
||||
```
|
||||
|
||||
When you need the full JSON Schema for a tool's inputs or outputs — for understanding nested objects, enum constraints, or complex types — opt in with `--input-schema` or `--output-schema`:
|
||||
|
||||
```bash
|
||||
fastmcp list server.py --input-schema
|
||||
```
|
||||
|
||||
### Resources and Prompts
|
||||
|
||||
By default, only tools are shown. Add `--resources` or `--prompts` to include those:
|
||||
|
||||
```bash
|
||||
fastmcp list server.py --resources --prompts
|
||||
```
|
||||
|
||||
### Machine-Readable Output
|
||||
|
||||
The `--json` flag switches to structured JSON with full schemas included. This is the format to use when feeding tool definitions to an LLM or building automation:
|
||||
|
||||
```bash
|
||||
fastmcp list server.py --json
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Flag | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| Command | `--command` | Connect via stdio (e.g., `'npx -y @mcp/server'`) |
|
||||
| Transport | `--transport`, `-t` | Force `http` or `sse` for URL targets |
|
||||
| Resources | `--resources` | Include resources in output |
|
||||
| Prompts | `--prompts` | Include prompts in output |
|
||||
| Input Schema | `--input-schema` | Show full input schemas |
|
||||
| Output Schema | `--output-schema` | Show full output schemas |
|
||||
| JSON | `--json` | Structured JSON output |
|
||||
| Timeout | `--timeout` | Connection timeout in seconds |
|
||||
| Auth | `--auth` | `oauth` (default for HTTP), a bearer token, or `none` |
|
||||
|
||||
## Calling Tools
|
||||
|
||||
`fastmcp call` invokes a single tool on a server. Pass arguments as `key=value` pairs — the CLI fetches the tool's schema and coerces your string values to the right types automatically:
|
||||
|
||||
```bash
|
||||
fastmcp call server.py greet name=World
|
||||
fastmcp call http://localhost:8000/mcp search query=hello limit=5
|
||||
```
|
||||
|
||||
Type coercion is schema-driven: `"5"` becomes the integer `5` when the schema expects an integer. Booleans accept `true`/`false`, `yes`/`no`, and `1`/`0`. Arrays and objects are parsed as JSON.
|
||||
|
||||
### Complex Arguments
|
||||
|
||||
For tools with nested or structured parameters, `key=value` syntax gets awkward. Pass a single JSON object instead:
|
||||
|
||||
```bash
|
||||
fastmcp call server.py create_item '{"name": "Widget", "tags": ["sale"], "metadata": {"color": "blue"}}'
|
||||
```
|
||||
|
||||
Or use `--input-json` to provide a base dictionary, then override individual keys with `key=value` pairs:
|
||||
|
||||
```bash
|
||||
fastmcp call server.py search --input-json '{"query": "hello", "limit": 5}' limit=10
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
If you misspell a tool name, the CLI suggests corrections via fuzzy matching. Missing required arguments produce a clear message with the tool's signature as a reminder. Tool execution errors are printed with a non-zero exit code, making the CLI straightforward to use in scripts.
|
||||
|
||||
### Structured Output
|
||||
|
||||
`--json` emits the raw result including content blocks, error status, and structured content:
|
||||
|
||||
```bash
|
||||
fastmcp call server.py get_weather city=London --json
|
||||
```
|
||||
|
||||
### Interactive Elicitation
|
||||
|
||||
Some tools request additional input during execution through MCP's elicitation mechanism. When this happens, the CLI prompts you in the terminal — showing each field's name, type, and whether it's required. You can type `decline` to skip a question or `cancel` to abort the call entirely.
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Flag | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| Command | `--command` | Connect via stdio |
|
||||
| Transport | `--transport`, `-t` | Force `http` or `sse` |
|
||||
| Input JSON | `--input-json` | Base arguments as JSON (merged with `key=value`) |
|
||||
| JSON | `--json` | Raw JSON output |
|
||||
| Timeout | `--timeout` | Connection timeout in seconds |
|
||||
| Auth | `--auth` | `oauth`, a bearer token, or `none` |
|
||||
|
||||
## Discovering Configured Servers
|
||||
|
||||
`fastmcp discover` scans your machine for MCP servers configured in editors and tools. It checks:
|
||||
|
||||
- **Claude Desktop** — `claude_desktop_config.json`
|
||||
- **Claude Code** — `~/.claude.json`
|
||||
- **Cursor** — `.cursor/mcp.json` (walks up from current directory)
|
||||
- **Gemini CLI** — `~/.gemini/settings.json`
|
||||
- **Goose** — `~/.config/goose/config.yaml`
|
||||
- **Project** — `./mcp.json` in the current directory
|
||||
|
||||
```bash
|
||||
fastmcp discover
|
||||
```
|
||||
|
||||
The output groups servers by source, showing each server's name and transport. Filter by source or get machine-readable output:
|
||||
|
||||
```bash
|
||||
fastmcp discover --source claude-code
|
||||
fastmcp discover --source cursor --source gemini --json
|
||||
```
|
||||
|
||||
Any server that appears here can be used by name with `list`, `call`, and other commands — so you can go from "I have a server in Claude Code" to querying it without copying URLs or paths.
|
||||
|
||||
## LLM Agent Integration
|
||||
|
||||
For LLM agents that can execute shell commands but don't have native MCP support, the CLI provides a clean bridge. The agent calls `fastmcp list --json` to discover available tools with full schemas, then `fastmcp call --json` to invoke them with structured results.
|
||||
|
||||
Because the CLI handles connection management, transport selection, and type coercion internally, the agent doesn't need to understand MCP protocol details — it just reads JSON and constructs shell commands.
|
||||
|
||||
## Remote Stdio Bridges
|
||||
|
||||
For MCP hosts that expect a local stdio command but need to connect to a remote HTTP server, use [`fastmcp-remote`](/clients/fastmcp-remote). It provides a small standalone bridge for host configuration, while `fastmcp list` and `fastmcp call` remain focused on direct inspection and invocation from the terminal.
|
||||
106
docs/v3/cli/generate-cli.mdx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
---
|
||||
title: Generate CLI
|
||||
sidebarTitle: Generate CLI
|
||||
description: Scaffold a standalone typed CLI from any MCP server
|
||||
icon: wand-magic-sparkles
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
`fastmcp list` and `fastmcp call` are general-purpose — you always specify the server, the tool name, and the arguments from scratch. `fastmcp generate-cli` goes further: it connects to a server, reads its tool schemas, and writes a standalone Python script where every tool is a proper subcommand with typed flags, help text, and tab completion. The result is a CLI that feels hand-written for that specific server.
|
||||
|
||||
MCP tool schemas already contain everything a CLI framework needs — parameter names, types, descriptions, required/optional status, and defaults. `generate-cli` maps that into [cyclopts](https://cyclopts.readthedocs.io/) commands, so JSON Schema types become Python type annotations, descriptions become `--help` text, and required parameters become mandatory flags.
|
||||
|
||||
## Generating a Script
|
||||
|
||||
Point the command at any [server target](/cli/overview#server-targets) and it writes a CLI script:
|
||||
|
||||
```bash
|
||||
fastmcp generate-cli weather
|
||||
fastmcp generate-cli http://localhost:8000/mcp
|
||||
fastmcp generate-cli server.py my_weather_cli.py
|
||||
```
|
||||
|
||||
The second positional argument sets the output path (defaults to `cli.py`). If the file already exists, pass `-f` to overwrite:
|
||||
|
||||
```bash
|
||||
fastmcp generate-cli weather -f
|
||||
```
|
||||
|
||||
## What You Get
|
||||
|
||||
The generated script is a regular Python file — executable, editable, and yours:
|
||||
|
||||
```
|
||||
$ python cli.py call-tool --help
|
||||
Usage: weather-cli call-tool COMMAND
|
||||
|
||||
Call a tool on the server
|
||||
|
||||
Commands:
|
||||
get_forecast Get the weather forecast for a city.
|
||||
search_city Search for a city by name.
|
||||
```
|
||||
|
||||
Each tool has typed parameters with help text pulled directly from the server's schema:
|
||||
|
||||
```
|
||||
$ python cli.py call-tool get_forecast --help
|
||||
Usage: weather-cli call-tool get_forecast [OPTIONS]
|
||||
|
||||
Get the weather forecast for a city.
|
||||
|
||||
Options:
|
||||
--city [str] City name (required)
|
||||
--days [int] Number of forecast days (default: 3)
|
||||
```
|
||||
|
||||
Beyond tool commands, the script includes generic MCP operations — `list-tools`, `list-resources`, `read-resource`, `list-prompts`, and `get-prompt` — that always reflect the server's current state, even if tools have changed since generation.
|
||||
|
||||
## Parameter Handling
|
||||
|
||||
Parameters are mapped based on their JSON Schema type:
|
||||
|
||||
**Simple types** (`string`, `integer`, `number`, `boolean`) become typed flags:
|
||||
|
||||
```bash
|
||||
python cli.py call-tool get_forecast --city London --days 3
|
||||
```
|
||||
|
||||
**Arrays of simple types** become repeatable flags:
|
||||
|
||||
```bash
|
||||
python cli.py call-tool tag_items --tags python --tags fastapi --tags mcp
|
||||
```
|
||||
|
||||
**Complex types** (objects, nested arrays, unions) accept JSON strings. The `--help` output shows the full schema so you know what structure to pass:
|
||||
|
||||
```bash
|
||||
python cli.py call-tool create_user \
|
||||
--name John \
|
||||
--metadata '{"role": "admin", "dept": "engineering"}'
|
||||
```
|
||||
|
||||
## Agent Skill
|
||||
|
||||
Alongside the CLI script, `generate-cli` writes a `SKILL.md` file — a [Claude Code agent skill](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/skills) that documents every tool's exact invocation syntax, parameter flags, types, and descriptions. An agent can pick up the CLI immediately without running `--help` or experimenting with flag names.
|
||||
|
||||
To skip skill generation:
|
||||
|
||||
```bash
|
||||
fastmcp generate-cli weather --no-skill
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The generated script is a *client*, not a server — it connects to the server on every invocation rather than bundling it. A `CLIENT_SPEC` variable at the top holds the resolved transport (a URL string or `StdioTransport` with baked-in command and arguments).
|
||||
|
||||
The most common edit is changing `CLIENT_SPEC` — for example, pointing a script generated from a dev server at production. Beyond that, the helper functions (`_call_tool`, `_print_tool_result`) are thin wrappers around `fastmcp.Client` that are easy to adapt.
|
||||
|
||||
The script requires `fastmcp` as a dependency. If it lives outside a project that already has FastMCP installed:
|
||||
|
||||
```bash
|
||||
uv run --with fastmcp python cli.py call-tool get_forecast --city London
|
||||
```
|
||||
72
docs/v3/cli/inspecting.mdx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
title: Inspecting Servers
|
||||
sidebarTitle: Inspecting
|
||||
description: View a server's components and metadata
|
||||
icon: magnifying-glass
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="2.9.0" />
|
||||
|
||||
`fastmcp inspect` loads a server and reports what it contains — its tools, resources, prompts, version, and metadata. The default output is a human-readable summary:
|
||||
|
||||
```bash
|
||||
fastmcp inspect server.py
|
||||
```
|
||||
|
||||
```
|
||||
Server: MyServer
|
||||
Instructions: A helpful MCP server
|
||||
Version: 1.0.0
|
||||
|
||||
Components:
|
||||
Tools: 5
|
||||
Prompts: 2
|
||||
Resources: 3
|
||||
Templates: 1
|
||||
|
||||
Environment:
|
||||
FastMCP: 2.0.0
|
||||
MCP: 1.0.0
|
||||
|
||||
Use --format [fastmcp|mcp] for complete JSON output
|
||||
```
|
||||
|
||||
## JSON Output
|
||||
|
||||
For programmatic use, two JSON formats are available:
|
||||
|
||||
**FastMCP format** (`--format fastmcp`) includes everything FastMCP knows about the server — tool tags, enabled status, output schemas, annotations, and custom metadata. Field names use `snake_case`. This is the format for debugging and introspecting FastMCP servers.
|
||||
|
||||
**MCP protocol format** (`--format mcp`) shows exactly what MCP clients see through the protocol — only standard MCP fields, `camelCase` names, no FastMCP-specific extensions. This is the format for verifying client compatibility and debugging what clients actually receive.
|
||||
|
||||
```bash
|
||||
# Full FastMCP metadata to stdout
|
||||
fastmcp inspect server.py --format fastmcp
|
||||
|
||||
# MCP protocol view saved to file
|
||||
fastmcp inspect server.py --format mcp -o manifest.json
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Flag | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| Format | `--format`, `-f` | `fastmcp` or `mcp` (required when using `-o`) |
|
||||
| Output File | `--output`, `-o` | Save to file instead of stdout |
|
||||
|
||||
## Entrypoints
|
||||
|
||||
The `inspect` command supports the same local entrypoints as [`fastmcp run`](/cli/running): inferred instances, explicit entrypoints, factory functions, and `fastmcp.json` configs.
|
||||
|
||||
```bash
|
||||
fastmcp inspect server.py # inferred instance
|
||||
fastmcp inspect server.py:my_server # explicit entrypoint
|
||||
fastmcp inspect server.py:create_server # factory function
|
||||
fastmcp inspect fastmcp.json # config file
|
||||
```
|
||||
|
||||
<Warning>
|
||||
`inspect` only works with local files and `fastmcp.json` — it doesn't connect to remote URLs or standard MCP config files.
|
||||
</Warning>
|
||||
146
docs/v3/cli/install-mcp.mdx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
---
|
||||
title: Install MCP Servers
|
||||
sidebarTitle: Install MCPs
|
||||
description: Install MCP servers into Claude, Cursor, Gemini, and other clients
|
||||
icon: download
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="2.10.3" />
|
||||
|
||||
`fastmcp install` registers a server with an MCP client application so the client can launch it automatically. Each MCP client runs servers in its own isolated environment, which means dependencies need to be explicitly declared — you can't rely on whatever happens to be installed locally.
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop server.py
|
||||
fastmcp install claude-code server.py --with pandas --with matplotlib
|
||||
fastmcp install cursor server.py -e .
|
||||
```
|
||||
|
||||
<Warning>
|
||||
`uv` must be installed and available in your system PATH. Both Claude Desktop and Cursor run servers in isolated environments managed by `uv`. On macOS, install it globally with Homebrew for Claude Desktop compatibility: `brew install uv`.
|
||||
</Warning>
|
||||
|
||||
## Supported Clients
|
||||
|
||||
| Client | Install method |
|
||||
| ------ | -------------- |
|
||||
| `claude-code` | Claude Code's built-in MCP management |
|
||||
| `claude-desktop` | Direct config file modification |
|
||||
| `cursor` | Deeplink that opens Cursor for confirmation |
|
||||
| `gemini-cli` | Gemini CLI's built-in MCP management |
|
||||
| `goose` | Deeplink that opens Goose for confirmation (uses `uvx`) |
|
||||
| `mcp-json` | Generates standard MCP JSON config for manual use |
|
||||
| `stdio` | Outputs the shell command to run via stdio |
|
||||
|
||||
## Declaring Dependencies
|
||||
|
||||
Because MCP clients run servers in isolation, you need to tell the install command what your server needs. There are two approaches:
|
||||
|
||||
**Command-line flags** let you specify dependencies directly:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop server.py --with pandas --with "sqlalchemy>=2.0"
|
||||
fastmcp install cursor server.py -e . --with-requirements requirements.txt
|
||||
```
|
||||
|
||||
**`fastmcp.json`** configuration files declare dependencies alongside the server definition. When you install from a config file, dependencies are picked up automatically:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop fastmcp.json
|
||||
fastmcp install claude-desktop # auto-detects fastmcp.json in current directory
|
||||
```
|
||||
|
||||
See [Server Configuration](/deployment/server-configuration) for the full config format.
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Flag | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| Server Name | `--server-name`, `-n` | Custom name for the server |
|
||||
| Editable Package | `--with-editable`, `-e` | Install a directory in editable mode |
|
||||
| Extra Packages | `--with` | Additional packages (repeatable) |
|
||||
| Environment Variables | `--env` | `KEY=VALUE` pairs (repeatable) |
|
||||
| Environment File | `--env-file`, `-f` | Load env vars from a `.env` file |
|
||||
| Python | `--python` | Python version (e.g., `3.11`) |
|
||||
| Project | `--project` | Run within a uv project directory |
|
||||
| Requirements | `--with-requirements` | Install from a requirements file |
|
||||
| Config Path | `--config-path` | Custom path to Claude Desktop config directory (`claude-desktop` only) |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Basic install with auto-detected server instance
|
||||
fastmcp install claude-desktop server.py
|
||||
|
||||
# Install from fastmcp.json with auto-detection
|
||||
fastmcp install claude-desktop
|
||||
|
||||
# Explicit entrypoint with dependencies
|
||||
fastmcp install claude-desktop server.py:my_server \
|
||||
--server-name "My Analysis Server" \
|
||||
--with pandas
|
||||
|
||||
# With environment variables
|
||||
fastmcp install claude-code server.py \
|
||||
--env API_KEY=secret \
|
||||
--env DEBUG=true
|
||||
|
||||
# With env file
|
||||
fastmcp install cursor server.py --env-file .env
|
||||
|
||||
# Specific Python version and requirements file
|
||||
fastmcp install claude-desktop server.py \
|
||||
--python 3.11 \
|
||||
--with-requirements requirements.txt
|
||||
|
||||
# With custom config path (claude-desktop only)
|
||||
fastmcp install claude-desktop server.py \
|
||||
--config-path "C:\Users\username\AppData\Local\Packages\Claude_xyz\LocalCache\Roaming\Claude"
|
||||
```
|
||||
|
||||
## Generating MCP JSON
|
||||
|
||||
The `mcp-json` target generates standard MCP configuration JSON instead of installing into a specific client. This is useful for clients that FastMCP doesn't directly support, for CI/CD environments, or for sharing server configs:
|
||||
|
||||
```bash
|
||||
fastmcp install mcp-json server.py
|
||||
```
|
||||
|
||||
The output follows the standard format used by Claude Desktop, Cursor, and other MCP clients:
|
||||
|
||||
```json
|
||||
{
|
||||
"server-name": {
|
||||
"command": "uv",
|
||||
"args": ["run", "--with", "fastmcp", "fastmcp", "run", "/path/to/server.py"],
|
||||
"env": {
|
||||
"API_KEY": "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `--copy` to send it to your clipboard instead of stdout.
|
||||
|
||||
## Generating Stdio Commands
|
||||
|
||||
The `stdio` target outputs the shell command an MCP host would use to start your server over stdio:
|
||||
|
||||
```bash
|
||||
fastmcp install stdio server.py
|
||||
# Output: uv run --with fastmcp fastmcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
When installing from a `fastmcp.json`, dependencies from the config are included automatically:
|
||||
|
||||
```bash
|
||||
fastmcp install stdio fastmcp.json
|
||||
# Output: uv run --with fastmcp --with pillow --with 'qrcode[pil]>=8.0' fastmcp run /path/to/server.py
|
||||
```
|
||||
|
||||
Use `--copy` to copy to clipboard.
|
||||
|
||||
<Tip>
|
||||
`fastmcp install` is designed for local server files with stdio transport. For remote servers running over HTTP, use your client's native configuration — FastMCP's value here is simplifying the complex local setup with `uv`, dependencies, and environment variables.
|
||||
</Tip>
|
||||
104
docs/v3/cli/overview.mdx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
title: CLI
|
||||
sidebarTitle: Overview
|
||||
description: The fastmcp command-line interface
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
The `fastmcp` CLI is installed automatically with FastMCP. It's the primary way to run, test, install, and interact with MCP servers from your terminal.
|
||||
|
||||
```bash
|
||||
fastmcp --help
|
||||
```
|
||||
|
||||
## Commands at a Glance
|
||||
|
||||
| Command | What it does |
|
||||
| ------- | ------------ |
|
||||
| [`run`](/cli/running) | Run a server (local file, factory function, remote URL, or config file) |
|
||||
| [`dev apps`](/cli/running#previewing-apps) | Launch a browser-based preview UI for Prefab App tools |
|
||||
| [`dev inspector`](/cli/running#development-with-the-inspector) | Launch a server inside the MCP Inspector for interactive testing |
|
||||
| [`install`](/cli/install-mcp) | Install a server into Claude Code, Claude Desktop, Cursor, Gemini CLI, or Goose |
|
||||
| [`inspect`](/cli/inspecting) | Print a server's tools, resources, and prompts as a summary or JSON report |
|
||||
| [`list`](/cli/client) | List a server's tools (and optionally resources and prompts) |
|
||||
| [`call`](/cli/client#calling-tools) | Call a single tool with arguments |
|
||||
| [`discover`](/cli/client#discovering-configured-servers) | Find MCP servers configured in your editors and tools |
|
||||
| [`generate-cli`](/cli/generate-cli) | Scaffold a standalone typed CLI from a server's tool schemas |
|
||||
| [`project prepare`](/cli/running#pre-building-environments) | Pre-install dependencies into a reusable uv project |
|
||||
| [`auth cimd`](/cli/auth) | Create and validate CIMD documents for OAuth |
|
||||
| `version` | Print version info (`--copy` to copy to clipboard) |
|
||||
|
||||
## Server Targets
|
||||
|
||||
Most commands need to know *which server* to talk to. You pass a "server spec" as the first argument, and FastMCP resolves the right transport automatically.
|
||||
|
||||
**URLs** connect to a running HTTP server:
|
||||
|
||||
```bash
|
||||
fastmcp list http://localhost:8000/mcp
|
||||
fastmcp call http://localhost:8000/mcp get_forecast city=London
|
||||
```
|
||||
|
||||
**Python files** are loaded directly — no `mcp.run()` boilerplate needed. FastMCP finds a server instance named `mcp`, `server`, or `app` in the file, or you can specify one explicitly:
|
||||
|
||||
```bash
|
||||
fastmcp list server.py
|
||||
fastmcp run server.py:my_custom_server
|
||||
```
|
||||
|
||||
**Config files** work too — both FastMCP's own `fastmcp.json` format and standard MCP config files with an `mcpServers` key:
|
||||
|
||||
```bash
|
||||
fastmcp run fastmcp.json
|
||||
fastmcp list mcp-config.json
|
||||
```
|
||||
|
||||
**Stdio commands** connect to any MCP server that speaks over standard I/O. Use `--command` instead of a positional argument:
|
||||
|
||||
```bash
|
||||
fastmcp list --command 'npx -y @modelcontextprotocol/server-github'
|
||||
```
|
||||
|
||||
### Name-Based Resolution
|
||||
|
||||
If your servers are already configured in an editor or tool, you can refer to them by name. FastMCP scans configs from Claude Desktop, Claude Code, Cursor, Gemini CLI, and Goose:
|
||||
|
||||
```bash
|
||||
fastmcp list weather
|
||||
fastmcp call weather get_forecast city=London
|
||||
```
|
||||
|
||||
When the same name appears in multiple configs, use the `source:name` form to be specific:
|
||||
|
||||
```bash
|
||||
fastmcp list claude-code:my-server
|
||||
fastmcp call cursor:weather get_forecast city=London
|
||||
```
|
||||
|
||||
Run [`fastmcp discover`](/cli/client#discovering-configured-servers) to see what's available on your machine.
|
||||
|
||||
## Authentication
|
||||
|
||||
When targeting an HTTP URL, the CLI enables OAuth authentication by default. If the server requires it, you'll be guided through the flow (typically opening a browser). If it doesn't, the setup is a silent no-op.
|
||||
|
||||
To skip authentication entirely — useful for local development servers — pass `--auth none`:
|
||||
|
||||
```bash
|
||||
fastmcp call http://localhost:8000/mcp my_tool --auth none
|
||||
```
|
||||
|
||||
You can also pass a bearer token directly:
|
||||
|
||||
```bash
|
||||
fastmcp list http://localhost:8000/mcp --auth "Bearer sk-..."
|
||||
```
|
||||
|
||||
## Transport Override
|
||||
|
||||
FastMCP defaults to Streamable HTTP for URL targets. If the server only supports Server-Sent Events (SSE), force the older transport:
|
||||
|
||||
```bash
|
||||
fastmcp list http://localhost:8000 --transport sse
|
||||
```
|
||||
166
docs/v3/cli/running.mdx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
---
|
||||
title: Running Servers
|
||||
sidebarTitle: Running
|
||||
description: Start, develop, and configure servers from the command line
|
||||
icon: play
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
## Starting a Server
|
||||
|
||||
`fastmcp run` starts a server. Point it at a Python file, a factory function, a remote URL, or a config file:
|
||||
|
||||
```bash
|
||||
fastmcp run server.py
|
||||
fastmcp run server.py:create_server
|
||||
fastmcp run https://example.com/mcp
|
||||
fastmcp run fastmcp.json
|
||||
```
|
||||
|
||||
By default, the server runs over **stdio** — the transport that MCP clients like Claude Desktop expect. To serve over HTTP instead, specify the transport:
|
||||
|
||||
```bash
|
||||
fastmcp run server.py --transport http
|
||||
fastmcp run server.py --transport http --host 0.0.0.0 --port 9000
|
||||
```
|
||||
|
||||
### Entrypoints
|
||||
|
||||
FastMCP supports several ways to locate and start your server:
|
||||
|
||||
**Inferred instance** — FastMCP imports the file and looks for a variable named `mcp`, `server`, or `app`:
|
||||
|
||||
```bash
|
||||
fastmcp run server.py
|
||||
```
|
||||
|
||||
**Explicit instance** — point at a specific variable:
|
||||
|
||||
```bash
|
||||
fastmcp run server.py:my_server
|
||||
```
|
||||
|
||||
**Factory function** — FastMCP calls the function and uses the returned server. Useful when your server needs async setup or configuration that runs before startup:
|
||||
|
||||
```bash
|
||||
fastmcp run server.py:create_server
|
||||
```
|
||||
|
||||
**Remote URL** — starts a local proxy that bridges to a remote server. Handy for local development against a deployed server, or for bridging a remote HTTP server to stdio:
|
||||
|
||||
```bash
|
||||
fastmcp run https://example.com/mcp
|
||||
```
|
||||
|
||||
**FastMCP config** — uses a `fastmcp.json` file that declaratively specifies the server, its dependencies, and deployment settings. When you run `fastmcp run` with no arguments, it auto-detects `fastmcp.json` in the current directory:
|
||||
|
||||
```bash
|
||||
fastmcp run
|
||||
fastmcp run my-config.fastmcp.json
|
||||
```
|
||||
|
||||
See [Server Configuration](/deployment/server-configuration) for the full `fastmcp.json` format.
|
||||
|
||||
**MCP config** — runs servers defined in a standard MCP configuration file (any `.json` with an `mcpServers` key):
|
||||
|
||||
```bash
|
||||
fastmcp run mcp.json
|
||||
```
|
||||
|
||||
<Warning>
|
||||
`fastmcp run` completely ignores the `if __name__ == "__main__"` block. Any setup code in that block won't execute. If you need initialization logic to run, use a [factory function](/cli/overview#factory-functions).
|
||||
</Warning>
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Flag | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| Transport | `--transport`, `-t` | `stdio` (default), `http`, or `sse` |
|
||||
| Host | `--host` | Bind address for HTTP (default: `127.0.0.1`) |
|
||||
| Port | `--port`, `-p` | Bind port for HTTP (default: `8000`) |
|
||||
| Path | `--path` | URL path for HTTP (default: `/mcp/`) |
|
||||
| Log Level | `--log-level`, `-l` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
|
||||
| No Banner | `--no-banner` | Suppress the startup banner |
|
||||
| Auto-Reload | `--reload` / `--no-reload` | Watch for file changes and restart automatically |
|
||||
| Reload Dirs | `--reload-dir` | Directories to watch (repeatable) |
|
||||
| Skip Env | `--skip-env` | Don't set up a uv environment (use when already in one) |
|
||||
| Python | `--python` | Python version to use (e.g., `3.11`) |
|
||||
| Extra Packages | `--with` | Additional packages to install (repeatable) |
|
||||
| Project | `--project` | Run within a specific uv project directory |
|
||||
| Requirements | `--with-requirements` | Install from a requirements file |
|
||||
|
||||
### Dependency Management
|
||||
|
||||
By default, `fastmcp run` uses your current Python environment directly. When you pass `--python`, `--with`, `--project`, or `--with-requirements`, it switches to running via `uv run` in a subprocess, which handles dependency isolation automatically.
|
||||
|
||||
The `--skip-env` flag is useful when you're already inside an activated venv, a Docker container with pre-installed dependencies, or a uv-managed project — it prevents uv from trying to set up another environment layer.
|
||||
|
||||
## Previewing Apps
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
`fastmcp dev apps` launches a browser-based preview UI for servers with [Prefab App tools](/apps/prefab). It starts your MCP server on one port and a local dev UI on another — giving you a live, interactive picker where you can call app tools and see their rendered output without needing a full MCP host client.
|
||||
|
||||
```bash
|
||||
fastmcp dev apps server.py
|
||||
fastmcp dev apps server.py:mcp --mcp-port 9000 --dev-port 9090
|
||||
```
|
||||
|
||||
The picker auto-generates a form from each tool's input schema. Submit the form and the result opens in a new tab as a rendered Prefab UI.
|
||||
|
||||
Auto-reload is on by default — save a file and the MCP server restarts automatically.
|
||||
|
||||
<Tip>
|
||||
`fastmcp dev apps` requires `fastmcp[apps]` — install with `pip install "fastmcp[apps]"`.
|
||||
</Tip>
|
||||
|
||||
| Option | Flag | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| MCP Port | `--mcp-port` | Port for the MCP server (default: `8000`) |
|
||||
| Dev Port | `--dev-port` | Port for the dev UI (default: `8080`) |
|
||||
| Auto-Reload | `--reload` / `--no-reload` | Watch for file changes (default: on) |
|
||||
|
||||
## Development with the Inspector
|
||||
|
||||
`fastmcp dev inspector` launches your server inside the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), a browser-based tool for interactively testing MCP servers. Auto-reload is on by default, so your server restarts when you save changes.
|
||||
|
||||
```bash
|
||||
fastmcp dev inspector server.py
|
||||
fastmcp dev inspector server.py -e . --with pandas
|
||||
```
|
||||
|
||||
<Tip>
|
||||
The Inspector always runs your server via `uv run` in a subprocess — it never uses your local environment directly. Specify dependencies with `--with`, `--with-editable`, `--with-requirements`, or through a `fastmcp.json` file.
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
The Inspector connects over **stdio only**. When it launches, you may need to select "STDIO" from the transport dropdown and click connect. To test a server over HTTP, start it separately with `fastmcp run server.py --transport http` and point the Inspector at the URL.
|
||||
</Warning>
|
||||
|
||||
| Option | Flag | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| Editable Package | `--with-editable`, `-e` | Install a directory in editable mode |
|
||||
| Extra Packages | `--with` | Additional packages (repeatable) |
|
||||
| Inspector Version | `--inspector-version` | MCP Inspector version to use |
|
||||
| UI Port | `--ui-port` | Port for the Inspector UI |
|
||||
| Server Port | `--server-port` | Port for the Inspector proxy |
|
||||
| Auto-Reload | `--reload` / `--no-reload` | File watching (default: on) |
|
||||
| Reload Dirs | `--reload-dir` | Directories to watch (repeatable) |
|
||||
| Python | `--python` | Python version |
|
||||
| Project | `--project` | Run within a uv project directory |
|
||||
| Requirements | `--with-requirements` | Install from a requirements file |
|
||||
|
||||
## Pre-Building Environments
|
||||
|
||||
`fastmcp project prepare` creates a persistent uv project from a `fastmcp.json` file, pre-installing all dependencies. This separates environment setup from server execution — install once, run many times.
|
||||
|
||||
```bash
|
||||
# Step 1: Build the environment (slow, does dependency resolution)
|
||||
fastmcp project prepare fastmcp.json --output-dir ./env
|
||||
|
||||
# Step 2: Run using the prepared environment (fast, no install step)
|
||||
fastmcp run fastmcp.json --project ./env
|
||||
```
|
||||
|
||||
The prepared directory contains a `pyproject.toml`, a `.venv` with all packages installed, and a `uv.lock` for reproducibility. This is particularly useful in deployment scenarios where you want deterministic, pre-built environments.
|
||||
88
docs/v3/clients/auth/bearer.mdx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
title: Bearer Token Authentication
|
||||
sidebarTitle: Bearer Auth
|
||||
description: Authenticate your FastMCP client with a Bearer token.
|
||||
icon: key
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="2.6.0" />
|
||||
|
||||
<Tip>
|
||||
Bearer Token authentication is only relevant for HTTP-based transports.
|
||||
</Tip>
|
||||
|
||||
You can configure your FastMCP client to use **bearer authentication** by supplying a valid access token. This is most appropriate for service accounts, long-lived API keys, CI/CD, applications where authentication is managed separately, or other non-interactive authentication methods.
|
||||
|
||||
A Bearer token is a JSON Web Token (JWT) that is used to authenticate a request. It is most commonly used in the `Authorization` header of an HTTP request, using the `Bearer` scheme:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
|
||||
## Client Usage
|
||||
|
||||
The most straightforward way to use a pre-existing Bearer token is to provide it as a string to the `auth` parameter of the `fastmcp.Client` or transport instance. FastMCP will automatically format it correctly for the `Authorization` header and bearer scheme.
|
||||
|
||||
<Tip>
|
||||
If you're using a string token, do not include the `Bearer` prefix. FastMCP will add it for you.
|
||||
</Tip>
|
||||
|
||||
```python {5}
|
||||
from fastmcp import Client
|
||||
|
||||
async with Client(
|
||||
"https://your-server.fastmcp.app/mcp",
|
||||
auth="<your-token>",
|
||||
) as client:
|
||||
await client.ping()
|
||||
```
|
||||
|
||||
You can also supply a Bearer token to a transport instance, such as `StreamableHttpTransport` or `SSETransport`:
|
||||
|
||||
```python {6}
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.transports import StreamableHttpTransport
|
||||
|
||||
transport = StreamableHttpTransport(
|
||||
"http://your-server.fastmcp.app/mcp",
|
||||
auth="<your-token>",
|
||||
)
|
||||
|
||||
async with Client(transport) as client:
|
||||
await client.ping()
|
||||
```
|
||||
|
||||
## `BearerAuth` Helper
|
||||
|
||||
If you prefer to be more explicit and not rely on FastMCP to transform your string token, you can use the `BearerAuth` class yourself, which implements the `httpx.Auth` interface.
|
||||
|
||||
```python {6}
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import BearerAuth
|
||||
|
||||
async with Client(
|
||||
"https://your-server.fastmcp.app/mcp",
|
||||
auth=BearerAuth(token="<your-token>"),
|
||||
) as client:
|
||||
await client.ping()
|
||||
```
|
||||
|
||||
## Custom Headers
|
||||
|
||||
If the MCP server expects a custom header or token scheme, you can manually set the client's `headers` instead of using the `auth` parameter by setting them on your transport:
|
||||
|
||||
```python {5}
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.transports import StreamableHttpTransport
|
||||
|
||||
async with Client(
|
||||
transport=StreamableHttpTransport(
|
||||
"https://your-server.fastmcp.app/mcp",
|
||||
headers={"X-API-Key": "<your-token>"},
|
||||
),
|
||||
) as client:
|
||||
await client.ping()
|
||||
```
|
||||
138
docs/v3/clients/auth/cimd.mdx
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
---
|
||||
title: CIMD Authentication
|
||||
sidebarTitle: CIMD
|
||||
description: Use Client ID Metadata Documents for verifiable, domain-based client identity.
|
||||
icon: id-badge
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
<Tip>
|
||||
CIMD authentication is only relevant for HTTP-based transports and requires a server that advertises CIMD support.
|
||||
</Tip>
|
||||
|
||||
With standard OAuth, your client registers dynamically with every server it connects to, receiving a fresh `client_id` each time. This works, but the server has no way to verify *who* your client actually is — any client can claim any name during registration.
|
||||
|
||||
CIMD (Client ID Metadata Documents) flips this around. You host a small JSON document at an HTTPS URL you control, and that URL becomes your `client_id`. When your client connects to a server, the server fetches your metadata document and can verify your identity through your domain ownership. Users see a verified domain badge in the consent screen instead of an unverified client name.
|
||||
|
||||
## Client Usage
|
||||
|
||||
Pass your CIMD document URL to the `client_metadata_url` parameter of `OAuth`:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import OAuth
|
||||
|
||||
async with Client(
|
||||
"https://mcp-server.example.com/mcp",
|
||||
auth=OAuth(
|
||||
client_metadata_url="https://myapp.example.com/oauth/client.json",
|
||||
),
|
||||
) as client:
|
||||
await client.ping()
|
||||
```
|
||||
|
||||
When the server supports CIMD, the client uses your metadata URL as its `client_id` instead of performing Dynamic Client Registration. The server fetches your document, validates it, and proceeds with the standard OAuth authorization flow.
|
||||
|
||||
<Note>
|
||||
You don't need to pass `mcp_url` when using `OAuth` with `Client(auth=...)` — the transport provides the server URL automatically.
|
||||
</Note>
|
||||
|
||||
## Creating a CIMD Document
|
||||
|
||||
A CIMD document is a JSON file that describes your client. The most important field is `client_id`, which must exactly match the URL where you host the document.
|
||||
|
||||
Use the FastMCP CLI to generate one:
|
||||
|
||||
```bash
|
||||
fastmcp auth cimd create \
|
||||
--name "My Application" \
|
||||
--redirect-uri "http://localhost:*/callback" \
|
||||
--client-id "https://myapp.example.com/oauth/client.json"
|
||||
```
|
||||
|
||||
This produces:
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "https://myapp.example.com/oauth/client.json",
|
||||
"client_name": "My Application",
|
||||
"redirect_uris": ["http://localhost:*/callback"],
|
||||
"token_endpoint_auth_method": "none",
|
||||
"grant_types": ["authorization_code"],
|
||||
"response_types": ["code"]
|
||||
}
|
||||
```
|
||||
|
||||
If you omit `--client-id`, the CLI generates a placeholder value and reminds you to update it before hosting.
|
||||
|
||||
### CLI Options
|
||||
|
||||
The `create` command accepts these flags:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--name` | Human-readable client name (required) |
|
||||
| `--redirect-uri`, `-r` | Allowed redirect URIs — can be specified multiple times (required) |
|
||||
| `--client-id` | The URL where you'll host this document (sets `client_id` directly) |
|
||||
| `--output`, `-o` | Write to a file instead of stdout |
|
||||
| `--scope` | Space-separated list of scopes the client may request |
|
||||
| `--client-uri` | URL of the client's home page |
|
||||
| `--logo-uri` | URL of the client's logo image |
|
||||
| `--no-pretty` | Output compact JSON |
|
||||
|
||||
### Redirect URIs
|
||||
|
||||
The `redirect_uris` field supports wildcard port matching for localhost. The pattern `http://localhost:*/callback` matches any port, which is useful for development clients that bind to random available ports (which is what FastMCP's `OAuth` helper does by default).
|
||||
|
||||
## Hosting Requirements
|
||||
|
||||
CIMD documents must be hosted at a publicly accessible HTTPS URL with a non-root path:
|
||||
|
||||
- **HTTPS required** — HTTP URLs are rejected for security
|
||||
- **Non-root path** — The URL must have a path component (e.g., `/oauth/client.json`, not just `/`)
|
||||
- **Public accessibility** — The server must be able to fetch the document over the internet
|
||||
- **Matching `client_id`** — The `client_id` field in the document must exactly match the hosting URL
|
||||
|
||||
Common hosting options include static file hosting services like GitHub Pages, Cloudflare Pages, Vercel, or S3 — anywhere you can serve a JSON file over HTTPS.
|
||||
|
||||
## Validating Your Document
|
||||
|
||||
Before deploying, verify your hosted document passes validation:
|
||||
|
||||
```bash
|
||||
fastmcp auth cimd validate https://myapp.example.com/oauth/client.json
|
||||
```
|
||||
|
||||
The validator fetches the document and checks that:
|
||||
- The URL is valid (HTTPS, non-root path)
|
||||
- The document is well-formed JSON conforming to the CIMD schema
|
||||
- The `client_id` in the document matches the URL it was fetched from
|
||||
|
||||
## How It Works
|
||||
|
||||
When your client connects to a CIMD-enabled server, the flow works like this:
|
||||
|
||||
<Steps>
|
||||
<Step title="Client Presents Metadata URL">
|
||||
Your client sends its `client_metadata_url` as the `client_id` in the OAuth authorization request.
|
||||
</Step>
|
||||
<Step title="Server Recognizes CIMD URL">
|
||||
The server sees that the `client_id` is an HTTPS URL with a path — the signature of a CIMD client — and skips Dynamic Client Registration.
|
||||
</Step>
|
||||
<Step title="Server Fetches and Validates">
|
||||
The server fetches your JSON document from the URL, validates that `client_id` matches the URL, and extracts your client metadata (name, redirect URIs, scopes).
|
||||
</Step>
|
||||
<Step title="Authorization Proceeds">
|
||||
The standard OAuth flow continues: browser opens for user consent, authorization code exchange, token issuance. The consent screen shows your verified domain.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
The server caches your CIMD document according to HTTP cache headers, so subsequent requests don't require re-fetching.
|
||||
|
||||
## Server Configuration
|
||||
|
||||
CIMD is a server-side feature that your MCP server must support. FastMCP's OAuth proxy providers (GitHub, Google, Auth0, etc.) support CIMD by default. See the [OAuth Proxy CIMD documentation](/servers/auth/oauth-proxy#cimd-support) for server-side configuration, including private key JWT authentication and security details.
|
||||
186
docs/v3/clients/auth/oauth.mdx
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
---
|
||||
title: OAuth Authentication
|
||||
sidebarTitle: OAuth
|
||||
description: Authenticate your FastMCP client via OAuth 2.1.
|
||||
icon: window
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="2.6.0" />
|
||||
|
||||
<Tip>
|
||||
OAuth authentication is only relevant for HTTP-based transports and requires user interaction via a web browser.
|
||||
</Tip>
|
||||
|
||||
When your FastMCP client needs to access an MCP server protected by OAuth 2.1, and the process requires user interaction (like logging in and granting consent), you should use the Authorization Code Flow. FastMCP provides the `fastmcp.client.auth.OAuth` helper to simplify this entire process.
|
||||
|
||||
This flow is common for user-facing applications where the application acts on behalf of the user.
|
||||
|
||||
## Client Usage
|
||||
|
||||
|
||||
### Default Configuration
|
||||
|
||||
The simplest way to use OAuth is to pass the string `"oauth"` to the `auth` parameter of the `Client` or transport instance. FastMCP will automatically configure the client to use OAuth with default settings:
|
||||
|
||||
```python {4}
|
||||
from fastmcp import Client
|
||||
|
||||
# Uses default OAuth settings
|
||||
async with Client("https://your-server.fastmcp.app/mcp", auth="oauth") as client:
|
||||
await client.ping()
|
||||
```
|
||||
|
||||
|
||||
### `OAuth` Helper
|
||||
|
||||
To fully configure the OAuth flow, use the `OAuth` helper and pass it to the `auth` parameter of the `Client` or transport instance. `OAuth` manages the complexities of the OAuth 2.1 Authorization Code Grant with PKCE (Proof Key for Code Exchange) for enhanced security, and implements the full `httpx.Auth` interface.
|
||||
|
||||
```python {2, 4, 6}
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import OAuth
|
||||
|
||||
oauth = OAuth(scopes=["user"])
|
||||
|
||||
async with Client("https://your-server.fastmcp.app/mcp", auth=oauth) as client:
|
||||
await client.ping()
|
||||
```
|
||||
|
||||
<Note>
|
||||
You don't need to pass `mcp_url` when using `OAuth` with `Client(auth=...)` — the transport provides the server URL automatically.
|
||||
</Note>
|
||||
|
||||
#### `OAuth` Parameters
|
||||
|
||||
- **`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
|
||||
- **`callback_port`** (`int`, optional): Fixed port for OAuth callback server. If not specified, uses a random available port
|
||||
- **`httpx_client_factory`** (`McpHttpClientFactory`, optional): Factory for creating httpx clients
|
||||
|
||||
|
||||
## OAuth Flow
|
||||
|
||||
The OAuth flow is triggered when you use a FastMCP `Client` configured to use OAuth.
|
||||
|
||||
<Steps>
|
||||
<Step title="Token Check">
|
||||
The client first checks the configured `token_storage` backend for existing, valid tokens for the target server. If one is found, it will be used to authenticate the client.
|
||||
</Step>
|
||||
<Step title="OAuth Server Discovery">
|
||||
If no valid tokens exist, the client attempts to discover the OAuth server's endpoints using a well-known URI (e.g., `/.well-known/oauth-authorization-server`) based on the `mcp_url`.
|
||||
</Step>
|
||||
<Step title="Client Registration">
|
||||
If a `client_id` is provided, the client uses those pre-registered credentials directly and skips this step entirely. Otherwise, if a `client_metadata_url` is configured and the server supports CIMD, the client uses its metadata URL as its identity. As a fallback, the client performs Dynamic Client Registration (RFC 7591) if the server supports it.
|
||||
</Step>
|
||||
<Step title="Local Callback Server">
|
||||
A temporary local HTTP server is started on an available port (or the port specified via `callback_port`). This server's address (e.g., `http://127.0.0.1:<port>/callback`) acts as the `redirect_uri` for the OAuth flow.
|
||||
</Step>
|
||||
<Step title="Browser Interaction">
|
||||
The user's default web browser is automatically opened, directing them to the OAuth server's authorization endpoint. The user logs in and grants (or denies) the requested `scopes`.
|
||||
</Step>
|
||||
<Step title="Authorization Code & Token Exchange">
|
||||
Upon approval, the OAuth server redirects the user's browser to the local callback server with an `authorization_code`. The client captures this code and exchanges it with the OAuth server's token endpoint for an `access_token` (and often a `refresh_token`) using PKCE for security.
|
||||
</Step>
|
||||
<Step title="Token Caching">
|
||||
The obtained tokens are saved to the configured `token_storage` backend for future use, eliminating the need for repeated browser interactions.
|
||||
</Step>
|
||||
<Step title="Authenticated Requests">
|
||||
The access token is automatically included in the `Authorization` header for requests to the MCP server.
|
||||
</Step>
|
||||
<Step title="Refresh Token">
|
||||
If the access token expires, the client will automatically use the refresh token to get a new access token.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Token Storage
|
||||
|
||||
<VersionBadge version="2.13.0" />
|
||||
|
||||
By default, tokens are stored in memory and lost when your application restarts. For persistent storage, pass an `AsyncKeyValue`-compatible storage backend to the `token_storage` parameter.
|
||||
|
||||
<Warning>
|
||||
**Security Consideration**: Use encrypted storage for production. MCP clients can accumulate OAuth credentials for many servers over time, and a compromised token store could expose access to multiple services.
|
||||
</Warning>
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import OAuth
|
||||
from key_value.aio.stores.disk import DiskStore
|
||||
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
|
||||
from cryptography.fernet import Fernet
|
||||
import os
|
||||
|
||||
# Create encrypted disk storage
|
||||
encrypted_storage = FernetEncryptionWrapper(
|
||||
key_value=DiskStore(directory="~/.fastmcp/oauth-tokens"),
|
||||
fernet=Fernet(os.environ["OAUTH_STORAGE_ENCRYPTION_KEY"])
|
||||
)
|
||||
|
||||
oauth = OAuth(token_storage=encrypted_storage)
|
||||
|
||||
async with Client("https://your-server.fastmcp.app/mcp", auth=oauth) as client:
|
||||
await client.ping()
|
||||
```
|
||||
|
||||
You can use any `AsyncKeyValue`-compatible backend from the [key-value library](https://github.com/strawgate/py-key-value) including Redis, DynamoDB, and more. Wrap your storage in `FernetEncryptionWrapper` for encryption.
|
||||
|
||||
<Note>
|
||||
When selecting a storage backend, review the [py-key-value documentation](https://github.com/strawgate/py-key-value) to understand the maturity level and limitations of your chosen backend. Some backends may be in preview or have constraints that affect production suitability.
|
||||
</Note>
|
||||
|
||||
## CIMD Authentication
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
Client ID Metadata Documents (CIMD) provide an alternative to Dynamic Client Registration. Instead of registering with each server, your client hosts a static JSON document at an HTTPS URL. That URL becomes your client's identity, and servers can verify who you are through your domain ownership.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import OAuth
|
||||
|
||||
async with Client(
|
||||
"https://mcp-server.example.com/mcp",
|
||||
auth=OAuth(
|
||||
client_metadata_url="https://myapp.example.com/oauth/client.json",
|
||||
),
|
||||
) as client:
|
||||
await client.ping()
|
||||
```
|
||||
|
||||
See the [CIMD Authentication](/clients/auth/cimd) page for complete documentation on creating, hosting, and validating CIMD documents.
|
||||
|
||||
## Pre-Registered Clients
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
Some OAuth servers don't support Dynamic Client Registration — the MCP spec explicitly makes DCR optional. If your client has been pre-registered with the server (you already have a `client_id` and optionally a `client_secret`), you can provide them directly to skip DCR entirely.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import OAuth
|
||||
|
||||
async with Client(
|
||||
"https://mcp-server.example.com/mcp",
|
||||
auth=OAuth(
|
||||
client_id="my-registered-client-id",
|
||||
client_secret="my-client-secret",
|
||||
),
|
||||
) as client:
|
||||
await client.ping()
|
||||
```
|
||||
|
||||
Public clients that rely on PKCE for security can omit `client_secret`:
|
||||
|
||||
```python
|
||||
oauth = OAuth(client_id="my-public-client-id")
|
||||
```
|
||||
|
||||
<Note>
|
||||
When using pre-registered credentials, the client will not attempt Dynamic Client Registration. If the server rejects the credentials, the error is surfaced immediately rather than falling back to DCR.
|
||||
</Note>
|
||||
161
docs/v3/clients/cli.mdx
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
---
|
||||
title: Client CLI
|
||||
sidebarTitle: CLI
|
||||
description: Query and invoke MCP server tools directly from the terminal with fastmcp list and fastmcp call.
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
MCP servers are designed for programmatic consumption by AI assistants and applications. But during development, you often want to poke at a server directly: check what tools it exposes, call one with test arguments, or verify that a deployment is responding correctly. The FastMCP CLI gives you that direct access with two commands, `fastmcp list` and `fastmcp call`, so you can query and invoke any MCP server without writing a single line of Python.
|
||||
|
||||
These commands are also valuable for LLM-based agents that lack native MCP support. An agent that can execute shell commands can use `fastmcp list --json` to discover available tools and `fastmcp call --json` to invoke them, with structured JSON output designed for programmatic consumption.
|
||||
|
||||
## Server Targets
|
||||
|
||||
Both commands need to know which server to talk to. You provide a "server spec" as the first argument, and FastMCP figures out the transport automatically. You can point at an HTTP URL for a running server, a Python file that defines one, a JSON configuration file that describes one, or a JavaScript file. The CLI resolves the right connection mechanism so you can focus on the query.
|
||||
|
||||
```bash
|
||||
fastmcp list http://localhost:8000/mcp
|
||||
fastmcp list server.py
|
||||
fastmcp list mcp-config.json
|
||||
```
|
||||
|
||||
Python files are handled with particular care. Rather than requiring your script to call `mcp.run()` at the bottom, the CLI routes it through `fastmcp run` internally, which means any Python file that defines a FastMCP server object works as a target with no boilerplate.
|
||||
|
||||
For servers that communicate over stdio (common with Node.js-based MCP servers), use the `--command` flag instead of a positional server spec. The string is shell-split into a command and arguments.
|
||||
|
||||
```bash
|
||||
fastmcp list --command 'npx -y @modelcontextprotocol/server-github'
|
||||
```
|
||||
|
||||
### Name-Based Resolution
|
||||
|
||||
If your MCP servers are already configured in an editor or tool, you can refer to them by name instead of spelling out URLs or file paths. The CLI scans config files from Claude Desktop, Claude Code, Cursor, Gemini CLI, and Goose, and matches the name you provide.
|
||||
|
||||
```bash
|
||||
fastmcp list weather
|
||||
fastmcp call weather get_forecast city=London
|
||||
```
|
||||
|
||||
You can also use the `source:name` form to target a specific source directly, which is useful when the same server name appears in multiple configs or when you want to be explicit about which config you mean.
|
||||
|
||||
```bash
|
||||
fastmcp list claude-code:my-server
|
||||
fastmcp call cursor:weather get_forecast city=London
|
||||
```
|
||||
|
||||
The available source names are `claude-desktop`, `claude-code`, `cursor`, `gemini`, `goose`, and `project` (for `./mcp.json`). Run `fastmcp discover` to see what's available.
|
||||
|
||||
## Discovering Configured Servers
|
||||
|
||||
`fastmcp discover` scans your local editor and project configurations for MCP server definitions. It checks Claude Desktop, Claude Code (`~/.claude.json`), Cursor workspace configs (walking up from the current directory), Gemini CLI (`~/.gemini/settings.json`), Goose (`~/.config/goose/config.yaml`), and `mcp.json` in the current directory.
|
||||
|
||||
```bash
|
||||
fastmcp discover
|
||||
```
|
||||
|
||||
The output groups servers by source, showing each server's name and transport. Use `--source` to filter to specific sources, and `--json` for machine-readable output.
|
||||
|
||||
```bash
|
||||
fastmcp discover --source claude-code
|
||||
fastmcp discover --source cursor --source gemini --json
|
||||
```
|
||||
|
||||
Any server that appears here can be used by name (or `source:name`) with `fastmcp list` and `fastmcp call`, which means you can go from "I have a server configured in Claude Code" to querying it without copying any URLs or paths.
|
||||
|
||||
## Discovering Tools
|
||||
|
||||
`fastmcp list` connects to a server and prints every tool it exposes. The default output is compact: each tool appears as a function signature with its parameter names, types, and a description.
|
||||
|
||||
```bash
|
||||
fastmcp list http://localhost:8000/mcp
|
||||
```
|
||||
|
||||
The output looks like a Python function signature, making it easy to see at a glance what a tool expects and what it returns. Required parameters appear with just their type annotation, while optional ones show their defaults.
|
||||
|
||||
When you need the full JSON Schema for a tool's inputs or outputs -- useful for understanding nested object structures or enum constraints -- opt into them with `--input-schema` or `--output-schema`. These print the raw schema beneath each tool signature.
|
||||
|
||||
### Beyond Tools
|
||||
|
||||
MCP servers can expose resources and prompts alongside tools. By default, `fastmcp list` only shows tools because they are the most common interaction point. Add `--resources` or `--prompts` to include those in the output.
|
||||
|
||||
```bash
|
||||
fastmcp list server.py --resources --prompts
|
||||
```
|
||||
|
||||
Resources appear with their URIs and descriptions. Prompts appear with their argument names so you can see what parameters they accept.
|
||||
|
||||
### Machine-Readable Output
|
||||
|
||||
The `--json` flag switches from human-friendly text to structured JSON. Each tool includes its name, description, and full input schema (and output schema when present). When combined with `--resources` or `--prompts`, those are included as additional top-level keys.
|
||||
|
||||
```bash
|
||||
fastmcp list server.py --json
|
||||
```
|
||||
|
||||
This is the format to use when building automation around MCP servers or feeding tool definitions to an LLM agent that needs to decide which tool to call.
|
||||
|
||||
## Calling Tools
|
||||
|
||||
`fastmcp call` invokes a single tool on a server. You provide the server spec, the tool name, and arguments as `key=value` pairs. The CLI fetches the tool's schema, coerces your string values to the correct types (integers, floats, booleans, arrays, objects), and makes the call.
|
||||
|
||||
```bash
|
||||
fastmcp call http://localhost:8000/mcp search query=hello limit=5
|
||||
```
|
||||
|
||||
Type coercion is driven by the tool's JSON Schema. If a parameter is declared as an integer, the string `"5"` becomes the integer `5`. Booleans accept `true`/`false`, `yes`/`no`, and `1`/`0`. Array and object parameters are parsed as JSON.
|
||||
|
||||
For tools with complex or deeply nested arguments, the `key=value` syntax gets unwieldy. You can pass a single JSON object as the argument instead, and the CLI treats it as the full input dictionary.
|
||||
|
||||
```bash
|
||||
fastmcp call server.py create_item '{"name": "Widget", "tags": ["sale", "new"], "metadata": {"color": "blue"}}'
|
||||
```
|
||||
|
||||
Alternatively, `--input-json` provides the base argument dictionary. Any `key=value` pairs you add alongside it override keys from the JSON, which is useful for templating a complex call and varying one parameter at a time.
|
||||
|
||||
### Error Handling
|
||||
|
||||
The CLI validates your call before sending it. If you misspell a tool name, it uses fuzzy matching to suggest corrections. If you omit a required argument, it tells you which ones are missing and prints the tool's signature as a reminder.
|
||||
|
||||
When a tool call itself returns an error (the server executed the tool but it failed), the error message is printed and the CLI exits with a non-zero status code, making it straightforward to use in scripts.
|
||||
|
||||
### Structured Output
|
||||
|
||||
Like `fastmcp list`, the `--json` flag on `fastmcp call` emits structured JSON instead of formatted text. The output includes the content blocks, error status, and structured content when the server provides it. Use this when you need to parse tool results programmatically.
|
||||
|
||||
```bash
|
||||
fastmcp call server.py get_weather city=London --json
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
When the server target is an HTTP URL, the CLI automatically enables OAuth authentication. If the server requires it, you will be guided through the OAuth flow (typically opening a browser for authorization). If the server has no auth requirements, the OAuth setup is a silent no-op.
|
||||
|
||||
To explicitly disable authentication -- for example, when connecting to a local development server where OAuth setup would just slow you down -- pass `--auth none`.
|
||||
|
||||
```bash
|
||||
fastmcp call http://localhost:8000/mcp my_tool --auth none
|
||||
```
|
||||
|
||||
## Transport Override
|
||||
|
||||
FastMCP defaults to Streamable HTTP for URL targets. If you are connecting to a server that only supports Server-Sent Events (SSE), use `--transport sse` to force the older transport. This appends `/sse` to the URL path automatically so the client picks the correct protocol.
|
||||
|
||||
```bash
|
||||
fastmcp list http://localhost:8000 --transport sse
|
||||
```
|
||||
|
||||
## Interactive Elicitation
|
||||
|
||||
Some MCP tools request additional input from the user during execution through a mechanism called elicitation. When a tool sends an elicitation request, the CLI prints the server's question to the terminal and prompts you to respond. Each field in the elicitation schema is presented with its name and expected type, and required fields are clearly marked.
|
||||
|
||||
You can type `decline` to skip a question or `cancel` to abort the tool call entirely. This interactive behavior means the CLI works naturally with tools that have multi-step or conversational workflows.
|
||||
|
||||
## LLM Agent Integration
|
||||
|
||||
For LLM agents that can execute shell commands but lack built-in MCP support, the CLI provides a clean integration path. The agent calls `fastmcp list --json` to get a structured description of every available tool, including full input schemas, and then calls `fastmcp call --json` with the chosen tool and arguments. Both commands return well-formed JSON that is straightforward to parse.
|
||||
|
||||
Because the CLI handles connection management, transport selection, and type coercion internally, the agent does not need to understand MCP protocol details. It just needs to read JSON and construct shell commands.
|
||||
89
docs/v3/clients/client-only-package.mdx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
---
|
||||
title: Client-Only Package
|
||||
description: Use FastMCP's client without installing the full server framework.
|
||||
icon: box
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.3.0" />
|
||||
|
||||
FastMCP's full `fastmcp` package includes everything needed to build and run MCP servers, apps, proxies, and clients. If you are only embedding an MCP client in another framework, building your own LLM host, or testing MCP servers, you can install the smaller client-only package instead.
|
||||
|
||||
```bash
|
||||
pip install "fastmcp-slim[client]"
|
||||
```
|
||||
|
||||
The client-only package uses the `fastmcp` import namespace:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
client = Client("https://example.com/mcp")
|
||||
```
|
||||
|
||||
Use `fastmcp-slim[client]` when your code connects to MCP servers but does not define or run FastMCP servers itself. For example, framework authors can depend on `fastmcp-slim[client]` to provide MCP connectivity without requiring users to install the full FastMCP server stack.
|
||||
|
||||
## Supported Usage
|
||||
|
||||
Client-only installs support remote and subprocess transports:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
# Remote MCP server
|
||||
http_client = Client("https://example.com/mcp")
|
||||
|
||||
# Local MCP server over stdio
|
||||
stdio_client = Client("my_server.py")
|
||||
```
|
||||
|
||||
Single-server MCP configuration works as well:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
config = {
|
||||
"mcpServers": {
|
||||
"weather": {
|
||||
"url": "https://weather.example.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client = Client(config)
|
||||
```
|
||||
|
||||
Optional sampling handlers are available through the same extras as the full package:
|
||||
|
||||
```bash
|
||||
pip install "fastmcp-slim[client,openai]"
|
||||
pip install "fastmcp-slim[client,anthropic]"
|
||||
pip install "fastmcp-slim[client,gemini]"
|
||||
```
|
||||
|
||||
## When to Use the Full Package
|
||||
|
||||
Install `fastmcp` when you need server-side FastMCP features:
|
||||
|
||||
```bash
|
||||
pip install fastmcp
|
||||
```
|
||||
|
||||
The full package remains the default for most users and continues to support the existing import style:
|
||||
|
||||
```python
|
||||
from fastmcp import Client, FastMCP
|
||||
|
||||
server = FastMCP("Example")
|
||||
client = Client(server)
|
||||
```
|
||||
|
||||
Use the full package for:
|
||||
|
||||
- defining or running FastMCP servers
|
||||
- in-memory clients connected directly to `FastMCP` server objects
|
||||
- multi-server MCP configurations
|
||||
- FastMCP apps, proxies, server auth, middleware, and other server-side features
|
||||
|
||||
The `fastmcp-slim` package is intentionally narrower: it is for client-only consumers who want FastMCP's MCP client behavior without depending on the full framework.
|
||||
237
docs/v3/clients/client.mdx
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
---
|
||||
title: The FastMCP Client
|
||||
sidebarTitle: Overview
|
||||
description: Programmatic client for interacting with MCP servers through a well-typed, Pythonic interface.
|
||||
icon: user-robot
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="2.0.0" />
|
||||
|
||||
The `fastmcp.Client` class provides a programmatic interface for interacting with any MCP server. It handles protocol details and connection management automatically, letting you focus on the operations you want to perform.
|
||||
|
||||
The FastMCP Client is designed for deterministic, controlled interactions rather than autonomous behavior, making it ideal for testing MCP servers during development, building deterministic applications that need reliable MCP interactions, and creating the foundation for agentic or LLM-based clients with structured, type-safe operations.
|
||||
|
||||
<Note>
|
||||
This is a programmatic client that requires explicit function calls and provides direct control over all MCP operations. Use it as a building block for higher-level systems.
|
||||
</Note>
|
||||
|
||||
## Creating a Client
|
||||
|
||||
You provide a server source and the client automatically infers the appropriate transport mechanism.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from fastmcp import Client, FastMCP
|
||||
|
||||
# In-memory server (ideal for testing)
|
||||
server = FastMCP("TestServer")
|
||||
client = Client(server)
|
||||
|
||||
# HTTP server
|
||||
client = Client("https://example.com/mcp")
|
||||
|
||||
# Local Python script
|
||||
client = Client("my_mcp_server.py")
|
||||
|
||||
async def main():
|
||||
async with client:
|
||||
# Basic server interaction
|
||||
await client.ping()
|
||||
|
||||
# List available operations
|
||||
tools = await client.list_tools()
|
||||
resources = await client.list_resources()
|
||||
prompts = await client.list_prompts()
|
||||
|
||||
# Execute operations
|
||||
result = await client.call_tool("example_tool", {"param": "value"})
|
||||
print(result)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
All client operations require using the `async with` context manager for proper connection lifecycle management.
|
||||
|
||||
## Choosing a Transport
|
||||
|
||||
The client automatically selects a transport based on what you pass to it, but different transports have different characteristics that matter for your use case.
|
||||
|
||||
**In-memory transport** connects directly to a FastMCP server instance within the same Python process. Use this for testing and development where you want to eliminate subprocess and network complexity. The server shares your process's environment and memory space.
|
||||
|
||||
```python
|
||||
from fastmcp import Client, FastMCP
|
||||
|
||||
server = FastMCP("TestServer")
|
||||
client = Client(server) # In-memory, no network or subprocess
|
||||
```
|
||||
|
||||
**STDIO transport** launches a server as a subprocess and communicates through stdin/stdout pipes. This is the standard mechanism used by desktop clients like Claude Desktop. The subprocess runs in an isolated environment, so you must explicitly pass any environment variables the server needs.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
# Simple inference from file path
|
||||
client = Client("my_server.py")
|
||||
|
||||
# With explicit environment configuration
|
||||
client = Client("my_server.py", env={"API_KEY": "secret"})
|
||||
```
|
||||
|
||||
**HTTP transport** connects to servers running as web services. Use this for production deployments where the server runs independently and manages its own lifecycle.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
client = Client("https://api.example.com/mcp")
|
||||
```
|
||||
|
||||
See [Transports](/clients/transports) for detailed configuration options including authentication headers, session persistence, and multi-server configurations.
|
||||
|
||||
## Configuration-Based Clients
|
||||
|
||||
<VersionBadge version="2.4.0" />
|
||||
|
||||
Create clients from MCP configuration dictionaries, which can include multiple servers. While there is no official standard for MCP configuration format, FastMCP follows established conventions used by tools like Claude Desktop.
|
||||
|
||||
```python
|
||||
config = {
|
||||
"mcpServers": {
|
||||
"weather": {
|
||||
"url": "https://weather-api.example.com/mcp"
|
||||
},
|
||||
"assistant": {
|
||||
"command": "python",
|
||||
"args": ["./assistant_server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client = Client(config)
|
||||
|
||||
async with client:
|
||||
# Tools are prefixed with server names
|
||||
weather_data = await client.call_tool("weather_get_forecast", {"city": "London"})
|
||||
response = await client.call_tool("assistant_answer_question", {"question": "What's the capital of France?"})
|
||||
|
||||
# Resources use prefixed URIs
|
||||
icons = await client.read_resource("weather://weather/icons/sunny")
|
||||
```
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
The client uses context managers for connection management. When you enter the context, the client establishes a connection and performs an MCP initialization handshake with the server. This handshake exchanges capabilities, server metadata, and instructions.
|
||||
|
||||
```python
|
||||
from fastmcp import Client, FastMCP
|
||||
|
||||
mcp = FastMCP(name="MyServer", instructions="Use the greet tool to say hello!")
|
||||
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet a user by name."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
# Initialization already happened automatically
|
||||
print(f"Server: {client.initialize_result.serverInfo.name}")
|
||||
print(f"Instructions: {client.initialize_result.instructions}")
|
||||
print(f"Capabilities: {client.initialize_result.capabilities.tools}")
|
||||
```
|
||||
|
||||
For advanced scenarios where you need precise control over when initialization happens, disable automatic initialization and call `initialize()` manually:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
client = Client("my_mcp_server.py", auto_initialize=False)
|
||||
|
||||
async with client:
|
||||
# Connection established, but not initialized yet
|
||||
print(f"Connected: {client.is_connected()}")
|
||||
print(f"Initialized: {client.initialize_result is not None}") # False
|
||||
|
||||
# Initialize manually with custom timeout
|
||||
result = await client.initialize(timeout=10.0)
|
||||
print(f"Server: {result.serverInfo.name}")
|
||||
|
||||
# Now ready for operations
|
||||
tools = await client.list_tools()
|
||||
```
|
||||
|
||||
## Operations
|
||||
|
||||
FastMCP clients interact with three types of server components.
|
||||
|
||||
**Tools** are server-side functions that the client can execute with arguments. Call them with `call_tool()` and receive structured results.
|
||||
|
||||
```python
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
result = await client.call_tool("multiply", {"a": 5, "b": 3})
|
||||
print(result.data) # 15
|
||||
```
|
||||
|
||||
See [Tools](/clients/tools) for detailed documentation including version selection, error handling, and structured output.
|
||||
|
||||
**Resources** are data sources that the client can read, either static or templated. Access them with `read_resource()` using URIs.
|
||||
|
||||
```python
|
||||
async with client:
|
||||
resources = await client.list_resources()
|
||||
content = await client.read_resource("file:///config/settings.json")
|
||||
print(content[0].text)
|
||||
```
|
||||
|
||||
See [Resources](/clients/resources) for detailed documentation including templates and binary content.
|
||||
|
||||
**Prompts** are reusable message templates that can accept arguments. Retrieve rendered prompts with `get_prompt()`.
|
||||
|
||||
```python
|
||||
async with client:
|
||||
prompts = await client.list_prompts()
|
||||
messages = await client.get_prompt("analyze_data", {"data": [1, 2, 3]})
|
||||
print(messages.messages)
|
||||
```
|
||||
|
||||
See [Prompts](/clients/prompts) for detailed documentation including argument serialization.
|
||||
|
||||
## Callback Handlers
|
||||
|
||||
The client supports callback handlers for advanced server interactions. These let you respond to server-initiated requests and receive notifications.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.logging import LogMessage
|
||||
|
||||
async def log_handler(message: LogMessage):
|
||||
print(f"Server log: {message.data}")
|
||||
|
||||
async def progress_handler(progress: float, total: float | None, message: str | None):
|
||||
print(f"Progress: {progress}/{total} - {message}")
|
||||
|
||||
async def sampling_handler(messages, params, context):
|
||||
# Integrate with your LLM service here
|
||||
return "Generated response"
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
log_handler=log_handler,
|
||||
progress_handler=progress_handler,
|
||||
sampling_handler=sampling_handler,
|
||||
timeout=30.0
|
||||
)
|
||||
```
|
||||
|
||||
Each handler type has its own documentation:
|
||||
|
||||
- **[Sampling](/clients/sampling)** - Respond to server LLM requests
|
||||
- **[Elicitation](/clients/elicitation)** - Handle server requests for user input
|
||||
- **[Progress](/clients/progress)** - Monitor long-running operations
|
||||
- **[Logging](/clients/logging)** - Handle server log messages
|
||||
- **[Roots](/clients/roots)** - Provide local context to servers
|
||||
|
||||
<Tip>
|
||||
The FastMCP Client is designed as a foundational tool. Use it directly for deterministic operations, or build higher-level agentic systems on top of its reliable, type-safe interface.
|
||||
</Tip>
|
||||
138
docs/v3/clients/elicitation.mdx
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
---
|
||||
title: User Elicitation
|
||||
sidebarTitle: Elicitation
|
||||
description: Handle server requests for structured user input.
|
||||
icon: message-question
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx";
|
||||
|
||||
<VersionBadge version="2.10.0" />
|
||||
|
||||
Use this when you need to respond to server requests for user input during tool execution.
|
||||
|
||||
Elicitation allows MCP servers to request structured input from users during operations. Instead of requiring all inputs upfront, servers can interactively ask for missing parameters, request clarification, or gather additional context.
|
||||
|
||||
## Handler Template
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.elicitation import ElicitResult, ElicitRequestParams, RequestContext
|
||||
|
||||
async def elicitation_handler(
|
||||
message: str,
|
||||
response_type: type | None,
|
||||
params: ElicitRequestParams,
|
||||
context: RequestContext
|
||||
) -> ElicitResult | object:
|
||||
"""
|
||||
Handle server requests for user input.
|
||||
|
||||
Args:
|
||||
message: The prompt to display to the user
|
||||
response_type: Python dataclass type for the response (None if no data expected)
|
||||
params: Original MCP elicitation parameters including raw JSON schema
|
||||
context: Request context with metadata
|
||||
|
||||
Returns:
|
||||
- Data directly (implicitly accepts the elicitation)
|
||||
- ElicitResult for explicit control over the action
|
||||
"""
|
||||
# Present the message and collect input
|
||||
user_input = input(f"{message}: ")
|
||||
|
||||
if not user_input:
|
||||
return ElicitResult(action="decline")
|
||||
|
||||
# Create response using the provided dataclass type
|
||||
return response_type(value=user_input)
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
elicitation_handler=elicitation_handler,
|
||||
)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
When a server needs user input, it sends an elicitation request with a message prompt and a JSON schema describing the expected response structure. FastMCP automatically converts this schema into a Python dataclass type, making it easy to construct properly typed responses without manually parsing JSON schemas.
|
||||
|
||||
The handler receives four parameters:
|
||||
|
||||
<Card icon="code" title="Handler Parameters">
|
||||
<ResponseField name="message" type="str">
|
||||
The prompt message to display to the user
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="response_type" type="type | None">
|
||||
A Python dataclass type that FastMCP created from the server's JSON schema. Use this to construct your response with proper typing. If the server requests an empty object, this will be `None`.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="params" type="ElicitRequestParams">
|
||||
The original MCP elicitation parameters, including the raw JSON schema in `params.requestedSchema`
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="context" type="RequestContext">
|
||||
Request context containing metadata about the elicitation request
|
||||
</ResponseField>
|
||||
</Card>
|
||||
|
||||
## Response Actions
|
||||
|
||||
You can return data directly, which implicitly accepts the elicitation:
|
||||
|
||||
```python
|
||||
async def elicitation_handler(message, response_type, params, context):
|
||||
user_input = input(f"{message}: ")
|
||||
return response_type(value=user_input) # Implicit accept
|
||||
```
|
||||
|
||||
Or return an `ElicitResult` for explicit control over the action:
|
||||
|
||||
```python
|
||||
from fastmcp.client.elicitation import ElicitResult
|
||||
|
||||
async def elicitation_handler(message, response_type, params, context):
|
||||
user_input = input(f"{message}: ")
|
||||
|
||||
if not user_input:
|
||||
return ElicitResult(action="decline") # User declined
|
||||
|
||||
if user_input == "cancel":
|
||||
return ElicitResult(action="cancel") # Cancel entire operation
|
||||
|
||||
return ElicitResult(
|
||||
action="accept",
|
||||
content=response_type(value=user_input)
|
||||
)
|
||||
```
|
||||
|
||||
**Action types:**
|
||||
- **`accept`**: User provided valid input. Include the data in the `content` field.
|
||||
- **`decline`**: User chose not to provide the requested information. Omit `content`.
|
||||
- **`cancel`**: User cancelled the entire operation. Omit `content`.
|
||||
|
||||
## Example
|
||||
|
||||
A file management tool might ask which directory to create:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.elicitation import ElicitResult
|
||||
|
||||
async def elicitation_handler(message, response_type, params, context):
|
||||
print(f"Server asks: {message}")
|
||||
|
||||
user_response = input("Your response: ")
|
||||
|
||||
if not user_response:
|
||||
return ElicitResult(action="decline")
|
||||
|
||||
# Use the response_type dataclass to create a properly structured response
|
||||
return response_type(value=user_response)
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
elicitation_handler=elicitation_handler
|
||||
)
|
||||
```
|
||||
169
docs/v3/clients/fastmcp-remote.mdx
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
---
|
||||
title: fastmcp-remote
|
||||
description: Bridge remote MCP servers into stdio-only MCP hosts with uvx fastmcp-remote.
|
||||
icon: bridge
|
||||
---
|
||||
|
||||
`fastmcp-remote` is FastMCP's standalone stdio bridge for remote MCP servers. Use it when an MCP host expects to launch a local command, but the server you want to use is hosted over Streamable HTTP or SSE.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"linear": {
|
||||
"command": "uvx",
|
||||
"args": ["fastmcp-remote", "https://mcp.linear.app/mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The package is powered by FastMCP. It builds one FastMCP client for the remote URL, exposes that client as a local stdio proxy, and keeps the executable focused on that bridge. For running Python server files, local project environments, FastMCP config files, and development reload loops, use [`fastmcp run`](/cli/running).
|
||||
|
||||
The command shape follows the original [`mcp-remote`](https://github.com/geelen/mcp-remote) npm project, which established this stdio-to-remote bridge pattern for MCP hosts.
|
||||
|
||||
## Installation
|
||||
|
||||
Most MCP hosts can run `fastmcp-remote` directly through `uvx`, so you usually do not need to install it yourself:
|
||||
|
||||
```bash
|
||||
uvx fastmcp-remote https://example.com/mcp
|
||||
```
|
||||
|
||||
If your host requires an already-installed command, install the package with your Python package manager:
|
||||
|
||||
```bash
|
||||
uv tool install fastmcp-remote
|
||||
```
|
||||
|
||||
## Host Configuration
|
||||
|
||||
For hosts that use `mcpServers` JSON configuration, set the command to `uvx` and pass `fastmcp-remote` plus the remote server URL as arguments:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"remote-api": {
|
||||
"command": "uvx",
|
||||
"args": ["fastmcp-remote", "https://example.com/mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Endpoint URLs and Connection Status
|
||||
|
||||
Pass the full MCP endpoint URL for the remote server. Many FastMCP HTTP servers expose MCP at `/mcp`, so a local development server may need `http://localhost:8000/mcp` rather than `http://localhost:8000`.
|
||||
|
||||
`fastmcp-remote` starts a local stdio bridge, then connects to the upstream server when the MCP host initializes that bridge. If the upstream server is unavailable, the URL does not point to an MCP endpoint, or authentication cannot complete, initialization fails and the host should report the remote server as failed. After initialization succeeds, later tool, resource, prompt, and ping requests continue to proxy through the same remote server configuration.
|
||||
|
||||
OAuth is enabled automatically for HTTPS servers. The first connection opens the browser-based OAuth flow when the server requires authentication, then stores tokens locally for future runs.
|
||||
|
||||
To pass a bearer token or another custom header directly, provide `--header` in `Name: Value` form. The header name ends at the first colon, so values can contain additional colons. Quote the header when the value contains spaces, just like any other shell argument. An `Authorization` header disables OAuth by default:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"private-api": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"fastmcp-remote",
|
||||
"https://example.com/mcp",
|
||||
"--header",
|
||||
"Authorization: Bearer <token>"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Repeat `--header` to send multiple headers:
|
||||
|
||||
```bash
|
||||
uvx fastmcp-remote https://example.com/mcp \
|
||||
--header "Authorization: Bearer <token>" \
|
||||
--header "X-Workspace: production" \
|
||||
--header "X-Client-Name: My MCP Host" \
|
||||
--header "X-Callback-Url: https://example.com/oauth/callback"
|
||||
```
|
||||
|
||||
Some MCP hosts on Windows have trouble preserving spaces inside command arguments. Put the spaced value in an environment variable and reference it from the header value:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"remote-api": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"fastmcp-remote",
|
||||
"https://example.com/mcp",
|
||||
"--header",
|
||||
"Authorization:${AUTH_HEADER}"
|
||||
],
|
||||
"env": {
|
||||
"AUTH_HEADER": "Bearer <token>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For local development servers over plain HTTP, disable OAuth when the server is unauthenticated:
|
||||
|
||||
```bash
|
||||
uvx fastmcp-remote http://localhost:8000/mcp --auth none
|
||||
```
|
||||
|
||||
## Self-Signed Certificates
|
||||
|
||||
For servers behind a self-signed certificate, point `--verify` at a CA bundle that trusts the certificate:
|
||||
|
||||
```bash
|
||||
uvx fastmcp-remote https://internal.example.com/mcp --verify /path/to/ca-bundle.pem
|
||||
```
|
||||
|
||||
To disable certificate verification entirely, pass `--verify false`. This is insecure and should only be used for trusted servers on private networks:
|
||||
|
||||
```bash
|
||||
uvx fastmcp-remote https://internal.example.com/mcp --verify false
|
||||
```
|
||||
|
||||
To trust a CA bundle without a flag, set the standard `SSL_CERT_FILE` environment variable, which OpenSSL reads automatically:
|
||||
|
||||
```bash
|
||||
SSL_CERT_FILE=/path/to/ca-bundle.pem uvx fastmcp-remote https://internal.example.com/mcp
|
||||
```
|
||||
|
||||
## OAuth Storage
|
||||
|
||||
OAuth tokens are stored under `~/.fastmcp/remote` by default. Set `FASTMCP_REMOTE_CONFIG_DIR` to use another directory:
|
||||
|
||||
```bash
|
||||
FASTMCP_REMOTE_CONFIG_DIR=~/.config/fastmcp-remote uvx fastmcp-remote https://example.com/mcp
|
||||
```
|
||||
|
||||
Use `--resource` to isolate tokens for a particular remote server identity:
|
||||
|
||||
```bash
|
||||
uvx fastmcp-remote https://example.com/mcp --resource example-prod
|
||||
```
|
||||
|
||||
If the remote authorization server requires a fixed callback port or hostname, pass them after the URL:
|
||||
|
||||
```bash
|
||||
uvx fastmcp-remote https://example.com/mcp 3334 --host 127.0.0.1
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
| ------ | ----------- |
|
||||
| `--transport` | Choose `http` or `sse`. Defaults to `http`. |
|
||||
| `--header` | Add a header to upstream requests, for example `--header "Authorization: Bearer <token>"`. Values may contain colons. Quote headers whose values contain spaces. Use `${VAR}` to expand environment variables inside values. Repeat for multiple headers. |
|
||||
| `--auth` | Choose `oauth` or `none`. The default uses OAuth unless an `Authorization` header is provided. |
|
||||
| `--verify` | Control TLS certificate verification. Pass a path to a CA bundle to trust a self-signed certificate, or `false` to disable verification (insecure). Defaults to verification enabled. |
|
||||
| `--resource` | Isolate OAuth token storage for a named remote resource. |
|
||||
| `--host` | Set the OAuth callback hostname. Defaults to `localhost`. |
|
||||
| `--auth-timeout` | Set how long to wait for the OAuth callback. Defaults to 300 seconds. |
|
||||
| `--ignore-tool` | Hide tools whose names match a glob pattern. Repeat for multiple patterns. |
|
||||
| `--debug` | Enable debug logging. |
|
||||
| `--silent` | Suppress non-critical logs. |
|
||||
166
docs/v3/clients/generate-cli.mdx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
---
|
||||
title: Generate CLI
|
||||
sidebarTitle: Generate CLI
|
||||
description: Turn any MCP server into a standalone, typed command-line tool.
|
||||
icon: wand-magic-sparkles
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
`fastmcp list` and `fastmcp call` let you poke at a server interactively, but they're developer tools — you always have to spell out the server spec, the tool name, and the arguments. `fastmcp generate-cli` takes the next step: it connects to a server, reads its schemas, and writes a standalone Python script where every tool is a proper subcommand with typed flags, help text, and tab completion. The result is a CLI that feels like it was hand-written for that specific server.
|
||||
|
||||
The key insight is that MCP tool schemas already contain everything a CLI framework needs: parameter names, types, descriptions, required/optional status, and defaults. `generate-cli` maps that schema into [cyclopts](https://cyclopts.readthedocs.io/) commands, so JSON Schema types become Python type annotations, descriptions become `--help` text, and required parameters become mandatory flags.
|
||||
|
||||
## Generating a Script
|
||||
|
||||
Point the command at any server spec — URLs, Python files, discovered server names, MCPConfig JSON — and it writes a CLI script:
|
||||
|
||||
```bash
|
||||
fastmcp generate-cli weather
|
||||
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 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
|
||||
fastmcp generate-cli weather my_cli.py -f
|
||||
```
|
||||
|
||||
Name-based resolution works here too, so if you have a server configured in Claude Desktop, Cursor, or any other supported editor, you can reference it by name. Run [`fastmcp discover`](/clients/cli#discovering-configured-servers) to see what's available.
|
||||
|
||||
```bash
|
||||
fastmcp generate-cli claude-code:my-server output.py
|
||||
```
|
||||
|
||||
The `--timeout` and `--auth` flags work the same way they do in `fastmcp list` and `fastmcp call`.
|
||||
|
||||
## What You Get
|
||||
|
||||
The generated script is a regular Python file — executable, editable, and yours. Here's what it looks like in practice:
|
||||
|
||||
```
|
||||
$ python cli.py --help
|
||||
Usage: weather-cli COMMAND
|
||||
|
||||
CLI for weather MCP server
|
||||
|
||||
Commands:
|
||||
call-tool Call a tool on the server
|
||||
list-tools List available tools.
|
||||
list-resources List available resources.
|
||||
read-resource Read a resource by URI.
|
||||
list-prompts List available prompts.
|
||||
get-prompt Get a prompt by name. Pass arguments as key=value pairs.
|
||||
```
|
||||
|
||||
The `call-tool` subcommand is where the generated code lives. Each tool on the server becomes its own command:
|
||||
|
||||
```
|
||||
$ python cli.py call-tool --help
|
||||
Usage: weather-cli call-tool COMMAND
|
||||
|
||||
Call a tool on the server
|
||||
|
||||
Commands:
|
||||
get_forecast Get the weather forecast for a city.
|
||||
search_city Search for a city by name.
|
||||
```
|
||||
|
||||
And each tool has typed parameters with help text pulled directly from the server's schema:
|
||||
|
||||
```
|
||||
$ python cli.py call-tool get_forecast --help
|
||||
Usage: weather-cli call-tool get_forecast [OPTIONS]
|
||||
|
||||
Get the weather forecast for a city.
|
||||
|
||||
Options:
|
||||
--city [str] City name (required)
|
||||
--days [int] Number of forecast days (default: 3)
|
||||
```
|
||||
|
||||
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 <value> --days <value>
|
||||
```
|
||||
|
||||
| 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`.
|
||||
|
||||
At the top of the generated file, a `CLIENT_SPEC` variable holds the resolved transport: either a URL string or a `StdioTransport` with the command and arguments baked in. Every invocation connects through this spec, so the script works without any external configuration.
|
||||
|
||||
### Parameter Handling
|
||||
|
||||
Parameters are mapped intelligently based on their complexity:
|
||||
|
||||
**Simple types** (`string`, `integer`, `number`, `boolean`) become typed Python parameters with clean flags:
|
||||
```bash
|
||||
python cli.py call-tool get_forecast --city London --days 3
|
||||
```
|
||||
|
||||
**Arrays of simple types** (`array` with `string`/`integer`/`number`/`boolean` items) become `list[T]` parameters that accept multiple flags:
|
||||
```bash
|
||||
python cli.py call-tool tag_items --tags python --tags fastapi --tags mcp
|
||||
```
|
||||
|
||||
**Complex types** (objects, nested arrays, or unions) accept JSON strings. The tool's `--help` displays the full JSON schema so you know exactly what structure to pass:
|
||||
```bash
|
||||
python cli.py call-tool create_user \
|
||||
--name John \
|
||||
--metadata '{"role": "admin", "dept": "engineering"}'
|
||||
```
|
||||
|
||||
Required parameters are mandatory flags; optional ones default to their schema default or `None`. Empty values are filtered out before calling the server.
|
||||
|
||||
Beyond tool commands, the script includes generic commands that work regardless of what the server exposes: `list-tools`, `list-resources`, `read-resource`, `list-prompts`, and `get-prompt`. These connect to the server at runtime, so they always reflect the server's current state even if the tools have changed since generation.
|
||||
|
||||
## Editing the Output
|
||||
|
||||
The most common edit is changing `CLIENT_SPEC`. If you generated from a local dev server and want to point at production, just change the string. If you generated from a discovered name and want to pin the transport, replace it with an explicit URL or `StdioTransport`.
|
||||
|
||||
Beyond that, it's a regular Python file. You can add commands, change the output formatting, integrate it into a larger application, or strip out the parts you don't need. The helper functions (`_call_tool`, `_print_tool_result`) are thin wrappers around `fastmcp.Client` that are easy to adapt.
|
||||
|
||||
The generated script requires `fastmcp` as a dependency. If the script lives outside a project that already has fastmcp installed, `uv run` is the easiest way to run it without permanent installation:
|
||||
|
||||
```bash
|
||||
uv run --with fastmcp python cli.py call-tool get_forecast --city London
|
||||
```
|
||||
92
docs/v3/clients/logging.mdx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
---
|
||||
title: Server Logging
|
||||
sidebarTitle: Logging
|
||||
description: Receive and handle log messages from MCP servers.
|
||||
icon: receipt
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="2.0.0" />
|
||||
|
||||
Use this when you need to capture or process log messages sent by the server.
|
||||
|
||||
MCP servers can emit log messages to clients. The client handles these through a log handler callback.
|
||||
|
||||
## Log Handler
|
||||
|
||||
Provide a `log_handler` function when creating the client:
|
||||
|
||||
```python
|
||||
import logging
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.logging import LogMessage
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
LOGGING_LEVEL_MAP = logging.getLevelNamesMapping()
|
||||
|
||||
async def log_handler(message: LogMessage):
|
||||
"""Forward MCP server logs to Python's logging system."""
|
||||
msg = message.data.get('msg')
|
||||
extra = message.data.get('extra')
|
||||
|
||||
level = LOGGING_LEVEL_MAP.get(message.level.upper(), logging.INFO)
|
||||
logger.log(level, msg, extra=extra)
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
log_handler=log_handler,
|
||||
)
|
||||
```
|
||||
|
||||
The handler receives a `LogMessage` object:
|
||||
|
||||
<Card icon="code" title="LogMessage">
|
||||
<ResponseField name="level" type='Literal["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"]'>
|
||||
The log level
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="logger" type="str | None">
|
||||
The logger name (may be None)
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="data" type="dict">
|
||||
The log payload, containing `msg` and `extra` keys
|
||||
</ResponseField>
|
||||
</Card>
|
||||
|
||||
## Structured Logs
|
||||
|
||||
The `message.data` attribute is a dictionary containing the log payload. This enables structured logging with rich contextual information.
|
||||
|
||||
```python
|
||||
async def detailed_log_handler(message: LogMessage):
|
||||
msg = message.data.get('msg')
|
||||
extra = message.data.get('extra')
|
||||
|
||||
if message.level == "error":
|
||||
print(f"ERROR: {msg} | Details: {extra}")
|
||||
elif message.level == "warning":
|
||||
print(f"WARNING: {msg} | Details: {extra}")
|
||||
else:
|
||||
print(f"{message.level.upper()}: {msg}")
|
||||
```
|
||||
|
||||
This structure is preserved even when logs are forwarded through a FastMCP proxy, making it useful for debugging multi-server applications.
|
||||
|
||||
## Default Behavior
|
||||
|
||||
If you do not provide a custom `log_handler`, FastMCP's default handler routes server logs to Python's logging system at the appropriate severity level. The MCP levels map as follows: `notice` becomes INFO; `alert` and `emergency` become CRITICAL.
|
||||
|
||||
```python
|
||||
client = Client("my_mcp_server.py")
|
||||
|
||||
async with client:
|
||||
# Server logs are forwarded at proper severity automatically
|
||||
await client.call_tool("some_tool")
|
||||
```
|
||||
155
docs/v3/clients/notifications.mdx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
---
|
||||
title: Notifications
|
||||
sidebarTitle: Notifications
|
||||
description: Handle server-sent notifications for list changes and other events.
|
||||
icon: envelope
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx";
|
||||
|
||||
<VersionBadge version="2.9.1" />
|
||||
|
||||
Use this when you need to react to server-side changes like tool list updates or resource modifications.
|
||||
|
||||
MCP servers can send notifications to inform clients about state changes. The message handler provides a unified way to process these notifications.
|
||||
|
||||
## Handling Notifications
|
||||
|
||||
The simplest approach is a function that receives all messages and filters for the notifications you care about:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
async def message_handler(message):
|
||||
"""Handle MCP notifications from the server."""
|
||||
if hasattr(message, 'root'):
|
||||
method = message.root.method
|
||||
|
||||
if method == "notifications/tools/list_changed":
|
||||
print("Tools have changed - refresh tool cache")
|
||||
elif method == "notifications/resources/list_changed":
|
||||
print("Resources have changed")
|
||||
elif method == "notifications/prompts/list_changed":
|
||||
print("Prompts have changed")
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
message_handler=message_handler,
|
||||
)
|
||||
```
|
||||
|
||||
## MessageHandler Class
|
||||
|
||||
For fine-grained targeting, subclass `MessageHandler` to use specific hooks:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.messages import MessageHandler
|
||||
import mcp.types
|
||||
|
||||
class MyMessageHandler(MessageHandler):
|
||||
async def on_tool_list_changed(
|
||||
self, notification: mcp.types.ToolListChangedNotification
|
||||
) -> None:
|
||||
"""Handle tool list changes."""
|
||||
print("Tool list changed - refreshing available tools")
|
||||
|
||||
async def on_resource_list_changed(
|
||||
self, notification: mcp.types.ResourceListChangedNotification
|
||||
) -> None:
|
||||
"""Handle resource list changes."""
|
||||
print("Resource list changed")
|
||||
|
||||
async def on_prompt_list_changed(
|
||||
self, notification: mcp.types.PromptListChangedNotification
|
||||
) -> None:
|
||||
"""Handle prompt list changes."""
|
||||
print("Prompt list changed")
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
message_handler=MyMessageHandler(),
|
||||
)
|
||||
```
|
||||
|
||||
### Handler Template
|
||||
|
||||
```python
|
||||
from fastmcp.client.messages import MessageHandler
|
||||
import mcp.types
|
||||
|
||||
class MyMessageHandler(MessageHandler):
|
||||
async def on_message(self, message) -> None:
|
||||
"""Called for ALL messages (requests and notifications)."""
|
||||
pass
|
||||
|
||||
async def on_notification(
|
||||
self, notification: mcp.types.ServerNotification
|
||||
) -> None:
|
||||
"""Called for notifications (fire-and-forget)."""
|
||||
pass
|
||||
|
||||
async def on_tool_list_changed(
|
||||
self, notification: mcp.types.ToolListChangedNotification
|
||||
) -> None:
|
||||
"""Called when the server's tool list changes."""
|
||||
pass
|
||||
|
||||
async def on_resource_list_changed(
|
||||
self, notification: mcp.types.ResourceListChangedNotification
|
||||
) -> None:
|
||||
"""Called when the server's resource list changes."""
|
||||
pass
|
||||
|
||||
async def on_prompt_list_changed(
|
||||
self, notification: mcp.types.PromptListChangedNotification
|
||||
) -> None:
|
||||
"""Called when the server's prompt list changes."""
|
||||
pass
|
||||
|
||||
async def on_progress(
|
||||
self, notification: mcp.types.ProgressNotification
|
||||
) -> None:
|
||||
"""Called for progress updates during long-running operations."""
|
||||
pass
|
||||
|
||||
async def on_logging_message(
|
||||
self, notification: mcp.types.LoggingMessageNotification
|
||||
) -> None:
|
||||
"""Called for log messages from the server."""
|
||||
pass
|
||||
```
|
||||
|
||||
## List Change Notifications
|
||||
|
||||
A practical example of maintaining a tool cache that refreshes when tools change:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.messages import MessageHandler
|
||||
import mcp.types
|
||||
|
||||
class ToolCacheHandler(MessageHandler):
|
||||
def __init__(self):
|
||||
self.cached_tools = []
|
||||
|
||||
async def on_tool_list_changed(
|
||||
self, notification: mcp.types.ToolListChangedNotification
|
||||
) -> None:
|
||||
"""Clear tool cache when tools change."""
|
||||
print("Tools changed - clearing cache")
|
||||
self.cached_tools = [] # Force refresh on next access
|
||||
|
||||
client = Client("server.py", message_handler=ToolCacheHandler())
|
||||
```
|
||||
|
||||
## Server Requests
|
||||
|
||||
While the message handler receives server-initiated requests, you should use dedicated callback parameters for most interactive scenarios:
|
||||
|
||||
- **Sampling requests**: Use [`sampling_handler`](/clients/sampling)
|
||||
- **Elicitation requests**: Use [`elicitation_handler`](/clients/elicitation)
|
||||
- **Progress updates**: Use [`progress_handler`](/clients/progress)
|
||||
- **Log messages**: Use [`log_handler`](/clients/logging)
|
||||
|
||||
The message handler is primarily for monitoring and handling notifications rather than responding to requests.
|
||||
67
docs/v3/clients/progress.mdx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
title: Progress Monitoring
|
||||
sidebarTitle: Progress
|
||||
description: Handle progress notifications from long-running server operations.
|
||||
icon: bars-progress
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="2.3.5" />
|
||||
|
||||
Use this when you need to track progress of long-running operations.
|
||||
|
||||
MCP servers can report progress during operations. The client receives these updates through a progress handler.
|
||||
|
||||
## Progress Handler
|
||||
|
||||
Set a handler when creating the client:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
async def progress_handler(
|
||||
progress: float,
|
||||
total: float | None,
|
||||
message: str | None
|
||||
) -> None:
|
||||
if total is not None:
|
||||
percentage = (progress / total) * 100
|
||||
print(f"Progress: {percentage:.1f}% - {message or ''}")
|
||||
else:
|
||||
print(f"Progress: {progress} - {message or ''}")
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
progress_handler=progress_handler
|
||||
)
|
||||
```
|
||||
|
||||
The handler receives three parameters:
|
||||
|
||||
<Card icon="code" title="Handler Parameters">
|
||||
<ResponseField name="progress" type="float">
|
||||
Current progress value
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="total" type="float | None">
|
||||
Expected total value (may be None if unknown)
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="message" type="str | None">
|
||||
Optional status message
|
||||
</ResponseField>
|
||||
</Card>
|
||||
|
||||
## Per-Call Handler
|
||||
|
||||
Override the client-level handler for specific tool calls:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
result = await client.call_tool(
|
||||
"long_running_task",
|
||||
{"param": "value"},
|
||||
progress_handler=my_progress_handler
|
||||
)
|
||||
```
|
||||
147
docs/v3/clients/prompts.mdx
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
---
|
||||
title: Getting Prompts
|
||||
sidebarTitle: Prompts
|
||||
description: Retrieve rendered message templates with automatic argument serialization.
|
||||
icon: message-lines
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="2.0.0" />
|
||||
|
||||
Use this when you need to retrieve server-defined message templates for LLM interactions.
|
||||
|
||||
Prompts are reusable message templates exposed by MCP servers. They can accept arguments to generate personalized message sequences for LLM interactions.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Request a rendered prompt with `get_prompt()`:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
# Simple prompt without arguments
|
||||
result = await client.get_prompt("welcome_message")
|
||||
# result -> mcp.types.GetPromptResult
|
||||
|
||||
# Access the generated messages
|
||||
for message in result.messages:
|
||||
print(f"Role: {message.role}")
|
||||
print(f"Content: {message.content}")
|
||||
```
|
||||
|
||||
Pass arguments to customize the prompt:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
result = await client.get_prompt("user_greeting", {
|
||||
"name": "Alice",
|
||||
"role": "administrator"
|
||||
})
|
||||
|
||||
for message in result.messages:
|
||||
print(f"Generated message: {message.content}")
|
||||
```
|
||||
|
||||
## Argument Serialization
|
||||
|
||||
<VersionBadge version="2.9.0" />
|
||||
|
||||
FastMCP automatically serializes complex arguments to JSON strings as required by the MCP specification. You can pass typed objects directly:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class UserData:
|
||||
name: str
|
||||
age: int
|
||||
|
||||
async with client:
|
||||
result = await client.get_prompt("analyze_user", {
|
||||
"user": UserData(name="Alice", age=30), # Automatically serialized
|
||||
"preferences": {"theme": "dark"}, # Dict serialized
|
||||
"scores": [85, 92, 78], # List serialized
|
||||
"simple_name": "Bob" # Strings unchanged
|
||||
})
|
||||
```
|
||||
|
||||
The client handles serialization using `pydantic_core.to_json()` for consistent formatting. FastMCP servers automatically deserialize these JSON strings back to the expected types.
|
||||
|
||||
## Working with Results
|
||||
|
||||
The `get_prompt()` method returns a `GetPromptResult` containing a list of messages:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
result = await client.get_prompt("conversation_starter", {"topic": "climate"})
|
||||
|
||||
for i, message in enumerate(result.messages):
|
||||
print(f"Message {i + 1}:")
|
||||
print(f" Role: {message.role}")
|
||||
print(f" Content: {message.content.text if hasattr(message.content, 'text') else message.content}")
|
||||
```
|
||||
|
||||
Prompts can generate different message types. System messages configure LLM behavior:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
result = await client.get_prompt("system_configuration", {
|
||||
"role": "helpful assistant",
|
||||
"expertise": "python programming"
|
||||
})
|
||||
|
||||
# Access the returned messages
|
||||
message = result.messages[0]
|
||||
print(f"Prompt: {message.content}")
|
||||
```
|
||||
|
||||
Conversation templates generate multi-turn flows:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
result = await client.get_prompt("interview_template", {
|
||||
"candidate_name": "Alice",
|
||||
"position": "Senior Developer"
|
||||
})
|
||||
|
||||
# Multiple messages for a conversation flow
|
||||
for message in result.messages:
|
||||
print(f"{message.role}: {message.content}")
|
||||
```
|
||||
|
||||
## Version Selection
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
When a server exposes multiple versions of a prompt, you can request a specific version:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
# Get the highest version (default)
|
||||
result = await client.get_prompt("summarize", {"text": "..."})
|
||||
|
||||
# Get a specific version
|
||||
result_v1 = await client.get_prompt("summarize", {"text": "..."}, version="1.0")
|
||||
```
|
||||
|
||||
See [Metadata](/servers/versioning#version-discovery) for how to discover available versions.
|
||||
|
||||
## Multi-Server Clients
|
||||
|
||||
When using multi-server clients, prompts are accessible directly without prefixing:
|
||||
|
||||
```python
|
||||
async with client: # Multi-server client
|
||||
result1 = await client.get_prompt("weather_prompt", {"city": "London"})
|
||||
result2 = await client.get_prompt("assistant_prompt", {"query": "help"})
|
||||
```
|
||||
|
||||
## Raw Protocol Access
|
||||
|
||||
For complete control, use `get_prompt_mcp()` which returns the full MCP protocol object:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
result = await client.get_prompt_mcp("example_prompt", {"arg": "value"})
|
||||
# result -> mcp.types.GetPromptResult
|
||||
```
|
||||
110
docs/v3/clients/resources.mdx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
title: Reading Resources
|
||||
sidebarTitle: Resources
|
||||
description: Access static and templated data sources from MCP servers.
|
||||
icon: folder-open
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="2.0.0" />
|
||||
|
||||
Use this when you need to read data from server-exposed resources like configuration files, generated content, or external data sources.
|
||||
|
||||
Resources are data sources exposed by MCP servers. They can be static files with fixed content, or dynamic templates that generate content based on parameters in the URI.
|
||||
|
||||
## Reading Resources
|
||||
|
||||
Read a resource using its URI:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
content = await client.read_resource("file:///path/to/README.md")
|
||||
# content -> list[TextResourceContents | BlobResourceContents]
|
||||
|
||||
# Access text content
|
||||
if hasattr(content[0], 'text'):
|
||||
print(content[0].text)
|
||||
|
||||
# Access binary content
|
||||
if hasattr(content[0], 'blob'):
|
||||
print(f"Binary data: {len(content[0].blob)} bytes")
|
||||
```
|
||||
|
||||
Resource templates generate content based on URI parameters. The template defines a pattern like `weather://{{city}}/current`, and you fill in the parameters when reading:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
# Read from a resource template
|
||||
weather_content = await client.read_resource("weather://london/current")
|
||||
print(weather_content[0].text)
|
||||
```
|
||||
|
||||
## Content Types
|
||||
|
||||
Resources return different content types depending on what they expose.
|
||||
|
||||
Text resources include configuration files, JSON data, and other human-readable content:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
content = await client.read_resource("resource://config/settings.json")
|
||||
|
||||
for item in content:
|
||||
if hasattr(item, 'text'):
|
||||
print(f"Text content: {item.text}")
|
||||
print(f"MIME type: {item.mimeType}")
|
||||
```
|
||||
|
||||
Binary resources include images, PDFs, and other non-text data:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
content = await client.read_resource("resource://images/logo.png")
|
||||
|
||||
for item in content:
|
||||
if hasattr(item, 'blob'):
|
||||
print(f"Binary content: {len(item.blob)} bytes")
|
||||
print(f"MIME type: {item.mimeType}")
|
||||
|
||||
# Save to file
|
||||
with open("downloaded_logo.png", "wb") as f:
|
||||
f.write(item.blob)
|
||||
```
|
||||
|
||||
## Multi-Server Clients
|
||||
|
||||
When using multi-server clients, resource URIs are prefixed with the server name:
|
||||
|
||||
```python
|
||||
async with client: # Multi-server client
|
||||
weather_icons = await client.read_resource("weather://weather/icons/sunny")
|
||||
templates = await client.read_resource("resource://assistant/templates/list")
|
||||
```
|
||||
|
||||
## Version Selection
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
When a server exposes multiple versions of a resource, you can request a specific version:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
# Read the highest version (default)
|
||||
content = await client.read_resource("data://config")
|
||||
|
||||
# Read a specific version
|
||||
content_v1 = await client.read_resource("data://config", version="1.0")
|
||||
```
|
||||
|
||||
See [Metadata](/servers/versioning#version-discovery) for how to discover available versions.
|
||||
|
||||
## Raw Protocol Access
|
||||
|
||||
For complete control, use `read_resource_mcp()` which returns the full MCP protocol object:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
result = await client.read_resource_mcp("resource://example")
|
||||
# result -> mcp.types.ReadResourceResult
|
||||
```
|
||||
45
docs/v3/clients/roots.mdx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
title: Client Roots
|
||||
sidebarTitle: Roots
|
||||
description: Provide local context and resource boundaries to MCP servers.
|
||||
icon: folder-tree
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="2.0.0" />
|
||||
|
||||
Use this when you need to tell servers what local resources the client has access to.
|
||||
|
||||
Roots inform servers about resources the client can provide. Servers can use this information to adjust behavior or provide more relevant responses.
|
||||
|
||||
## Static Roots
|
||||
|
||||
Provide a list of roots when creating the client:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
roots=["/path/to/root1", "/path/to/root2"]
|
||||
)
|
||||
```
|
||||
|
||||
## Dynamic Roots
|
||||
|
||||
Use a callback to compute roots dynamically when the server requests them:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.roots import RequestContext
|
||||
|
||||
async def roots_callback(context: RequestContext) -> list[str]:
|
||||
print(f"Server requested roots (Request ID: {context.request_id})")
|
||||
return ["/path/to/root1", "/path/to/root2"]
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
roots=roots_callback
|
||||
)
|
||||
```
|
||||
190
docs/v3/clients/sampling.mdx
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
---
|
||||
title: LLM Sampling
|
||||
sidebarTitle: Sampling
|
||||
description: Handle server-initiated LLM completion requests.
|
||||
icon: robot
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx";
|
||||
|
||||
<VersionBadge version="2.0.0" />
|
||||
|
||||
Use this when you need to respond to server requests for LLM completions.
|
||||
|
||||
MCP servers can request LLM completions from clients during tool execution. This enables servers to delegate AI reasoning to the client, which controls which LLM is used and how requests are made.
|
||||
|
||||
## Handler Template
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.sampling import SamplingMessage, SamplingParams, RequestContext
|
||||
|
||||
async def sampling_handler(
|
||||
messages: list[SamplingMessage],
|
||||
params: SamplingParams,
|
||||
context: RequestContext
|
||||
) -> str:
|
||||
"""
|
||||
Handle server requests for LLM completions.
|
||||
|
||||
Args:
|
||||
messages: Conversation messages to send to the LLM
|
||||
params: Sampling parameters (temperature, max_tokens, etc.)
|
||||
context: Request context with metadata
|
||||
|
||||
Returns:
|
||||
Generated text response from your LLM
|
||||
"""
|
||||
# Extract message content
|
||||
conversation = []
|
||||
for message in messages:
|
||||
content = message.content.text if hasattr(message.content, 'text') else str(message.content)
|
||||
conversation.append(f"{message.role}: {content}")
|
||||
|
||||
# Use the system prompt if provided
|
||||
system_prompt = params.systemPrompt or "You are a helpful assistant."
|
||||
|
||||
# Integrate with your LLM service here
|
||||
return "Generated response based on the messages"
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
sampling_handler=sampling_handler,
|
||||
)
|
||||
```
|
||||
|
||||
## Handler Parameters
|
||||
|
||||
<Card icon="code" title="SamplingMessage">
|
||||
<ResponseField name="role" type='Literal["user", "assistant"]'>
|
||||
The role of the message
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="content" type="TextContent | ImageContent | AudioContent">
|
||||
The content of the message. TextContent has a `.text` attribute.
|
||||
</ResponseField>
|
||||
</Card>
|
||||
|
||||
<Card icon="code" title="SamplingParams">
|
||||
<ResponseField name="systemPrompt" type="str | None">
|
||||
Optional system prompt the server wants to use
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="modelPreferences" type="ModelPreferences | None">
|
||||
Server preferences for model selection (hints, cost/speed/intelligence priorities)
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="temperature" type="float | None">
|
||||
Sampling temperature
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="maxTokens" type="int">
|
||||
Maximum tokens to generate
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="stopSequences" type="list[str] | None">
|
||||
Stop sequences for sampling
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="tools" type="list[Tool] | None">
|
||||
Tools the LLM can use during sampling
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="toolChoice" type="ToolChoice | None">
|
||||
Tool usage behavior (`auto`, `required`, or `none`)
|
||||
</ResponseField>
|
||||
</Card>
|
||||
|
||||
## Built-in Handlers
|
||||
|
||||
FastMCP provides built-in handlers for OpenAI, Anthropic, and Google Gemini APIs that support the full sampling API including tool use.
|
||||
|
||||
### OpenAI Handler
|
||||
|
||||
<VersionBadge version="2.11.0" />
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
sampling_handler=OpenAISamplingHandler(default_model="gpt-4o"),
|
||||
)
|
||||
```
|
||||
|
||||
For OpenAI-compatible APIs (like local models):
|
||||
|
||||
```python
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
sampling_handler=OpenAISamplingHandler(
|
||||
default_model="llama-3.1-70b",
|
||||
client=AsyncOpenAI(base_url="http://localhost:8000/v1"),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
<Note>
|
||||
Install the OpenAI handler with `pip install fastmcp[openai]`.
|
||||
</Note>
|
||||
|
||||
### Anthropic Handler
|
||||
|
||||
<VersionBadge version="2.14.1" />
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
sampling_handler=AnthropicSamplingHandler(default_model="claude-sonnet-4-5"),
|
||||
)
|
||||
```
|
||||
|
||||
<Note>
|
||||
Install the Anthropic handler with `pip install fastmcp[anthropic]`.
|
||||
</Note>
|
||||
|
||||
### Google Gemini Handler
|
||||
|
||||
<VersionBadge version="3.1.0" />
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.sampling.handlers.google_genai import GoogleGenaiSamplingHandler
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
sampling_handler=GoogleGenaiSamplingHandler(default_model="gemini-2.0-flash"),
|
||||
)
|
||||
```
|
||||
|
||||
<Note>
|
||||
Install the Google Gemini handler with `pip install fastmcp[gemini]`.
|
||||
</Note>
|
||||
|
||||
## Sampling Capabilities
|
||||
|
||||
When you provide a `sampling_handler`, FastMCP automatically advertises full sampling capabilities to the server, including tool support. To disable tool support for simpler handlers:
|
||||
|
||||
```python
|
||||
from mcp.types import SamplingCapability
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
sampling_handler=basic_handler,
|
||||
sampling_capabilities=SamplingCapability(), # No tool support
|
||||
)
|
||||
```
|
||||
|
||||
## Tool Execution
|
||||
|
||||
Tool execution happens on the server side. The client's role is to pass tools to the LLM and return the LLM's response (which may include tool use requests). The server then executes the tools and may send follow-up sampling requests with tool results.
|
||||
|
||||
<Tip>
|
||||
To implement a custom sampling handler, see the [handler source code](https://github.com/PrefectHQ/fastmcp/tree/main/fastmcp_slim/fastmcp/client/sampling/handlers) as a reference.
|
||||
</Tip>
|
||||
182
docs/v3/clients/tasks.mdx
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
---
|
||||
title: Background Tasks
|
||||
sidebarTitle: Tasks
|
||||
description: Execute operations asynchronously and track their progress.
|
||||
icon: clock
|
||||
tag: "NEW"
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="2.14.0" />
|
||||
|
||||
Use this when you need to run long operations asynchronously while doing other work.
|
||||
|
||||
The MCP task protocol lets you request operations to run in the background. The call returns a Task object immediately, letting you track progress, cancel operations, or await results.
|
||||
|
||||
## Requesting Background Execution
|
||||
|
||||
Pass `task=True` to run an operation as a background task:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
async with Client(server) as client:
|
||||
# Start a background task
|
||||
task = await client.call_tool("slow_computation", {"duration": 10}, task=True)
|
||||
|
||||
print(f"Task started: {task.task_id}")
|
||||
|
||||
# Do other work while it runs...
|
||||
|
||||
# Get the result when ready
|
||||
result = await task.result()
|
||||
```
|
||||
|
||||
This works with tools, resources, and prompts:
|
||||
|
||||
```python
|
||||
tool_task = await client.call_tool("my_tool", args, task=True)
|
||||
resource_task = await client.read_resource("file://large.txt", task=True)
|
||||
prompt_task = await client.get_prompt("my_prompt", args, task=True)
|
||||
```
|
||||
|
||||
## Task API
|
||||
|
||||
All task types share a common interface.
|
||||
|
||||
### Getting Results
|
||||
|
||||
Call `await task.result()` or simply `await task` to block until the task completes:
|
||||
|
||||
```python
|
||||
task = await client.call_tool("analyze", {"text": "hello"}, task=True)
|
||||
|
||||
# Wait for result (blocking)
|
||||
result = await task.result()
|
||||
# or: result = await task
|
||||
```
|
||||
|
||||
### Checking Status
|
||||
|
||||
Check the current status without blocking:
|
||||
|
||||
```python
|
||||
status = await task.status()
|
||||
print(f"{status.status}: {status.statusMessage}")
|
||||
# status.status is "working", "completed", "failed", or "cancelled"
|
||||
```
|
||||
|
||||
### Waiting with Control
|
||||
|
||||
Use `task.wait()` for more control over waiting:
|
||||
|
||||
```python
|
||||
# Wait up to 30 seconds for completion
|
||||
status = await task.wait(timeout=30.0)
|
||||
|
||||
# Wait for a specific state
|
||||
status = await task.wait(state="completed", timeout=30.0)
|
||||
```
|
||||
|
||||
### Cancellation
|
||||
|
||||
Cancel a running task:
|
||||
|
||||
```python
|
||||
await task.cancel()
|
||||
```
|
||||
|
||||
## Status Updates
|
||||
|
||||
Register callbacks to receive real-time status updates as the server reports progress:
|
||||
|
||||
```python
|
||||
def on_status_change(status):
|
||||
print(f"Task {status.taskId}: {status.status} - {status.statusMessage}")
|
||||
|
||||
task.on_status_change(on_status_change)
|
||||
|
||||
# Async callbacks work too
|
||||
async def on_status_async(status):
|
||||
await log_status(status)
|
||||
|
||||
task.on_status_change(on_status_async)
|
||||
```
|
||||
|
||||
### Handler Template
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
def status_handler(status):
|
||||
"""
|
||||
Handle task status updates.
|
||||
|
||||
Args:
|
||||
status: Task status object with:
|
||||
- taskId: Unique task identifier
|
||||
- status: "working", "completed", "failed", or "cancelled"
|
||||
- statusMessage: Optional progress message from server
|
||||
"""
|
||||
if status.status == "working":
|
||||
print(f"Progress: {status.statusMessage}")
|
||||
elif status.status == "completed":
|
||||
print("Task completed")
|
||||
elif status.status == "failed":
|
||||
print(f"Task failed: {status.statusMessage}")
|
||||
|
||||
task.on_status_change(status_handler)
|
||||
```
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
You can always pass `task=True` regardless of whether the server supports background tasks. Per the MCP specification, servers without task support execute the operation immediately and return the result inline.
|
||||
|
||||
```python
|
||||
task = await client.call_tool("my_tool", args, task=True)
|
||||
|
||||
if task.returned_immediately:
|
||||
print("Server executed immediately (no background support)")
|
||||
else:
|
||||
print("Running in background")
|
||||
|
||||
# Either way, this works
|
||||
result = await task.result()
|
||||
```
|
||||
|
||||
This lets you write task-aware client code without worrying about server capabilities.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from fastmcp import Client
|
||||
|
||||
async def main():
|
||||
async with Client(server) as client:
|
||||
# Start background task
|
||||
task = await client.call_tool(
|
||||
"slow_computation",
|
||||
{"duration": 10},
|
||||
task=True,
|
||||
)
|
||||
|
||||
# Subscribe to updates
|
||||
def on_update(status):
|
||||
print(f"Progress: {status.statusMessage}")
|
||||
|
||||
task.on_status_change(on_update)
|
||||
|
||||
# Do other work while task runs
|
||||
print("Doing other work...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Wait for completion and get result
|
||||
result = await task.result()
|
||||
print(f"Result: {result.content}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
See [Server Background Tasks](/servers/tasks) for how to enable background task support on the server side.
|
||||
183
docs/v3/clients/tools.mdx
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
---
|
||||
title: Calling Tools
|
||||
sidebarTitle: Tools
|
||||
description: Execute server-side tools and handle structured results.
|
||||
icon: wrench
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="2.0.0" />
|
||||
|
||||
Use this when you need to execute server-side functions and process their results.
|
||||
|
||||
Tools are executable functions exposed by MCP servers. The client's `call_tool()` method executes a tool by name with arguments and returns structured results.
|
||||
|
||||
## Basic Execution
|
||||
|
||||
```python
|
||||
async with client:
|
||||
result = await client.call_tool("add", {"a": 5, "b": 3})
|
||||
# result -> CallToolResult with structured and unstructured data
|
||||
|
||||
# Access structured data (automatically deserialized)
|
||||
print(result.data) # 8
|
||||
|
||||
# Access traditional content blocks
|
||||
print(result.content[0].text) # "8"
|
||||
```
|
||||
|
||||
Arguments are passed as a dictionary. For multi-server clients, tool names are automatically prefixed with the server name (e.g., `weather_get_forecast` for a tool named `get_forecast` on the `weather` server).
|
||||
|
||||
## Execution Options
|
||||
|
||||
The `call_tool()` method supports timeout control and progress monitoring:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
# With timeout (aborts if execution takes longer than 2 seconds)
|
||||
result = await client.call_tool(
|
||||
"long_running_task",
|
||||
{"param": "value"},
|
||||
timeout=2.0
|
||||
)
|
||||
|
||||
# With progress handler
|
||||
result = await client.call_tool(
|
||||
"long_running_task",
|
||||
{"param": "value"},
|
||||
progress_handler=my_progress_handler
|
||||
)
|
||||
```
|
||||
|
||||
## Structured Results
|
||||
|
||||
<VersionBadge version="2.10.0" />
|
||||
|
||||
Tool execution returns a `CallToolResult` object. The `.data` property provides fully hydrated Python objects including complex types like datetimes and UUIDs, reconstructed from the server's output schema.
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
async with client:
|
||||
result = await client.call_tool("get_weather", {"city": "London"})
|
||||
|
||||
# FastMCP reconstructs complete Python objects
|
||||
weather = result.data
|
||||
print(f"Temperature: {weather.temperature}C at {weather.timestamp}")
|
||||
|
||||
# Complex types are properly deserialized
|
||||
assert isinstance(weather.timestamp, datetime)
|
||||
assert isinstance(weather.station_id, UUID)
|
||||
|
||||
# Raw structured JSON is also available
|
||||
print(f"Raw JSON: {result.structured_content}")
|
||||
```
|
||||
|
||||
<Card icon="code" title="CallToolResult Properties">
|
||||
<ResponseField name=".data" type="Any">
|
||||
Fully hydrated Python objects with complex type support (datetimes, UUIDs, custom classes). FastMCP exclusive.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name=".content" type="list[mcp.types.ContentBlock]">
|
||||
Standard MCP content blocks (`TextContent`, `ImageContent`, `AudioContent`, etc.).
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name=".structured_content" type="dict[str, Any] | None">
|
||||
Standard MCP structured JSON data as sent by the server.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name=".is_error" type="bool">
|
||||
Boolean indicating if the tool execution failed.
|
||||
</ResponseField>
|
||||
</Card>
|
||||
|
||||
For tools without output schemas or when deserialization fails, `.data` will be `None`. Fall back to content blocks in that case:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
result = await client.call_tool("legacy_tool", {"param": "value"})
|
||||
|
||||
if result.data is not None:
|
||||
print(f"Structured: {result.data}")
|
||||
else:
|
||||
for content in result.content:
|
||||
if hasattr(content, 'text'):
|
||||
print(f"Text result: {content.text}")
|
||||
```
|
||||
|
||||
<Tip>
|
||||
FastMCP servers automatically wrap primitive results (like `int`, `str`, `bool`) in a `{"result": value}` structure. FastMCP clients automatically unwrap this, so you get the original value in `.data`.
|
||||
</Tip>
|
||||
|
||||
## Error Handling
|
||||
|
||||
By default, `call_tool()` raises a `ToolError` if the tool execution fails:
|
||||
|
||||
```python
|
||||
from fastmcp.exceptions import ToolError
|
||||
|
||||
async with client:
|
||||
try:
|
||||
result = await client.call_tool("potentially_failing_tool", {"param": "value"})
|
||||
print("Tool succeeded:", result.data)
|
||||
except ToolError as e:
|
||||
print(f"Tool failed: {e}")
|
||||
```
|
||||
|
||||
To handle errors manually instead of catching exceptions, disable automatic error raising:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
result = await client.call_tool(
|
||||
"potentially_failing_tool",
|
||||
{"param": "value"},
|
||||
raise_on_error=False
|
||||
)
|
||||
|
||||
if result.is_error:
|
||||
print(f"Tool failed: {result.content[0].text}")
|
||||
else:
|
||||
print(f"Tool succeeded: {result.data}")
|
||||
```
|
||||
|
||||
## Sending Metadata
|
||||
|
||||
<VersionBadge version="2.13.1" />
|
||||
|
||||
The `meta` parameter sends ancillary information alongside tool calls for observability, debugging, or client identification:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
result = await client.call_tool(
|
||||
name="send_email",
|
||||
arguments={
|
||||
"to": "user@example.com",
|
||||
"subject": "Hello",
|
||||
"body": "Welcome!"
|
||||
},
|
||||
meta={
|
||||
"trace_id": "abc-123",
|
||||
"request_source": "mobile_app"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
See [Client Metadata](/servers/context#client-metadata) to learn how servers access this data.
|
||||
|
||||
## Raw Protocol Access
|
||||
|
||||
For complete control, use `call_tool_mcp()` which returns the raw MCP protocol object:
|
||||
|
||||
```python
|
||||
async with client:
|
||||
result = await client.call_tool_mcp("my_tool", {"param": "value"})
|
||||
# result -> mcp.types.CallToolResult
|
||||
|
||||
if result.isError:
|
||||
print(f"Tool failed: {result.content}")
|
||||
else:
|
||||
print(f"Tool succeeded: {result.content}")
|
||||
# Note: No automatic deserialization with call_tool_mcp()
|
||||
```
|
||||
267
docs/v3/clients/transports.mdx
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
---
|
||||
title: Client Transports
|
||||
sidebarTitle: Transports
|
||||
description: Configure how clients connect to and communicate with MCP servers.
|
||||
icon: link
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="2.0.0" />
|
||||
|
||||
Transports handle the underlying connection between your client and MCP servers. While the client can automatically select a transport based on what you pass to it, instantiating transports explicitly gives you full control over configuration.
|
||||
|
||||
## STDIO Transport
|
||||
|
||||
STDIO transport communicates with MCP servers through subprocess pipes. When using STDIO, your client launches and manages the server process, controlling its lifecycle and environment.
|
||||
|
||||
<Warning>
|
||||
STDIO servers run in isolated environments by default. They do not inherit your shell's environment variables. You must explicitly pass any configuration the server needs.
|
||||
</Warning>
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.transports import StdioTransport
|
||||
|
||||
transport = StdioTransport(
|
||||
command="python",
|
||||
args=["my_server.py", "--verbose"],
|
||||
env={"API_KEY": "secret", "LOG_LEVEL": "DEBUG"},
|
||||
cwd="/path/to/server"
|
||||
)
|
||||
client = Client(transport)
|
||||
```
|
||||
|
||||
For convenience, the client can infer STDIO transport from file paths, though this limits configuration options:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
client = Client("my_server.py") # Limited - no configuration options
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Since STDIO servers do not inherit your environment, you need strategies for passing configuration.
|
||||
|
||||
**Selective forwarding** passes only the variables your server needs:
|
||||
|
||||
```python
|
||||
import os
|
||||
from fastmcp.client.transports import StdioTransport
|
||||
|
||||
required_vars = ["API_KEY", "DATABASE_URL", "REDIS_HOST"]
|
||||
env = {var: os.environ[var] for var in required_vars if var in os.environ}
|
||||
|
||||
transport = StdioTransport(command="python", args=["server.py"], env=env)
|
||||
client = Client(transport)
|
||||
```
|
||||
|
||||
**Loading from .env files** keeps configuration separate from code:
|
||||
|
||||
```python
|
||||
from dotenv import dotenv_values
|
||||
from fastmcp.client.transports import StdioTransport
|
||||
|
||||
env = dotenv_values(".env")
|
||||
transport = StdioTransport(command="python", args=["server.py"], env=env)
|
||||
client = Client(transport)
|
||||
```
|
||||
|
||||
### Session Persistence
|
||||
|
||||
STDIO transports maintain sessions across multiple client contexts by default (`keep_alive=True`). This reuses the same subprocess for multiple connections, improving performance.
|
||||
|
||||
```python
|
||||
from fastmcp.client.transports import StdioTransport
|
||||
|
||||
transport = StdioTransport(command="python", args=["server.py"])
|
||||
client = Client(transport)
|
||||
|
||||
async def efficient_multiple_operations():
|
||||
async with client:
|
||||
await client.ping()
|
||||
|
||||
async with client: # Reuses the same subprocess
|
||||
await client.call_tool("process_data", {"file": "data.csv"})
|
||||
```
|
||||
|
||||
For complete isolation between connections, disable session persistence:
|
||||
|
||||
```python
|
||||
transport = StdioTransport(command="python", args=["server.py"], keep_alive=False)
|
||||
```
|
||||
|
||||
## HTTP Transport
|
||||
|
||||
<VersionBadge version="2.3.0" />
|
||||
|
||||
HTTP transport connects to MCP servers running as web services. This is the recommended transport for production deployments.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.transports import StreamableHttpTransport
|
||||
|
||||
transport = StreamableHttpTransport(
|
||||
url="https://api.example.com/mcp",
|
||||
headers={
|
||||
"Authorization": "Bearer your-token-here",
|
||||
"X-Custom-Header": "value"
|
||||
}
|
||||
)
|
||||
client = Client(transport)
|
||||
```
|
||||
|
||||
FastMCP also provides authentication helpers:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import BearerAuth
|
||||
|
||||
client = Client(
|
||||
"https://api.example.com/mcp",
|
||||
auth=BearerAuth("your-token-here")
|
||||
)
|
||||
```
|
||||
|
||||
### SSL Verification
|
||||
|
||||
By default, HTTPS connections verify the server's SSL certificate. You can customize this behavior with the `verify` parameter, which accepts the same values as [httpx](https://www.python-httpx.org/advanced/ssl/):
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
# Disable SSL verification (e.g., for self-signed certs in development)
|
||||
client = Client("https://dev-server.internal/mcp", verify=False)
|
||||
|
||||
# Use a custom CA bundle
|
||||
client = Client("https://corp-server.internal/mcp", verify="/path/to/ca-bundle.pem")
|
||||
|
||||
# Use a custom SSL context for full control
|
||||
import ssl
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.load_verify_locations("/path/to/internal-ca.pem")
|
||||
client = Client("https://corp-server.internal/mcp", verify=ctx)
|
||||
```
|
||||
|
||||
The `verify` parameter is also available directly on `StreamableHttpTransport` and `SSETransport`:
|
||||
|
||||
```python
|
||||
from fastmcp.client.transports import StreamableHttpTransport
|
||||
|
||||
transport = StreamableHttpTransport(
|
||||
url="https://dev-server.internal/mcp",
|
||||
verify=False,
|
||||
)
|
||||
client = Client(transport)
|
||||
```
|
||||
|
||||
### SSE Transport
|
||||
|
||||
Server-Sent Events transport is maintained for backward compatibility. Use Streamable HTTP for new deployments unless you have specific infrastructure requirements.
|
||||
|
||||
```python
|
||||
from fastmcp.client.transports import SSETransport
|
||||
|
||||
transport = SSETransport(
|
||||
url="https://api.example.com/sse",
|
||||
headers={"Authorization": "Bearer token"}
|
||||
)
|
||||
client = Client(transport)
|
||||
```
|
||||
|
||||
## In-Memory Transport
|
||||
|
||||
In-memory transport connects directly to a FastMCP server instance within the same Python process. This eliminates both subprocess management and network overhead, making it ideal for testing.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP, Client
|
||||
import os
|
||||
|
||||
mcp = FastMCP("TestServer")
|
||||
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
prefix = os.environ.get("GREETING_PREFIX", "Hello")
|
||||
return f"{prefix}, {name}!"
|
||||
|
||||
client = Client(mcp)
|
||||
|
||||
async with client:
|
||||
result = await client.call_tool("greet", {"name": "World"})
|
||||
```
|
||||
|
||||
<Note>
|
||||
Unlike STDIO transports, in-memory servers share the same memory space and environment variables as your client code.
|
||||
</Note>
|
||||
|
||||
## Multi-Server Configuration
|
||||
|
||||
<VersionBadge version="2.4.0" />
|
||||
|
||||
Connect to multiple servers defined in a configuration dictionary:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
config = {
|
||||
"mcpServers": {
|
||||
"weather": {
|
||||
"url": "https://weather.example.com/mcp",
|
||||
"transport": "http"
|
||||
},
|
||||
"assistant": {
|
||||
"command": "python",
|
||||
"args": ["./assistant.py"],
|
||||
"env": {"LOG_LEVEL": "INFO"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client = Client(config)
|
||||
|
||||
async with client:
|
||||
# Tools are namespaced by server
|
||||
weather = await client.call_tool("weather_get_forecast", {"city": "NYC"})
|
||||
answer = await client.call_tool("assistant_ask", {"question": "What?"})
|
||||
```
|
||||
|
||||
### Tool Transformations
|
||||
|
||||
FastMCP supports tool transformations within the configuration. You can change names, descriptions, tags, and arguments for tools from a server.
|
||||
|
||||
```python
|
||||
config = {
|
||||
"mcpServers": {
|
||||
"weather": {
|
||||
"url": "https://weather.example.com/mcp",
|
||||
"transport": "http",
|
||||
"tools": {
|
||||
"weather_get_forecast": {
|
||||
"name": "miami_weather",
|
||||
"description": "Get the weather for Miami",
|
||||
"arguments": {
|
||||
"city": {
|
||||
"default": "Miami",
|
||||
"hide": True,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To filter tools by tag, use `include_tags` or `exclude_tags` at the server level:
|
||||
|
||||
```python
|
||||
config = {
|
||||
"mcpServers": {
|
||||
"weather": {
|
||||
"url": "https://weather.example.com/mcp",
|
||||
"include_tags": ["forecast"] # Only tools with this tag
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/v3/community/README.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Community Section
|
||||
|
||||
This directory contains community-contributed content and showcases for FastMCP.
|
||||
|
||||
## Structure
|
||||
|
||||
- `showcase.mdx` - Main community showcase page featuring high-quality projects and examples
|
||||
|
||||
## Adding Content
|
||||
|
||||
To add new community content:
|
||||
1. Create a new MDX file in this directory
|
||||
2. Update `docs.json` to include it in the navigation
|
||||
3. Follow the existing format for consistency
|
||||
|
||||
## Guidelines
|
||||
|
||||
Community content should:
|
||||
- Demonstrate best practices
|
||||
- Provide educational value
|
||||
- Include proper documentation
|
||||
- Be maintained and up-to-date
|
||||
65
docs/v3/community/showcase.mdx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
title: 'Community Showcase'
|
||||
description: 'High-quality projects and examples from the FastMCP community'
|
||||
icon: 'users'
|
||||
---
|
||||
|
||||
import { YouTubeEmbed } from '/snippets/youtube-embed.mdx'
|
||||
|
||||
## Join the Community
|
||||
|
||||
<Card title="FastMCP Discord" icon="discord" href="https://discord.gg/uu8dJCgttd">
|
||||
Connect with other FastMCP developers, share your projects, and discuss ideas.
|
||||
</Card>
|
||||
|
||||
## Featured Projects
|
||||
|
||||
Discover exemplary MCP servers and implementations created by our community. These projects demonstrate best practices and innovative uses of FastMCP.
|
||||
|
||||
### Learning Resources
|
||||
|
||||
<Card title="MCP Dummy Server" icon="graduation-cap" href="https://github.com/WaiYanNyeinNaing/mcp-dummy-server">
|
||||
A comprehensive educational example demonstrating FastMCP best practices with professional dual-transport server implementation, interactive test client, and detailed documentation.
|
||||
</Card>
|
||||
|
||||
#### Video Tutorials
|
||||
|
||||
**Build Remote MCP Servers w/ Python & FastMCP** - Claude Integrations Tutorial by Greg + Code
|
||||
|
||||
<YouTubeEmbed
|
||||
videoId="bOYkbXP-GGo"
|
||||
title="Build Remote MCP Servers w/ Python & FastMCP"
|
||||
/>
|
||||
|
||||
**FastMCP — the best way to build an MCP server with Python** - Tutorial by ZazenCodes
|
||||
|
||||
<YouTubeEmbed
|
||||
videoId="rnljvmHorQw"
|
||||
title="FastMCP — the best way to build an MCP server with Python"
|
||||
/>
|
||||
|
||||
**Speedrun a MCP server for Claude Desktop (fastmcp)** - Tutorial by Nate from Prefect
|
||||
|
||||
<YouTubeEmbed
|
||||
videoId="67ZwpkUEtSI"
|
||||
title="Speedrun a MCP server for Claude Desktop (fastmcp)"
|
||||
/>
|
||||
|
||||
### Community Examples
|
||||
|
||||
Have you built something interesting with FastMCP? We'd love to feature high-quality examples here! Start a [discussion on GitHub](https://github.com/PrefectHQ/fastmcp/discussions) to share your project.
|
||||
|
||||
## Contributing
|
||||
|
||||
To get your project featured:
|
||||
|
||||
1. Ensure your project demonstrates best practices
|
||||
2. Include comprehensive documentation
|
||||
3. Add clear usage examples
|
||||
4. Open a discussion in our [GitHub Discussions](https://github.com/PrefectHQ/fastmcp/discussions)
|
||||
|
||||
We review submissions regularly and feature projects that provide value to the FastMCP community.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Contrib Modules](/patterns/contrib) - Community-contributed modules that are distributed with FastMCP itself
|
||||
924
docs/v3/deployment/http.mdx
Normal file
|
|
@ -0,0 +1,924 @@
|
|||
---
|
||||
title: HTTP Deployment
|
||||
sidebarTitle: HTTP Deployment
|
||||
description: Deploy your FastMCP server over HTTP for remote access
|
||||
icon: server
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx";
|
||||
|
||||
<Tip>
|
||||
STDIO transport is perfect for local development and desktop applications. But to unlock the full potential of MCP—centralized services, multi-client access, and network availability—you need remote HTTP deployment.
|
||||
</Tip>
|
||||
|
||||
This guide walks you through deploying your FastMCP server as a remote MCP service that's accessible via a URL. Once deployed, your MCP server will be available over the network, allowing multiple clients to connect simultaneously and enabling integration with cloud-based LLM applications. This guide focuses specifically on remote MCP deployment, not local STDIO servers.
|
||||
|
||||
## Choosing Your Approach
|
||||
|
||||
FastMCP provides two ways to deploy your server as an HTTP service. Understanding the trade-offs helps you choose the right approach for your needs.
|
||||
|
||||
The **direct HTTP server** approach is simpler and perfect for getting started quickly. You modify your server's `run()` method to use HTTP transport, and FastMCP handles all the web server configuration. This approach works well for standalone deployments where you want your MCP server to be the only service running on a port.
|
||||
|
||||
The **ASGI application** approach gives you more control and flexibility. Instead of running the server directly, you create an ASGI application that can be served by Uvicorn. This approach is better when you need advanced server features like multiple workers, custom middleware, or when you're integrating with existing web applications.
|
||||
|
||||
### Direct HTTP Server
|
||||
|
||||
The simplest way to get your MCP server online is to use the built-in `run()` method with HTTP transport. This approach handles all the server configuration for you and is ideal when you want a standalone MCP server without additional complexity.
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
|
||||
@mcp.tool
|
||||
def process_data(input: str) -> str:
|
||||
"""Process data on the server"""
|
||||
return f"Processed: {input}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="http", host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
Run your server with a simple Python command:
|
||||
```bash
|
||||
python server.py
|
||||
```
|
||||
|
||||
Your server is now accessible at `http://localhost:8000/mcp` (or use your server's actual IP address for remote access).
|
||||
|
||||
This approach is ideal when you want to get online quickly with minimal configuration. It's perfect for internal tools, development environments, or simple deployments where you don't need advanced server features. The built-in server handles all the HTTP details, letting you focus on your MCP implementation.
|
||||
|
||||
### ASGI Application
|
||||
|
||||
For production deployments, you'll often want more control over how your server runs. FastMCP can create a standard ASGI application that works with any ASGI server like Uvicorn, Gunicorn, or Hypercorn. This approach is particularly useful when you need to configure advanced server options, run multiple workers, or integrate with existing infrastructure.
|
||||
|
||||
```python app.py
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
|
||||
@mcp.tool
|
||||
def process_data(input: str) -> str:
|
||||
"""Process data on the server"""
|
||||
return f"Processed: {input}"
|
||||
|
||||
# Create ASGI application
|
||||
app = mcp.http_app()
|
||||
```
|
||||
|
||||
Run with any ASGI server - here's an example with Uvicorn:
|
||||
```bash
|
||||
uvicorn app:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
Your server is accessible at the same URL: `http://localhost:8000/mcp` (or use your server's actual IP address for remote access).
|
||||
|
||||
The ASGI approach shines in production environments where you need reliability and performance. You can run multiple worker processes to handle concurrent requests, add custom middleware for logging or monitoring, integrate with existing deployment pipelines, or mount your MCP server as part of a larger application.
|
||||
|
||||
## Configuring Your Server
|
||||
|
||||
### Custom Path
|
||||
|
||||
By default, your MCP server is accessible at `/mcp/` on your domain. You can customize this path to fit your URL structure or avoid conflicts with existing endpoints. This is particularly useful when integrating MCP into an existing application or following specific API conventions.
|
||||
|
||||
```python
|
||||
# Option 1: With mcp.run()
|
||||
mcp.run(transport="http", host="0.0.0.0", port=8000, path="/api/mcp/")
|
||||
|
||||
# Option 2: With ASGI app
|
||||
app = mcp.http_app(path="/api/mcp/")
|
||||
```
|
||||
|
||||
Now your server is accessible at `http://localhost:8000/api/mcp/`.
|
||||
|
||||
### Authentication
|
||||
|
||||
<Warning>
|
||||
Authentication is **highly recommended** for remote MCP servers. Some LLM clients require authentication for remote servers and will refuse to connect without it.
|
||||
</Warning>
|
||||
|
||||
FastMCP supports multiple authentication methods to secure your remote server. See the [Authentication Overview](/servers/auth/authentication) for complete configuration options including Bearer tokens, JWT, and OAuth.
|
||||
|
||||
If you're mounting an authenticated server under a path prefix, see [Mounting Authenticated Servers](#mounting-authenticated-servers) below for important routing considerations.
|
||||
|
||||
### Host and Origin Protection
|
||||
|
||||
FastMCP can validate `Host` and browser `Origin` headers for Streamable HTTP requests before they reach MCP session handling. This request guard protects localhost-bound servers from DNS rebinding attacks, and it remains opt-in in FastMCP 3.x to preserve compatibility with existing ASGI, serverless, and reverse-proxy deployments.
|
||||
|
||||
Think of this as a request guard rather than CORS middleware. It decides whether a request can reach MCP session handling. CORS remains a separate browser response-header policy; configure CORS middleware separately when browser JavaScript must read cross-origin responses.
|
||||
|
||||
Enable strict validation with `host_origin_protection=True`. When you deploy behind a public hostname, add the hostname clients use to reach your MCP endpoint. If a browser-based MCP client runs on a separate origin, add that origin as well:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
|
||||
app = mcp.http_app(
|
||||
host_origin_protection=True,
|
||||
allowed_hosts=["mcp.example.com"],
|
||||
allowed_origins=["https://app.example.com"],
|
||||
)
|
||||
```
|
||||
|
||||
For the direct server approach, pass the same values to `run()`:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(
|
||||
transport="http",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
host_origin_protection=True,
|
||||
allowed_hosts=["mcp.example.com"],
|
||||
allowed_origins=["https://app.example.com"],
|
||||
)
|
||||
```
|
||||
|
||||
You can also configure these values with environment variables:
|
||||
|
||||
```bash
|
||||
export FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=true
|
||||
export FASTMCP_HTTP_ALLOWED_HOSTS='["mcp.example.com"]'
|
||||
export FASTMCP_HTTP_ALLOWED_ORIGINS='["https://app.example.com"]'
|
||||
```
|
||||
|
||||
Use `host_origin_protection="auto"` to protect localhost-bound direct servers while allowing ASGI, serverless, and reverse-proxy deployments to keep their existing Host handling unless they configure explicit trust rules. Use `host_origin_protection=False` to keep the request guard disabled.
|
||||
|
||||
### Health Checks
|
||||
|
||||
Health check endpoints are essential for monitoring your deployed server and ensuring it's responding correctly. FastMCP allows you to add custom routes alongside your MCP endpoints, making it easy to implement health checks that work with both deployment approaches.
|
||||
|
||||
```python
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
@mcp.custom_route("/health", methods=["GET"])
|
||||
async def health_check(request):
|
||||
return JSONResponse({"status": "healthy", "service": "mcp-server"})
|
||||
```
|
||||
|
||||
This health endpoint will be available at `http://localhost:8000/health` and can be used by load balancers, monitoring systems, or deployment platforms to verify your server is running.
|
||||
|
||||
<Note>
|
||||
Custom routes are never protected by the server's authentication middleware, even when an `AuthProvider` is configured. This is by design — the primary use case for custom routes is unauthenticated operational endpoints like health checks and readiness probes. If you need authenticated HTTP endpoints alongside your MCP server, [mount it in a FastAPI app](/integrations/fastapi) and use FastAPI's `Depends()` for auth on your routes.
|
||||
</Note>
|
||||
|
||||
### Custom Middleware
|
||||
|
||||
|
||||
<VersionBadge version="2.3.2" />
|
||||
|
||||
Add custom Starlette middleware to your FastMCP ASGI apps:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
# Create your FastMCP server
|
||||
mcp = FastMCP("MyServer")
|
||||
|
||||
# Define middleware
|
||||
middleware = [
|
||||
Middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
]
|
||||
|
||||
# Create ASGI app with middleware
|
||||
http_app = mcp.http_app(middleware=middleware)
|
||||
```
|
||||
|
||||
### CORS for Browser-Based Clients
|
||||
|
||||
<Tip>
|
||||
Most MCP clients, including those that you access through a browser like ChatGPT or Claude, don't need CORS configuration. Only enable CORS if you're working with an MCP client that connects directly from a browser, such as debugging tools or inspectors.
|
||||
</Tip>
|
||||
|
||||
CORS (Cross-Origin Resource Sharing) is needed when JavaScript running in a web browser connects directly to your MCP server. This is different from using an LLM through a browser—in that case, the browser connects to the LLM service, and the LLM service connects to your MCP server (no CORS needed).
|
||||
|
||||
Host and Origin protection runs before CORS when it is active for a request. Add browser client origins to `allowed_origins` so trusted browser requests reach the CORS middleware, then configure CORS to let browser JavaScript read the MCP response headers it needs. Setting `allowed_origins` trusts the request; it does not emit `Access-Control-Allow-Origin` or other CORS response headers.
|
||||
|
||||
Browser-based MCP clients that need CORS include:
|
||||
|
||||
- **MCP Inspector** - Browser-based debugging tool for testing MCP servers
|
||||
- **Custom browser-based MCP clients** - If you're building a web app that directly connects to MCP servers
|
||||
|
||||
For these scenarios, add CORS middleware with the specific headers required for MCP protocol:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
mcp = FastMCP("MyServer")
|
||||
|
||||
# Configure CORS for browser-based clients
|
||||
middleware = [
|
||||
Middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Allow all origins; use specific origins for security
|
||||
allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
|
||||
allow_headers=[
|
||||
"mcp-protocol-version",
|
||||
"mcp-session-id",
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
],
|
||||
expose_headers=["mcp-session-id"],
|
||||
)
|
||||
]
|
||||
|
||||
app = mcp.http_app(middleware=middleware)
|
||||
```
|
||||
|
||||
**Key configuration details:**
|
||||
|
||||
- **`allow_origins`**: Specify exact origins (e.g., `["http://localhost:3000"]`) rather than `["*"]` for production deployments
|
||||
- **`allow_headers`**: Must include `mcp-protocol-version`, `mcp-session-id`, and `Authorization` (for authenticated servers)
|
||||
- **`expose_headers`**: Must include `mcp-session-id` so JavaScript can read the session ID from responses and send it in subsequent requests
|
||||
|
||||
Without `expose_headers=["mcp-session-id"]`, browsers will receive the session ID but JavaScript won't be able to access it, causing session management to fail.
|
||||
|
||||
<Warning>
|
||||
**Production Security**: Never use `allow_origins=["*"]` in production. Specify the exact origins of your browser-based clients. Using wildcards exposes your server to unauthorized access from any website.
|
||||
</Warning>
|
||||
|
||||
### SSE Polling for Long-Running Operations
|
||||
|
||||
<VersionBadge version="2.14.0" />
|
||||
|
||||
<Note>
|
||||
This feature only applies to the **StreamableHTTP transport** (the default for `http_app()`). It does not apply to the legacy SSE transport (`transport="sse"`).
|
||||
</Note>
|
||||
|
||||
When running tools that take a long time to complete, you may encounter issues with load balancers or proxies terminating connections that stay idle too long. [SEP-1699](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699) introduces SSE polling to solve this by allowing the server to gracefully close connections and have clients automatically reconnect.
|
||||
|
||||
To enable SSE polling, configure an `EventStore` when creating your HTTP application:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP, Context
|
||||
from fastmcp.server.event_store import EventStore
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
|
||||
@mcp.tool
|
||||
async def long_running_task(ctx: Context) -> str:
|
||||
"""A task that takes several minutes to complete."""
|
||||
for i in range(100):
|
||||
await ctx.report_progress(i, 100)
|
||||
|
||||
# Periodically close the connection to avoid load balancer timeouts
|
||||
# Client will automatically reconnect and resume receiving progress
|
||||
if i % 30 == 0 and i > 0:
|
||||
await ctx.close_sse_stream()
|
||||
|
||||
await do_expensive_work()
|
||||
|
||||
return "Done!"
|
||||
|
||||
# Configure with EventStore for resumability
|
||||
event_store = EventStore()
|
||||
app = mcp.http_app(
|
||||
event_store=event_store,
|
||||
retry_interval=2000, # Client reconnects after 2 seconds
|
||||
)
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. When `event_store` is configured, the server stores all events (progress updates, results) with unique IDs
|
||||
2. Calling `ctx.close_sse_stream()` gracefully closes the HTTP connection
|
||||
3. The client automatically reconnects with a `Last-Event-ID` header
|
||||
4. The server replays any events the client missed during the disconnection
|
||||
|
||||
The `retry_interval` parameter (in milliseconds) controls how long clients wait before reconnecting. Choose a value that balances responsiveness with server load.
|
||||
|
||||
<Note>
|
||||
`close_sse_stream()` is a no-op if called without an `EventStore` configured, so you can safely include it in tools that may run in different deployment configurations.
|
||||
</Note>
|
||||
|
||||
#### Custom Storage Backends
|
||||
|
||||
By default, `EventStore` uses in-memory storage. For production deployments with multiple server instances, you can provide a custom storage backend using the `key_value` package:
|
||||
|
||||
```python
|
||||
from fastmcp.server.event_store import EventStore
|
||||
from key_value.aio.stores.redis import RedisStore
|
||||
|
||||
# Use Redis for distributed deployments
|
||||
redis_store = RedisStore(url="redis://localhost:6379")
|
||||
event_store = EventStore(
|
||||
storage=redis_store,
|
||||
max_events_per_stream=100, # Keep last 100 events per stream
|
||||
ttl=3600, # Events expire after 1 hour
|
||||
)
|
||||
|
||||
app = mcp.http_app(event_store=event_store)
|
||||
```
|
||||
|
||||
## Integration with Web Frameworks
|
||||
|
||||
If you already have a web application running, you can add MCP capabilities by mounting a FastMCP server as a sub-application. This allows you to expose MCP tools alongside your existing API endpoints, sharing the same domain and infrastructure. The MCP server becomes just another route in your application, making it easy to manage and deploy.
|
||||
|
||||
### Mounting in Starlette
|
||||
|
||||
Mount your FastMCP server in a Starlette application:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
||||
# Create your FastMCP server
|
||||
mcp = FastMCP("MyServer")
|
||||
|
||||
@mcp.tool
|
||||
def analyze(data: str) -> dict:
|
||||
return {"result": f"Analyzed: {data}"}
|
||||
|
||||
# Create the ASGI app
|
||||
mcp_app = mcp.http_app(path='/mcp')
|
||||
|
||||
# Create a Starlette app and mount the MCP server
|
||||
app = Starlette(
|
||||
routes=[
|
||||
Mount("/mcp-server", app=mcp_app),
|
||||
# Add other routes as needed
|
||||
],
|
||||
lifespan=mcp_app.lifespan,
|
||||
)
|
||||
```
|
||||
|
||||
The MCP endpoint will be available at `/mcp-server/mcp/` of the resulting Starlette app.
|
||||
|
||||
<Warning>
|
||||
For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.
|
||||
</Warning>
|
||||
|
||||
#### Nested Mounts
|
||||
|
||||
You can create complex routing structures by nesting mounts:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
||||
# Create your FastMCP server
|
||||
mcp = FastMCP("MyServer")
|
||||
|
||||
# Create the ASGI app
|
||||
mcp_app = mcp.http_app(path='/mcp')
|
||||
|
||||
# Create nested application structure
|
||||
inner_app = Starlette(routes=[Mount("/inner", app=mcp_app)])
|
||||
app = Starlette(
|
||||
routes=[Mount("/outer", app=inner_app)],
|
||||
lifespan=mcp_app.lifespan,
|
||||
)
|
||||
```
|
||||
|
||||
In this setup, the MCP server is accessible at the `/outer/inner/mcp/` path.
|
||||
|
||||
### FastAPI Integration
|
||||
|
||||
For FastAPI-specific integration patterns including both mounting MCP servers into FastAPI apps and generating MCP servers from FastAPI apps, see the [FastAPI Integration guide](/integrations/fastapi).
|
||||
|
||||
Here's a quick example showing how to add MCP to an existing FastAPI application:
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from fastmcp import FastMCP
|
||||
|
||||
# Create your MCP server
|
||||
mcp = FastMCP("API Tools")
|
||||
|
||||
@mcp.tool
|
||||
def query_database(query: str) -> dict:
|
||||
"""Run a database query"""
|
||||
return {"result": "data"}
|
||||
|
||||
# Create the MCP ASGI app with path="/" since we'll mount at /mcp
|
||||
mcp_app = mcp.http_app(path="/")
|
||||
|
||||
# Create FastAPI app with MCP lifespan (required for session management)
|
||||
api = FastAPI(lifespan=mcp_app.lifespan)
|
||||
|
||||
@api.get("/api/status")
|
||||
def status():
|
||||
return {"status": "ok"}
|
||||
|
||||
# Mount MCP at /mcp
|
||||
api.mount("/mcp", mcp_app)
|
||||
|
||||
# Run with: uvicorn app:api --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
Your existing API remains at `http://localhost:8000/api` while MCP is available at `http://localhost:8000/mcp`.
|
||||
|
||||
<Warning>
|
||||
Just like with Starlette, you **must** pass the lifespan from the MCP app to FastAPI. Without this, the session manager won't initialize properly and requests will fail.
|
||||
</Warning>
|
||||
|
||||
## Mounting Authenticated Servers
|
||||
|
||||
<VersionBadge version="2.13.0" />
|
||||
|
||||
<Tip>
|
||||
This section only applies if you're **mounting an OAuth-protected FastMCP server under a path prefix** (like `/api`) inside another application using `Mount()`.
|
||||
|
||||
If you're deploying your FastMCP server at root level without any `Mount()` prefix, the well-known routes are automatically included in `mcp.http_app()` and you don't need to do anything special.
|
||||
</Tip>
|
||||
|
||||
OAuth specifications (RFC 8414 and RFC 9728) require discovery metadata to be accessible at well-known paths under the root level of your domain. When you mount an OAuth-protected FastMCP server under a path prefix like `/api`, this creates a routing challenge: your operational OAuth endpoints move under the prefix, but discovery endpoints must remain at the root.
|
||||
|
||||
<Warning>
|
||||
**Common Mistakes to Avoid:**
|
||||
|
||||
1. **Forgetting to mount `.well-known` routes at root** - FastMCP cannot do this automatically when your server is mounted under a path prefix. You must explicitly mount well-known routes at the root level.
|
||||
|
||||
2. **Including mount prefix in both base_url AND mcp_path** - The mount prefix (like `/api`) should only be in `base_url`, not in `mcp_path`. Otherwise you'll get double paths.
|
||||
|
||||
✅ **Correct:**
|
||||
```python
|
||||
base_url = "http://localhost:8000/api"
|
||||
mcp_path = "/mcp"
|
||||
# Result: /api/mcp
|
||||
```
|
||||
|
||||
❌ **Wrong:**
|
||||
```python
|
||||
base_url = "http://localhost:8000/api"
|
||||
mcp_path = "/api/mcp"
|
||||
# Result: /api/api/mcp (double prefix!)
|
||||
```
|
||||
|
||||
Follow the configuration instructions below to set up mounting correctly.
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
**CORS Middleware Conflicts:**
|
||||
|
||||
If you're integrating FastMCP into an existing application with its own CORS middleware, be aware that layering CORS middleware can cause conflicts (such as 404 errors on `.well-known` routes or OPTIONS requests).
|
||||
|
||||
FastMCP and the MCP SDK already handle CORS for OAuth routes. If you need CORS on your own application routes, consider using the sub-app pattern: mount FastMCP and your routes as separate apps, each with their own middleware, rather than adding application-wide CORS middleware.
|
||||
</Warning>
|
||||
|
||||
### Route Types
|
||||
|
||||
OAuth-protected MCP servers expose two categories of routes:
|
||||
|
||||
**Operational routes** handle the OAuth flow and MCP protocol:
|
||||
- `/authorize` - OAuth authorization endpoint
|
||||
- `/token` - Token exchange endpoint
|
||||
- `/auth/callback` - OAuth callback handler
|
||||
- `/mcp` - MCP protocol endpoint
|
||||
|
||||
**Discovery routes** provide metadata for OAuth clients:
|
||||
- `/.well-known/oauth-authorization-server` - Authorization server metadata
|
||||
- `/.well-known/oauth-protected-resource/*` - Protected resource metadata
|
||||
|
||||
When you mount your MCP app under a prefix, operational routes move with it, but discovery routes must stay at root level for RFC compliance.
|
||||
|
||||
### Configuration Parameters
|
||||
|
||||
Three parameters control where routes are located and how they combine:
|
||||
|
||||
**`base_url`** tells clients where to find operational endpoints. This includes any Starlette `Mount()` path prefix (e.g., `/api`):
|
||||
|
||||
```python
|
||||
base_url="http://localhost:8000/api" # Includes mount prefix
|
||||
```
|
||||
|
||||
**`mcp_path`** is the internal FastMCP endpoint path, which gets appended to `base_url`:
|
||||
|
||||
```python
|
||||
mcp_path="/mcp" # Internal MCP path, NOT the mount prefix
|
||||
```
|
||||
|
||||
**`issuer_url`** (optional) controls the authorization server identity for OAuth discovery. Defaults to `base_url`.
|
||||
|
||||
```python
|
||||
# Usually not needed - just set base_url and it works
|
||||
issuer_url="http://localhost:8000" # Only if you want root-level discovery
|
||||
```
|
||||
|
||||
When `issuer_url` has a path (either explicitly or by defaulting from `base_url`), FastMCP creates path-aware discovery routes per RFC 8414. For example, if `base_url` is `http://localhost:8000/api`, the authorization server metadata will be at `/.well-known/oauth-authorization-server/api`.
|
||||
|
||||
**Key Invariant:** `base_url + mcp_path = actual externally-accessible MCP URL`
|
||||
|
||||
Example:
|
||||
- `base_url`: `http://localhost:8000/api` (mount prefix `/api`)
|
||||
- `mcp_path`: `/mcp` (internal path)
|
||||
- Result: `http://localhost:8000/api/mcp` (final MCP endpoint)
|
||||
|
||||
Note that the mount prefix (`/api` from `Mount("/api", ...)`) goes in `base_url`, while `mcp_path` is just the internal MCP route. Don't include the mount prefix in both places or you'll get `/api/api/mcp`.
|
||||
|
||||
### Mounting Strategy
|
||||
|
||||
When mounting an OAuth-protected server under a path prefix, declare your URLs upfront to make the relationships clear:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.github import GitHubProvider
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
||||
# Define the routing structure
|
||||
ROOT_URL = "http://localhost:8000"
|
||||
MOUNT_PREFIX = "/api"
|
||||
MCP_PATH = "/mcp"
|
||||
```
|
||||
|
||||
Create the auth provider with `base_url`:
|
||||
|
||||
```python
|
||||
auth = GitHubProvider(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
base_url=f"{ROOT_URL}{MOUNT_PREFIX}", # Operational endpoints under prefix
|
||||
# issuer_url defaults to base_url - path-aware discovery works automatically
|
||||
)
|
||||
```
|
||||
|
||||
Create the MCP app, which generates operational routes at the specified path:
|
||||
|
||||
```python
|
||||
mcp = FastMCP("Protected Server", auth=auth)
|
||||
mcp_app = mcp.http_app(path=MCP_PATH)
|
||||
```
|
||||
|
||||
Retrieve the discovery routes from the auth provider. The `mcp_path` argument should match the path used when creating the MCP app:
|
||||
|
||||
```python
|
||||
well_known_routes = auth.get_well_known_routes(mcp_path=MCP_PATH)
|
||||
```
|
||||
|
||||
Finally, mount everything in the Starlette app with discovery routes at root and the MCP app under the prefix:
|
||||
|
||||
```python
|
||||
app = Starlette(
|
||||
routes=[
|
||||
*well_known_routes, # Discovery routes at root level
|
||||
Mount(MOUNT_PREFIX, app=mcp_app), # Operational routes under prefix
|
||||
],
|
||||
lifespan=mcp_app.lifespan,
|
||||
)
|
||||
```
|
||||
|
||||
This configuration produces the following URL structure:
|
||||
|
||||
- MCP endpoint: `http://localhost:8000/api/mcp`
|
||||
- OAuth authorization: `http://localhost:8000/api/authorize`
|
||||
- OAuth callback: `http://localhost:8000/api/auth/callback`
|
||||
- Authorization server metadata: `http://localhost:8000/.well-known/oauth-authorization-server/api`
|
||||
- Protected resource metadata: `http://localhost:8000/.well-known/oauth-protected-resource/api/mcp`
|
||||
|
||||
Both discovery endpoints use path-aware URLs per RFC 8414 and RFC 9728, matching the `base_url` path.
|
||||
|
||||
### Complete Example
|
||||
|
||||
Here's a complete working example showing all the pieces together:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.github import GitHubProvider
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
import uvicorn
|
||||
|
||||
# Define routing structure
|
||||
ROOT_URL = "http://localhost:8000"
|
||||
MOUNT_PREFIX = "/api"
|
||||
MCP_PATH = "/mcp"
|
||||
|
||||
# Create OAuth provider
|
||||
auth = GitHubProvider(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
base_url=f"{ROOT_URL}{MOUNT_PREFIX}",
|
||||
# issuer_url defaults to base_url - path-aware discovery works automatically
|
||||
)
|
||||
|
||||
# Create MCP server
|
||||
mcp = FastMCP("Protected Server", auth=auth)
|
||||
|
||||
@mcp.tool
|
||||
def analyze(data: str) -> dict:
|
||||
return {"result": f"Analyzed: {data}"}
|
||||
|
||||
# Create MCP app
|
||||
mcp_app = mcp.http_app(path=MCP_PATH)
|
||||
|
||||
# Get discovery routes for root level
|
||||
well_known_routes = auth.get_well_known_routes(mcp_path=MCP_PATH)
|
||||
|
||||
# Assemble the application
|
||||
app = Starlette(
|
||||
routes=[
|
||||
*well_known_routes,
|
||||
Mount(MOUNT_PREFIX, app=mcp_app),
|
||||
],
|
||||
lifespan=mcp_app.lifespan,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
For more details on OAuth authentication, see the [Authentication guide](/servers/auth/authentication).
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Running with Uvicorn
|
||||
|
||||
When deploying to production, you'll want to optimize your server for performance and reliability. Uvicorn provides several options to improve your server's capabilities:
|
||||
|
||||
```bash
|
||||
# Run with basic configuration
|
||||
uvicorn app:app --host 0.0.0.0 --port 8000
|
||||
|
||||
# Run with multiple workers for production (requires stateless mode - see below)
|
||||
uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4
|
||||
```
|
||||
|
||||
### Horizontal Scaling
|
||||
|
||||
<VersionBadge version="2.10.2" />
|
||||
|
||||
When deploying FastMCP behind a load balancer or running multiple server instances, you need to understand how the HTTP transport handles sessions and configure your server appropriately.
|
||||
|
||||
#### Understanding Sessions
|
||||
|
||||
By default, FastMCP's Streamable HTTP transport maintains server-side sessions. Sessions enable stateful MCP features like [elicitation](/servers/elicitation) and [sampling](/servers/sampling), where the server needs to maintain context across multiple requests from the same client.
|
||||
|
||||
This works perfectly for single-instance deployments. However, sessions are stored in memory on each server instance, which creates challenges when scaling horizontally.
|
||||
|
||||
#### Without Stateless Mode
|
||||
|
||||
When running multiple server instances behind a load balancer (Traefik, nginx, HAProxy, Kubernetes, etc.), requests from the same client may be routed to different instances:
|
||||
|
||||
1. Client connects to Instance A → session created on Instance A
|
||||
2. Next request routes to Instance B → session doesn't exist → **request fails**
|
||||
|
||||
You might expect sticky sessions (session affinity) to solve this, but they don't work reliably with MCP clients.
|
||||
|
||||
<Warning>
|
||||
**Why sticky sessions don't work:** Most MCP clients—including Cursor and Claude Code—use `fetch()` internally and don't properly forward `Set-Cookie` headers. Without cookies, load balancers can't identify which instance should handle subsequent requests. This is a limitation in how these clients implement HTTP, not something you can fix with load balancer configuration.
|
||||
</Warning>
|
||||
|
||||
#### Enabling Stateless Mode
|
||||
|
||||
For horizontally scaled deployments, enable stateless HTTP mode. In stateless mode, each request creates a fresh transport context, eliminating the need for session affinity entirely.
|
||||
|
||||
**Option 1: Via `http_app()`**
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
|
||||
@mcp.tool
|
||||
def process(data: str) -> str:
|
||||
return f"Processed: {data}"
|
||||
|
||||
app = mcp.http_app(stateless_http=True)
|
||||
```
|
||||
|
||||
**Option 2: Via `run()`**
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="http", stateless_http=True)
|
||||
```
|
||||
|
||||
**Option 3: Via environment variable**
|
||||
|
||||
```bash
|
||||
FASTMCP_STATELESS_HTTP=true uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Production deployments should never hardcode sensitive information like API keys or authentication tokens. Instead, use environment variables to configure your server at runtime. This keeps your code secure and makes it easy to deploy the same code to different environments with different configurations.
|
||||
|
||||
Here's an example using static token authentication for development (OAuth is recommended for production):
|
||||
|
||||
```python
|
||||
import os
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth import StaticTokenVerifier
|
||||
|
||||
# Read configuration from environment
|
||||
auth_token = os.environ.get("MCP_AUTH_TOKEN")
|
||||
if auth_token:
|
||||
auth = StaticTokenVerifier(tokens={auth_token: {"sub": "admin", "client_id": "cli"}})
|
||||
mcp = FastMCP("Production Server", auth=auth)
|
||||
else:
|
||||
mcp = FastMCP("Production Server")
|
||||
|
||||
app = mcp.http_app()
|
||||
```
|
||||
|
||||
Deploy with your secrets safely stored in environment variables:
|
||||
```bash
|
||||
MCP_AUTH_TOKEN=secret uvicorn app:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### OAuth Token Security
|
||||
|
||||
<VersionBadge version="2.13.0" />
|
||||
|
||||
If you're using the [OAuth Proxy](/servers/auth/oauth-proxy), FastMCP issues its own JWT tokens to clients instead of forwarding upstream provider tokens. This maintains proper OAuth 2.0 token boundaries.
|
||||
|
||||
**Default Behavior (Development Only):**
|
||||
|
||||
By default, FastMCP automatically manages cryptographic keys:
|
||||
- **Mac/Windows**: Keys are generated and stored in your system keyring, surviving server restarts. Suitable **only** for development and local testing.
|
||||
- **Linux**: Keys are ephemeral (random salt at startup), so tokens are invalidated on restart.
|
||||
|
||||
This automatic approach is convenient for development but not suitable for production deployments.
|
||||
|
||||
**For Production:**
|
||||
|
||||
Production requires explicit key management to ensure tokens survive restarts and can be shared across multiple server instances. This requires the following two things working together:
|
||||
|
||||
1. **Explicit JWT signing key** for signing tokens issued to clients
|
||||
3. **Persistent network-accessible storage** for upstream tokens (wrapped in `FernetEncryptionWrapper` to encrypt sensitive data at rest)
|
||||
|
||||
**Configuration:**
|
||||
|
||||
Add two parameters to your auth provider:
|
||||
|
||||
```python {8-12}
|
||||
from key_value.aio.stores.redis import RedisStore
|
||||
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
auth = GitHubProvider(
|
||||
client_id=os.environ["GITHUB_CLIENT_ID"],
|
||||
client_secret=os.environ["GITHUB_CLIENT_SECRET"],
|
||||
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
|
||||
client_storage=FernetEncryptionWrapper(
|
||||
key_value=RedisStore(host="redis.example.com", port=6379),
|
||||
fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
|
||||
),
|
||||
base_url="https://your-server.com" # use HTTPS
|
||||
)
|
||||
```
|
||||
|
||||
Both parameters are required for production. Without an explicit signing key, keys are signed using a key derived from the client_secret, which will cause invalidation upon rotation of the client secret. Without persistent storage, tokens are local to the server and won't be trusted across hosts. **Wrap your storage backend in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without encryption, tokens are stored in plaintext.
|
||||
|
||||
For more details on the token architecture and key management, see [OAuth Proxy Key and Storage Management](/servers/auth/oauth-proxy#key-and-storage-management).
|
||||
|
||||
## Reverse Proxy (nginx)
|
||||
|
||||
In production, you'll typically run your FastMCP server behind a reverse proxy like nginx. A reverse proxy provides TLS termination, domain-based routing, static file serving, and an additional layer of security between the internet and your application.
|
||||
|
||||
### Running FastMCP as a Linux Service
|
||||
|
||||
Before configuring nginx, you need your FastMCP server running as a background service. A systemd unit file ensures your server starts automatically and restarts on failure.
|
||||
|
||||
Create a file at `/etc/systemd/system/fastmcp.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=FastMCP Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/opt/fastmcp
|
||||
ExecStart=/opt/fastmcp/.venv/bin/uvicorn app:app --host 127.0.0.1 --port 8000
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment="PATH=/opt/fastmcp/.venv/bin"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Enable and start the service:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable fastmcp
|
||||
sudo systemctl start fastmcp
|
||||
```
|
||||
|
||||
This assumes your ASGI application is in `/opt/fastmcp/app.py` with a virtual environment at `/opt/fastmcp/.venv`. Adjust paths to match your deployment layout.
|
||||
|
||||
### nginx Configuration
|
||||
|
||||
FastMCP's Streamable HTTP transport uses Server-Sent Events (SSE) for streaming responses. This requires specific nginx settings to prevent buffering from breaking the event stream.
|
||||
|
||||
Create a site configuration at `/etc/nginx/sites-available/fastmcp`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name mcp.example.com;
|
||||
|
||||
# Redirect HTTP to HTTPS
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name mcp.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection '';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Required for SSE (Server-Sent Events) streaming
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
|
||||
# Allow long-lived connections for streaming responses
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Enable the site and reload nginx:
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/fastmcp /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
Your FastMCP server is now accessible at `https://mcp.example.com/mcp`.
|
||||
|
||||
<Warning>
|
||||
**SSE buffering is the most common issue.** If clients connect but never receive streaming responses (progress updates, tool results), verify that `proxy_buffering off` is set. Without it, nginx buffers the entire SSE stream and delivers it only when the connection closes, which breaks real-time communication.
|
||||
</Warning>
|
||||
|
||||
### Key Considerations
|
||||
|
||||
When deploying FastMCP behind a reverse proxy, keep these points in mind:
|
||||
|
||||
- **Disable buffering**: SSE requires `proxy_buffering off` so events reach clients immediately. This is the single most important setting.
|
||||
- **Increase timeouts**: The default nginx `proxy_read_timeout` is 60 seconds. Long-running MCP tools will cause the connection to drop. Set timeouts to at least 300 seconds, or higher if your tools run longer. For tools that may exceed any timeout, use [SSE Polling](#sse-polling-for-long-running-operations) to gracefully handle proxy disconnections.
|
||||
- **Use HTTP/1.1**: Set `proxy_http_version 1.1` and `proxy_set_header Connection ''` to enable keep-alive connections between nginx and your server. Clearing the `Connection` header prevents clients from sending `Connection: close` to your upstream, which would break SSE streams. Both settings are required for proper SSE support.
|
||||
- **Forward headers**: Pass `X-Forwarded-For` and `X-Forwarded-Proto` so your FastMCP server can determine the real client IP and protocol. This is important for logging and for OAuth redirect URLs.
|
||||
- **TLS termination**: Let nginx handle TLS certificates (e.g., via Let's Encrypt with Certbot). Your FastMCP server can then run on plain HTTP internally.
|
||||
|
||||
### Mounting Under a Path Prefix
|
||||
|
||||
If you want your MCP server available at a subpath like `https://example.com/api/mcp` instead of at the root domain, adjust the nginx `location` block:
|
||||
|
||||
```nginx
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection '';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Required for SSE streaming
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
```
|
||||
|
||||
Note the trailing `/` on both `location /api/` and `proxy_pass http://127.0.0.1:8000/` — this ensures nginx strips the `/api` prefix before forwarding to your server. If you're using OAuth authentication with a mount prefix, see [Mounting Authenticated Servers](#mounting-authenticated-servers) for additional configuration.
|
||||
|
||||
## Testing Your Deployment
|
||||
|
||||
Once your server is deployed, you'll need to verify it's accessible and functioning correctly. For comprehensive testing strategies including connectivity tests, client testing, and authentication testing, see the [Testing Your Server](/development/tests) guide.
|
||||
|
||||
## Hosting Your Server
|
||||
|
||||
This guide has shown you how to create an HTTP-accessible MCP server, but you'll still need a hosting provider to make it available on the internet. Your FastMCP server can run anywhere that supports Python web applications:
|
||||
|
||||
- **Cloud VMs** (AWS EC2, Google Compute Engine, Azure VMs)
|
||||
- **Container platforms** (Cloud Run, Container Instances, ECS)
|
||||
- **Platform-as-a-Service** (Railway, Render, Vercel)
|
||||
- **Edge platforms** (Cloudflare Workers)
|
||||
- **Kubernetes clusters** (self-managed or managed)
|
||||
|
||||
The key requirements are Python 3.10+ support and the ability to expose an HTTP port. Most providers will require you to package your server (requirements.txt, Dockerfile, etc.) according to their deployment format. For managed, zero-configuration deployment, see [Prefect Horizon](/deployment/prefect-horizon).
|
||||
120
docs/v3/deployment/prefect-horizon.mdx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
---
|
||||
title: Prefect Horizon
|
||||
sidebarTitle: Prefect Horizon
|
||||
description: The MCP platform from the FastMCP team
|
||||
icon: cloud
|
||||
---
|
||||
|
||||
[Prefect Horizon](https://www.prefect.io/horizon) is a platform for deploying and managing MCP servers. Built by the FastMCP team at [Prefect](https://www.prefect.io), Horizon provides managed hosting, authentication, access control, and a registry of MCP capabilities.
|
||||
|
||||
Horizon includes a **free personal tier for FastMCP users**, making it the fastest way to get a secure, production-ready server URL with built-in OAuth authentication.
|
||||
|
||||
<Info>
|
||||
Horizon is free for personal projects. Enterprise governance features are available for teams deploying to thousands of users.
|
||||
</Info>
|
||||
|
||||
## The Platform
|
||||
|
||||
Horizon is organized into four integrated pillars:
|
||||
|
||||
- **Deploy**: Managed hosting with CI/CD, scaling, monitoring, and rollbacks. Push code and get a live, governed endpoint in 60 seconds.
|
||||
- **Registry**: A central catalog of MCP servers across your organization—first-party, third-party, and curated remix servers composed from multiple sources.
|
||||
- **Gateway**: Role-based access control, authentication, and audit logs. Define what agents can see and do at the tool level.
|
||||
- **Agents**: A permissioned chat interface for interacting with any MCP server or curated combination of servers.
|
||||
|
||||
This guide focuses on **Horizon Deploy**, the managed hosting layer that gives you the fastest path from a FastMCP server to a production URL.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To use Horizon, you'll need a [GitHub](https://github.com) account and a GitHub repo containing a FastMCP server. If you don't have one yet, Horizon can create a starter repo for you during onboarding.
|
||||
|
||||
Your repo can be public or private, but must include at least a Python file containing a FastMCP server instance.
|
||||
|
||||
<Tip>
|
||||
To verify your file is compatible with Horizon, run `fastmcp inspect <file.py:server_object>` to see what Horizon will see when it runs your server.
|
||||
</Tip>
|
||||
|
||||
If you have a `requirements.txt` or `pyproject.toml` in the repo, Horizon will automatically detect your server's dependencies and install them. Your file *can* have an `if __name__ == "__main__"` block, but it will be ignored by Horizon.
|
||||
|
||||
For example, a minimal server file might look like:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("MyServer")
|
||||
|
||||
@mcp.tool
|
||||
def hello(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
There are just three steps to deploying a server to Horizon:
|
||||
|
||||
### Step 1: Select a Repository
|
||||
|
||||
Visit [horizon.prefect.io](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) and sign in with your GitHub account. Connect your GitHub account to grant Horizon access to your repositories, then select the repo you want to deploy.
|
||||
|
||||
<img src="/assets/images/horizon/select-repo.png" alt="Horizon repository selection" />
|
||||
|
||||
### Step 2: Configure Your Server
|
||||
|
||||
Next, you'll configure how Horizon should build and deploy your server.
|
||||
|
||||
<img src="/assets/images/horizon/configure-server.png" alt="Horizon server configuration" />
|
||||
|
||||
The configuration screen lets you specify:
|
||||
- **Server name**: A unique name for your server. This determines your server's URL.
|
||||
- **Description**: A brief description of what your server does.
|
||||
- **Entrypoint**: The Python file containing your FastMCP server (e.g., `main.py`). This field has the same syntax as the `fastmcp run` command—use `main.py:mcp` to specify a specific object in the file.
|
||||
- **Authentication**: When enabled, only authenticated users in your organization can connect. Horizon handles all the OAuth complexity for you.
|
||||
|
||||
Horizon will automatically detect your server's Python dependencies from either a `requirements.txt` or `pyproject.toml` file.
|
||||
|
||||
### Step 3: Deploy and Connect
|
||||
|
||||
Click **Deploy Server** and Horizon will clone your repository, build your server, and deploy it to a unique URL—typically in under 60 seconds.
|
||||
|
||||
<img src="/assets/images/horizon/deployment-live.png" alt="Horizon deployment view showing live server" />
|
||||
|
||||
Once deployed, your server is accessible at a URL like:
|
||||
|
||||
```
|
||||
https://your-server-name.fastmcp.app/mcp
|
||||
```
|
||||
|
||||
Horizon monitors your repo and redeploys automatically whenever you push to `main`. It also builds preview deployments for every PR, so you can test changes before they go live.
|
||||
|
||||
## Testing Your Server
|
||||
|
||||
Horizon provides two ways to verify your server is working before connecting external clients.
|
||||
|
||||
### Inspector
|
||||
|
||||
The Inspector gives you a structured view of everything your server exposes—tools, resources, and prompts. You can click any tool, fill in the inputs, execute it, and see the output. This is useful for systematically validating each capability and debugging specific behaviors.
|
||||
|
||||
### ChatMCP
|
||||
|
||||
For quick end-to-end testing, ChatMCP lets you interact with your server conversationally. It uses a fast model optimized for rapid iteration—you can verify the server works, test tool calls in context, and confirm the overall behavior before sharing it with others.
|
||||
|
||||
<img src="/assets/images/horizon/chat.png" alt="Horizon ChatMCP interface" />
|
||||
|
||||
ChatMCP is designed for testing, not as a daily work environment. Once you've confirmed your server works, you can copy connection snippets for Claude Desktop, Cursor, Claude Code, and other MCP clients—or use the FastMCP client library to connect programmatically.
|
||||
|
||||
## Horizon Agents
|
||||
|
||||
Beyond testing individual servers, Horizon lets you create **Agents**—chat interfaces backed by one or more MCP servers. While ChatMCP tests a single server, Agents let you compose capabilities from multiple servers into a unified experience.
|
||||
|
||||
<img src="/assets/images/horizon/agent-detail.png" alt="Horizon Agent configuration" />
|
||||
|
||||
To create an agent:
|
||||
1. Navigate to **Agents** in the sidebar
|
||||
2. Click **Create Agent** and give it a name and description
|
||||
3. Add MCP servers to the agent—these can be servers you've deployed to Horizon or external servers in the registry
|
||||
|
||||
Once configured, you can chat with your agent directly in Horizon:
|
||||
|
||||
<img src="/assets/images/horizon/agent-chat.png" alt="Chatting with a Horizon Agent" />
|
||||
|
||||
Agents are useful for creating purpose-built interfaces that combine tools from different servers. For example, you might create an agent that has access to both your company's internal data server and a general-purpose utilities server.
|
||||
286
docs/v3/deployment/running-server.mdx
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
---
|
||||
title: Running Your Server
|
||||
sidebarTitle: Running Your Server
|
||||
description: Learn how to run your FastMCP server locally for development and testing
|
||||
icon: circle-play
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
FastMCP servers can be run in different ways depending on your needs. This guide focuses on running servers locally for development and testing. For production deployment to a URL, see the [HTTP Deployment](/deployment/http) guide.
|
||||
|
||||
## The `run()` Method
|
||||
|
||||
Every FastMCP server needs to be started to accept connections. The simplest way to run a server is by calling the `run()` method on your FastMCP instance. This method starts the server and blocks until it's stopped, handling all the connection management for you.
|
||||
|
||||
<Tip>
|
||||
For maximum compatibility, it's best practice to place the `run()` call within an `if __name__ == "__main__":` block. This ensures the server starts only when the script is executed directly, not when imported as a module.
|
||||
</Tip>
|
||||
|
||||
```python {9-10} my_server.py
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP(name="MyServer")
|
||||
|
||||
@mcp.tool
|
||||
def hello(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
You can now run this MCP server by executing `python my_server.py`.
|
||||
|
||||
## Transport Protocols
|
||||
|
||||
MCP servers communicate with clients through different transport protocols. Think of transports as the "language" your server speaks to communicate with clients. FastMCP supports three main transport protocols, each designed for specific use cases and deployment scenarios.
|
||||
|
||||
The choice of transport determines how clients connect to your server, what network capabilities are available, and how many clients can connect simultaneously. Understanding these transports helps you choose the right approach for your application.
|
||||
|
||||
### STDIO Transport (Default)
|
||||
|
||||
STDIO (Standard Input/Output) is the default transport for FastMCP servers. When you call `run()` without arguments, your server uses STDIO transport. This transport communicates through standard input and output streams, making it perfect for command-line tools and desktop applications like Claude Desktop.
|
||||
|
||||
With STDIO transport, the client spawns a new server process for each session and manages its lifecycle. The server reads MCP messages from stdin and writes responses to stdout. This is why STDIO servers don't stay running - they're started on-demand by the client.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("MyServer")
|
||||
|
||||
@mcp.tool
|
||||
def hello(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run() # Uses STDIO transport by default
|
||||
```
|
||||
|
||||
STDIO is ideal for:
|
||||
- Local development and testing
|
||||
- Claude Desktop integration
|
||||
- Command-line tools
|
||||
- Single-user applications
|
||||
|
||||
### HTTP Transport (Streamable)
|
||||
|
||||
HTTP transport turns your MCP server into a web service accessible via a URL. This transport uses the Streamable HTTP protocol, which allows clients to connect over the network. Unlike STDIO where each client gets its own process, an HTTP server can handle multiple clients simultaneously.
|
||||
|
||||
The Streamable HTTP protocol provides full bidirectional communication between client and server, supporting all MCP operations including streaming responses. This makes it the recommended choice for network-based deployments.
|
||||
|
||||
To use HTTP transport, specify it in the `run()` method along with networking options:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("MyServer")
|
||||
|
||||
@mcp.tool
|
||||
def hello(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Start an HTTP server on port 8000
|
||||
mcp.run(transport="http", host="127.0.0.1", port=8000)
|
||||
```
|
||||
|
||||
Your server is now accessible at `http://localhost:8000/mcp`. This URL is the MCP endpoint that clients will connect to. HTTP transport enables:
|
||||
- Network accessibility
|
||||
- Multiple concurrent clients
|
||||
- Integration with web infrastructure
|
||||
- Remote deployment capabilities
|
||||
|
||||
For production HTTP deployment with authentication and advanced configuration, see the [HTTP Deployment](/deployment/http) guide.
|
||||
|
||||
### SSE Transport (Legacy)
|
||||
|
||||
Server-Sent Events (SSE) transport was the original HTTP-based transport for MCP. While still supported for backward compatibility, it has limitations compared to the newer Streamable HTTP transport. SSE only supports server-to-client streaming, making it less efficient for bidirectional communication.
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
# SSE transport - use HTTP instead for new projects
|
||||
mcp.run(transport="sse", host="127.0.0.1", port=8000)
|
||||
```
|
||||
|
||||
We recommend using HTTP transport instead of SSE for all new projects. SSE remains available only for compatibility with older clients that haven't upgraded to Streamable HTTP.
|
||||
|
||||
### Choosing the Right Transport
|
||||
|
||||
Each transport serves different needs. STDIO is perfect when you need simple, local execution - it's what Claude Desktop and most command-line tools expect. HTTP transport is essential when you need network access, want to serve multiple clients, or plan to deploy your server remotely. SSE exists only for backward compatibility and shouldn't be used in new projects.
|
||||
|
||||
Consider your deployment scenario: Are you building a tool for local use? STDIO is your best choice. Need a centralized service that multiple clients can access? HTTP transport is the way to go.
|
||||
|
||||
## The FastMCP CLI
|
||||
|
||||
FastMCP provides a powerful command-line interface for running servers without modifying the source code. The CLI can automatically find and run your server with different transports, manage dependencies, and handle development workflows:
|
||||
|
||||
```bash
|
||||
fastmcp run server.py
|
||||
```
|
||||
|
||||
The CLI automatically finds a FastMCP instance in your file (named `mcp`, `server`, or `app`) and runs it with the specified options. This is particularly useful for testing different transports or configurations without changing your code.
|
||||
|
||||
### Dependency Management
|
||||
|
||||
The CLI integrates with `uv` to manage Python environments and dependencies:
|
||||
|
||||
```bash
|
||||
# Run with a specific Python version
|
||||
fastmcp run server.py --python 3.11
|
||||
|
||||
# Run with additional packages
|
||||
fastmcp run server.py --with pandas --with numpy
|
||||
|
||||
# Run with dependencies from a requirements file
|
||||
fastmcp run server.py --with-requirements requirements.txt
|
||||
|
||||
# Combine multiple options
|
||||
fastmcp run server.py --python 3.10 --with httpx --transport http
|
||||
|
||||
# Run within a specific project directory
|
||||
fastmcp run server.py --project /path/to/project
|
||||
```
|
||||
|
||||
<Note>
|
||||
When using `--python`, `--with`, `--project`, or `--with-requirements`, the server runs via `uv run` subprocess instead of using your local environment.
|
||||
</Note>
|
||||
|
||||
### Passing Arguments to Servers
|
||||
|
||||
When servers accept command line arguments (using argparse, click, or other libraries), you can pass them after `--`:
|
||||
|
||||
```bash
|
||||
fastmcp run config_server.py -- --config config.json
|
||||
fastmcp run database_server.py -- --database-path /tmp/db.sqlite --debug
|
||||
```
|
||||
|
||||
This is useful for servers that need configuration files, database paths, API keys, or other runtime options.
|
||||
|
||||
For more CLI features including development mode with the MCP Inspector, see the [CLI documentation](/cli/running).
|
||||
|
||||
### Auto-Reload for Development
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
During development, you can use the `--reload` flag to automatically restart your server when source files change:
|
||||
|
||||
```bash
|
||||
fastmcp run server.py --reload
|
||||
```
|
||||
|
||||
The server watches for changes to Python files in the current directory and restarts automatically when you save changes. This provides a fast feedback loop during development without manually stopping and starting the server.
|
||||
|
||||
```bash
|
||||
# Watch specific directories for changes
|
||||
fastmcp run server.py --reload --reload-dir ./src --reload-dir ./lib
|
||||
|
||||
# Combine with other options
|
||||
fastmcp run server.py --reload --transport http --port 8080
|
||||
```
|
||||
|
||||
<Note>
|
||||
Auto-reload uses stateless mode to enable seamless restarts. For stdio transport, this is fully featured. For HTTP transport, some bidirectional features like elicitation are not available during reload mode.
|
||||
</Note>
|
||||
|
||||
SSE transport does not support auto-reload due to session limitations. Use HTTP transport instead if you need both network access and auto-reload.
|
||||
|
||||
### Async Usage
|
||||
|
||||
FastMCP servers are built on async Python, but the framework provides both synchronous and asynchronous APIs to fit your application's needs. The `run()` method we've been using is actually a synchronous wrapper around the async server implementation.
|
||||
|
||||
For applications that are already running in an async context, FastMCP provides the `run_async()` method:
|
||||
|
||||
```python {10-12}
|
||||
from fastmcp import FastMCP
|
||||
import asyncio
|
||||
|
||||
mcp = FastMCP(name="MyServer")
|
||||
|
||||
@mcp.tool
|
||||
def hello(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
async def main():
|
||||
# Use run_async() in async contexts
|
||||
await mcp.run_async(transport="http", port=8000)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
<Warning>
|
||||
The `run()` method cannot be called from inside an async function because it creates its own async event loop internally. If you attempt to call `run()` from inside an async function, you'll get an error about the event loop already running.
|
||||
|
||||
Always use `run_async()` inside async functions and `run()` in synchronous contexts.
|
||||
</Warning>
|
||||
|
||||
Both `run()` and `run_async()` accept the same transport arguments, so all the examples above apply to both methods.
|
||||
|
||||
## Custom Routes
|
||||
|
||||
When using HTTP transport, you might want to add custom web endpoints alongside your MCP server. This is useful for health checks, status pages, or simple APIs. FastMCP lets you add custom routes using the `@custom_route` decorator:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import PlainTextResponse
|
||||
|
||||
mcp = FastMCP("MyServer")
|
||||
|
||||
@mcp.custom_route("/health", methods=["GET"])
|
||||
async def health_check(request: Request) -> PlainTextResponse:
|
||||
return PlainTextResponse("OK")
|
||||
|
||||
@mcp.tool
|
||||
def process(data: str) -> str:
|
||||
return f"Processed: {data}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="http") # Health check at http://localhost:8000/health
|
||||
```
|
||||
|
||||
Custom routes are served by the same web server as your MCP endpoint. They're available at the root of your domain while the MCP endpoint is at `/mcp/`. For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/http#integration-with-web-frameworks).
|
||||
|
||||
## Alternative Initialization Patterns
|
||||
|
||||
The `if __name__ == "__main__"` pattern works well for standalone scripts, but some deployment scenarios require different approaches. FastMCP handles these cases automatically.
|
||||
|
||||
### CLI-Only Servers
|
||||
|
||||
When using the FastMCP CLI, you don't need the `if __name__` block at all. The CLI will find your FastMCP instance and run it:
|
||||
|
||||
```python
|
||||
# server.py
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("MyServer") # CLI looks for 'mcp', 'server', or 'app'
|
||||
|
||||
@mcp.tool
|
||||
def process(data: str) -> str:
|
||||
return f"Processed: {data}"
|
||||
|
||||
# No if __name__ block needed - CLI will find and run 'mcp'
|
||||
```
|
||||
|
||||
### ASGI Applications
|
||||
|
||||
For ASGI deployment (running with Uvicorn or similar), you'll want to create an ASGI application object. This approach is common in production deployments where you need more control over the server configuration:
|
||||
|
||||
```python
|
||||
# app.py
|
||||
from fastmcp import FastMCP
|
||||
|
||||
def create_app():
|
||||
mcp = FastMCP("MyServer")
|
||||
|
||||
@mcp.tool
|
||||
def process(data: str) -> str:
|
||||
return f"Processed: {data}"
|
||||
|
||||
return mcp.http_app()
|
||||
|
||||
app = create_app() # Uvicorn will use this
|
||||
```
|
||||
|
||||
See the [HTTP Deployment](/deployment/http) guide for more ASGI deployment patterns.
|
||||
262
docs/v3/deployment/sandboxed-agents.mdx
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
---
|
||||
title: Sandboxed Agents
|
||||
sidebarTitle: Sandboxed Agents
|
||||
description: Expose MCP tools to isolated agents without giving the sandbox long-lived credentials.
|
||||
icon: box-open
|
||||
---
|
||||
|
||||
This guide is for deployments where an agent runs inside an isolated container, subprocess, or remote worker and still needs MCP access. In that setup, the sandbox itself becomes part of your trust boundary.
|
||||
|
||||
The core recommendation is simple: use FastMCP as the capability boundary. Run a remote FastMCP server, authenticate the sandbox with short-lived scoped credentials, and keep privileged credentials on the server side.
|
||||
|
||||
## When to Use This Pattern
|
||||
|
||||
This pattern is useful when:
|
||||
|
||||
- your agent runs in an ephemeral container or subprocess
|
||||
- you do not want long-lived credentials inside that sandbox
|
||||
- you need per-run, per-tenant, or per-job scoping
|
||||
- the sandbox must call internal APIs, databases, or upstream MCP servers indirectly
|
||||
|
||||
If you are building a local desktop integration, STDIO and normal local configuration may be enough. This guide is for cases where the sandbox is isolated enough that secret distribution, credential lifetimes, and privilege boundaries become part of the design.
|
||||
|
||||
## What Changes in a Sandboxed Deployment
|
||||
|
||||
A desktop MCP client usually runs on a developer's machine and launches local servers with configuration the developer controls. A sandboxed agent is different:
|
||||
|
||||
- It often runs in an ephemeral container or subprocess.
|
||||
- Its filesystem may be inspected after the fact.
|
||||
- Its environment variables may be broader than you intend.
|
||||
- You may launch many sandboxes concurrently for different users, tenants, or jobs.
|
||||
|
||||
That means convenience patterns that are acceptable locally become risky in sandboxes. Passing a GitHub token, database password, or cloud credentials directly into the sandbox creates a secret distribution problem you do not need to have.
|
||||
|
||||
The safer approach is to make your FastMCP server the only component with privileged access and let the sandbox call it over MCP.
|
||||
|
||||
## Recommended Architecture
|
||||
|
||||
Use this shape by default:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Sandboxed agent"] -->|"short-lived token"| B["FastMCP server"]
|
||||
B --> C["internal APIs"]
|
||||
B --> D["databases"]
|
||||
B --> E["other MCP servers"]
|
||||
```
|
||||
|
||||
The sandbox gets:
|
||||
|
||||
- the MCP server URL
|
||||
- a short-lived token scoped to its job, tenant, or run
|
||||
- no long-lived upstream credentials
|
||||
|
||||
The FastMCP server does the privileged work:
|
||||
|
||||
- verifies the sandbox token
|
||||
- authorizes the request from token claims, scopes, or other server-side policy
|
||||
- exposes only the tools that sandbox should see
|
||||
- talks to internal APIs, databases, or upstream MCP servers on the sandbox's behalf
|
||||
|
||||
The key design rule is simple:
|
||||
|
||||
<Tip>
|
||||
Give the sandbox capabilities, not credentials.
|
||||
</Tip>
|
||||
|
||||
With that boundary in place, the next questions are how the sandbox connects, how the server verifies and authorizes it, and how you design the tools the sandbox is allowed to call.
|
||||
|
||||
## Prefer HTTP for Sandboxed Agents
|
||||
|
||||
For sandboxes, prefer a remote HTTP server over a local STDIO server.
|
||||
|
||||
STDIO is still excellent for local development, but a remote HTTP server is usually the better production boundary for sandboxed agents because:
|
||||
|
||||
- authentication is explicit
|
||||
- the server lifecycle is independent from the sandbox lifecycle
|
||||
- secrets stay on the server
|
||||
- one deployment can safely serve many sandboxes
|
||||
- auditing and revocation happen in one place
|
||||
|
||||
This means the sandbox should connect as a client:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import BearerAuth
|
||||
|
||||
client = Client(
|
||||
"https://sandbox-tools.example.com/mcp",
|
||||
auth=BearerAuth("short-lived-sandbox-token"),
|
||||
)
|
||||
```
|
||||
|
||||
And your FastMCP server should run remotely:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Sandbox Tools")
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="http", host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
For production transport setup, see [HTTP Deployment](/deployment/http).
|
||||
|
||||
## Use Short-Lived, Scoped Credentials
|
||||
|
||||
For sandboxed agents, it is usually cleaner to issue credentials for the sandbox session than to place long-lived upstream credentials directly inside the container.
|
||||
|
||||
In practice, that usually means issuing a short-lived bearer token for each sandbox, run, or tenant and validating it on your FastMCP server with a token verifier.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
||||
|
||||
auth = JWTVerifier(
|
||||
jwks_uri="https://auth.example.com/.well-known/jwks.json",
|
||||
issuer="https://auth.example.com",
|
||||
audience="sandbox-mcp",
|
||||
)
|
||||
|
||||
mcp = FastMCP("Sandbox Tools", auth=auth)
|
||||
```
|
||||
|
||||
The token should identify the sandbox's scope. Depending on your system, it may represent a job, a tenant, a run, or a user-authorized session. Useful claims often include:
|
||||
|
||||
- sandbox or run id
|
||||
- tenant or installation id
|
||||
- user or actor id when applicable
|
||||
- expiration
|
||||
- optional capability scopes
|
||||
|
||||
Avoid shared static tokens across many sandboxes. If one sandbox token leaks, you want the blast radius to be small and the lifetime to be short.
|
||||
|
||||
Token verification is only one half of the boundary. Authorization still belongs on the FastMCP server: use scopes, claims, middleware, or custom auth checks to decide which tools and resources that sandbox can actually access.
|
||||
|
||||
For example, you can verify the token globally and still require a narrower scope on a specific tool:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth import require_scopes
|
||||
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
||||
|
||||
auth = JWTVerifier(
|
||||
jwks_uri="https://auth.example.com/.well-known/jwks.json",
|
||||
issuer="https://auth.example.com",
|
||||
audience="sandbox-mcp",
|
||||
)
|
||||
|
||||
mcp = FastMCP("Sandbox Tools", auth=auth)
|
||||
|
||||
@mcp.tool(auth=require_scopes("write:summary"))
|
||||
def write_summary(content: str) -> str:
|
||||
return f"Stored summary with {len(content)} characters"
|
||||
```
|
||||
|
||||
For validation patterns, see [Token Verification](/servers/auth/token-verification). For policy enforcement, see [Authorization](/servers/authorization).
|
||||
|
||||
## Expose Capabilities, Not Raw Access
|
||||
|
||||
The sandbox should not need:
|
||||
|
||||
- GitHub app private keys
|
||||
- database passwords
|
||||
- upstream OAuth client secrets
|
||||
- cloud provider credentials
|
||||
|
||||
Instead, expose MCP tools that perform privileged work on the server side.
|
||||
|
||||
Good sandbox-facing tools tend to look like this:
|
||||
|
||||
- `get_recent_updates`
|
||||
- `write_summary`
|
||||
- `fetch_repo_context`
|
||||
- `publish_review_comment`
|
||||
|
||||
These tools describe the capability the sandbox needs, not the low-level credentialed action required to perform it.
|
||||
|
||||
That distinction matters. A tool like `write_summary` lets the server decide where and how to persist the summary. A tool like `run_sql` or `call_internal_api` pushes privilege and policy into the sandbox where they are much harder to control.
|
||||
|
||||
Sandboxed agents behave best when those tools are narrow and structured:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Sandbox Tools")
|
||||
|
||||
@mcp.tool
|
||||
def write_summary(content: str) -> str:
|
||||
"""Store the final summary for the current run."""
|
||||
return f"Stored summary with {len(content)} characters"
|
||||
|
||||
@mcp.tool
|
||||
def publish_review_comment(pr_number: int, body: str) -> str:
|
||||
"""Queue a review comment for a specific pull request."""
|
||||
return f"Queued comment for PR #{pr_number}"
|
||||
```
|
||||
|
||||
These are easier to audit, easier to authorize, and easier for agents to use reliably than a broad catch-all tool like `mutate_state(kind: str, payload: dict)`.
|
||||
|
||||
Narrow tools also let you express different policies per tool instead of creating one large privileged escape hatch.
|
||||
|
||||
## Use a Proxy When Upstream Systems Are More Privileged
|
||||
|
||||
If the sandbox needs access to other MCP servers or internal systems, put FastMCP in front of them instead of forwarding secrets into the sandbox.
|
||||
|
||||
This is where proxying becomes useful. Your public-facing FastMCP server can authenticate the sandbox, then forward allowed capabilities to upstream systems with stronger credentials.
|
||||
|
||||
Typical examples:
|
||||
|
||||
- a sandbox-safe MCP gateway in front of internal MCP servers
|
||||
- a FastMCP layer in front of internal HTTP APIs
|
||||
- a job-scoped server that fronts a Git provider, issue tracker, or storage system
|
||||
|
||||
If the upstream system is itself an MCP server, FastMCP's proxy support is a natural fit. See [MCP Proxy](/servers/providers/proxy).
|
||||
|
||||
## mcp.json for Sandboxed Clients
|
||||
|
||||
If your sandboxed agent is configured through `mcp.json`, keep that configuration minimal. Point it at the remote FastMCP server and pass only the values the sandbox actually needs.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"sandbox-tools": {
|
||||
"url": "https://sandbox-tools.example.com/mcp",
|
||||
"transport": "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In many systems, authentication is injected by the launcher or environment rather than hardcoded in `mcp.json`. That is usually the right tradeoff for sandboxes. Avoid baking long-lived credentials directly into generated config files, and avoid treating `mcp.json` as the place where secret material should live.
|
||||
|
||||
That is all this section needs to do: tell the sandbox where the server lives. Keep auth and secret handling elsewhere.
|
||||
|
||||
For configuration details, see [MCP.json](/integrations/mcp-json-configuration).
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
The same few mistakes show up again and again in sandboxed deployments:
|
||||
|
||||
- passing long-lived API keys directly into the sandbox
|
||||
- treating helper scripts in the sandbox as a security boundary
|
||||
- exposing broad mutation tools instead of narrow capabilities
|
||||
- using one shared token for every sandbox
|
||||
- relying on STDIO inheritance for configuration in production
|
||||
|
||||
Each of these works at first. Each becomes painful once you have multiple tenants, multiple jobs, or an incident that requires revoking access quickly.
|
||||
|
||||
## Production Checklist
|
||||
|
||||
Before shipping a sandbox-facing FastMCP server, check these:
|
||||
|
||||
- The sandbox connects over HTTP, not with privileged local credentials.
|
||||
- Tokens are short-lived and scoped to a run, tenant, or job.
|
||||
- The FastMCP server verifies tokens on every request.
|
||||
- Long-lived secrets stay on the server side.
|
||||
- Tools are narrow, explicit, and structured.
|
||||
- Upstream privileged systems sit behind the FastMCP server or proxy.
|
||||
- Revocation and audit live at the server boundary, not inside the sandbox.
|
||||
|
||||
If you adopt those defaults, sandbox support stops being a special case and becomes a normal deployment pattern: isolated workers talk to a constrained FastMCP surface, and the server handles the privileged parts centrally.
|
||||
640
docs/v3/deployment/server-configuration.mdx
Normal file
|
|
@ -0,0 +1,640 @@
|
|||
---
|
||||
title: "Project Configuration"
|
||||
sidebarTitle: "Project Configuration"
|
||||
description: Use fastmcp.json for portable, declarative project configuration
|
||||
icon: file-code
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="2.12.0" />
|
||||
|
||||
FastMCP supports declarative configuration through `fastmcp.json` files. This is the canonical and preferred way to configure FastMCP projects, providing a single source of truth for server settings, dependencies, and deployment options that replaces complex command-line arguments.
|
||||
|
||||
The `fastmcp.json` file is designed to be a portable description of your server configuration that can be shared across environments and teams. When running from a `fastmcp.json` file, you can override any configuration values using CLI arguments.
|
||||
|
||||
## Overview
|
||||
|
||||
The `fastmcp.json` configuration file allows you to define all aspects of your FastMCP server in a structured, shareable format. Instead of remembering command-line arguments or writing shell scripts, you declare your server's configuration once and use it everywhere.
|
||||
|
||||
When you have a `fastmcp.json` file, running your server becomes as simple as:
|
||||
|
||||
```bash
|
||||
# Run the server using the configuration
|
||||
fastmcp run fastmcp.json
|
||||
|
||||
# Or if fastmcp.json exists in the current directory
|
||||
fastmcp run
|
||||
```
|
||||
|
||||
This configuration approach ensures reproducible deployments across different environments, from local development to production servers. It works seamlessly with Claude Desktop, VS Code extensions, and any MCP-compatible client.
|
||||
|
||||
## File Structure
|
||||
|
||||
The `fastmcp.json` configuration answers three fundamental questions about your server:
|
||||
|
||||
- **Source** = WHERE does your server code live?
|
||||
- **Environment** = WHAT environment setup does it require?
|
||||
- **Deployment** = HOW should the server run?
|
||||
|
||||
This conceptual model helps you understand the purpose of each configuration section and organize your settings effectively. The configuration file maps directly to these three concerns:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
|
||||
"source": {
|
||||
// WHERE: Location of your server code
|
||||
"type": "filesystem", // Optional, defaults to "filesystem"
|
||||
"path": "server.py",
|
||||
"entrypoint": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
// WHAT: Environment setup and dependencies
|
||||
"type": "uv", // Optional, defaults to "uv"
|
||||
"python": ">=3.10",
|
||||
"dependencies": ["pandas", "numpy"]
|
||||
},
|
||||
"deployment": {
|
||||
// HOW: Runtime configuration
|
||||
"transport": "stdio",
|
||||
"log_level": "INFO"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Only the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed.
|
||||
|
||||
### JSON Schema Support
|
||||
|
||||
FastMCP provides JSON schemas for IDE autocomplete and validation. Add the schema reference to your `fastmcp.json` for enhanced developer experience:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
|
||||
"source": {
|
||||
"path": "server.py",
|
||||
"entrypoint": "mcp"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Two schema URLs are available:
|
||||
- **Version-specific**: `https://gofastmcp.com/public/schemas/fastmcp.json/v1.json`
|
||||
- **Latest version**: `https://gofastmcp.com/public/schemas/fastmcp.json/latest.json`
|
||||
|
||||
Modern IDEs like VS Code will automatically provide autocomplete suggestions, validation, and inline documentation when the schema is specified.
|
||||
|
||||
### Source Configuration
|
||||
|
||||
The source configuration determines **WHERE** your server code lives. It tells FastMCP how to find and load your server, whether it's a local Python file, a remote repository, or hosted in the cloud. This section is required and forms the foundation of your configuration.
|
||||
|
||||
<Card icon="code" title="Source">
|
||||
<ParamField body="source" type="object" required>
|
||||
The server source configuration that determines where your server code lives.
|
||||
|
||||
<ParamField body="type" type="string" default="filesystem">
|
||||
The source type identifier that determines which implementation to use. Currently supports `"filesystem"` for local files. Future releases will add support for `"git"` and `"cloud"` source types.
|
||||
</ParamField>
|
||||
|
||||
<Expandable title="FileSystemSource">
|
||||
When `type` is `"filesystem"` (or omitted), the source points to a local Python file containing your FastMCP server:
|
||||
|
||||
<ParamField body="path" type="string" required>
|
||||
Path to the Python file containing your FastMCP server.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="entrypoint" type="string">
|
||||
Name of the server instance or factory function within the module:
|
||||
- Can be a FastMCP server instance (e.g., `mcp = FastMCP("MyServer")`)
|
||||
- Can be a function with no arguments that returns a FastMCP server
|
||||
- If not specified, FastMCP searches for common names: `mcp`, `server`, or `app`
|
||||
</ParamField>
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
"source": {
|
||||
"type": "filesystem",
|
||||
"path": "src/server.py",
|
||||
"entrypoint": "mcp"
|
||||
}
|
||||
```
|
||||
|
||||
Note: File paths are resolved relative to the configuration file's location.
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
</Card>
|
||||
|
||||
<Note>
|
||||
**Future Source Types**
|
||||
|
||||
Future releases will support additional source types:
|
||||
- **Git repositories** (`type: "git"`) for loading server code directly from version control
|
||||
- **Prefect Horizon** (`type: "cloud"`) for hosted servers with automatic scaling and management
|
||||
</Note>
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
The environment configuration determines **WHAT** environment setup your server requires. It controls the build-time setup of your Python environment, ensuring your server runs with the exact Python version and dependencies it requires. This section creates isolated, reproducible environments across different systems.
|
||||
|
||||
FastMCP uses an extensible environment system with a base `Environment` class that can be implemented by different environment providers. Currently, FastMCP supports the `UVEnvironment` for Python environment management using `uv`'s powerful dependency resolver.
|
||||
|
||||
<Card icon="code" title="Environment">
|
||||
<ParamField body="environment" type="object">
|
||||
Optional environment configuration. When specified, FastMCP uses the appropriate environment implementation to set up your server's runtime.
|
||||
|
||||
<ParamField body="type" type="string" default="uv">
|
||||
The environment type identifier that determines which implementation to use. Currently supports `"uv"` for Python environments managed by uv. If omitted, defaults to `"uv"`.
|
||||
</ParamField>
|
||||
|
||||
<Expandable title="UVEnvironment">
|
||||
When `type` is `"uv"` (or omitted), the environment uses uv to manage Python dependencies:
|
||||
|
||||
<ParamField body="python" type="string">
|
||||
Python version constraint. Examples:
|
||||
- Exact version: `"3.12"`
|
||||
- Minimum version: `">=3.10"`
|
||||
- Version range: `">=3.10,<3.13"`
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="dependencies" type="list[str]">
|
||||
List of pip packages with optional version specifiers (PEP 508 format).
|
||||
```json
|
||||
"dependencies": ["pandas>=2.0", "requests", "httpx"]
|
||||
```
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="requirements" type="string">
|
||||
Path to a requirements.txt file, resolved relative to the config file location.
|
||||
```json
|
||||
"requirements": "requirements.txt"
|
||||
```
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="project" type="string">
|
||||
Path to a project directory containing pyproject.toml for uv project management.
|
||||
```json
|
||||
"project": "."
|
||||
```
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="editable" type="list[string]">
|
||||
List of paths to packages to install in editable/development mode. Useful for local development when you want changes to be reflected immediately. Supports multiple packages for monorepo setups or shared libraries.
|
||||
```json
|
||||
"editable": ["."]
|
||||
```
|
||||
Or with multiple packages:
|
||||
```json
|
||||
"editable": [".", "../shared-lib", "/path/to/another-package"]
|
||||
```
|
||||
</ParamField>
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
"environment": {
|
||||
"type": "uv",
|
||||
"python": ">=3.10",
|
||||
"dependencies": ["pandas", "numpy"],
|
||||
"editable": ["."]
|
||||
}
|
||||
```
|
||||
|
||||
Note: When any UVEnvironment field is specified, FastMCP automatically creates an isolated environment using `uv` before running your server.
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
</Card>
|
||||
|
||||
When environment configuration is provided, FastMCP:
|
||||
1. Detects the environment type (defaults to `"uv"` if not specified)
|
||||
2. Creates an isolated environment using the appropriate provider
|
||||
3. Installs the specified dependencies
|
||||
4. Runs your server in this clean environment
|
||||
|
||||
This build-time setup ensures your server always has the dependencies it needs, without polluting your system Python or conflicting with other projects.
|
||||
|
||||
<Note>
|
||||
**Future Environment Types**
|
||||
|
||||
Similar to source types, future releases may support additional environment types for different runtime requirements, such as Docker containers or language-specific environments beyond Python.
|
||||
</Note>
|
||||
|
||||
### Deployment Configuration
|
||||
|
||||
The deployment configuration controls **HOW** your server runs. It defines the runtime behavior including network settings, environment variables, and execution context. These settings determine how your server operates when it executes, from transport protocols to logging levels.
|
||||
|
||||
Environment variables are included in this section because they're runtime configuration that affects how your server behaves when it executes, not how its environment is built. The deployment configuration is applied every time your server starts, controlling its operational characteristics.
|
||||
|
||||
<Card icon="code" title="Deployment Fields">
|
||||
<ParamField body="deployment" type="object">
|
||||
Optional runtime configuration for the server.
|
||||
|
||||
<Expandable title="Deployment Fields">
|
||||
<ParamField body="transport" type="string" default="stdio">
|
||||
Protocol for client communication:
|
||||
- `"stdio"`: Standard input/output for desktop clients
|
||||
- `"http"`: Network-accessible HTTP server
|
||||
- `"sse"`: Server-sent events
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="host" type="string" default="127.0.0.1">
|
||||
Network interface to bind (HTTP transport only):
|
||||
- `"127.0.0.1"`: Local connections only
|
||||
- `"0.0.0.0"`: All network interfaces
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="port" type="integer" default="3000">
|
||||
Port number for HTTP transport.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="path" type="string" default="/mcp/">
|
||||
URL path for the MCP endpoint when using HTTP transport.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="log_level" type="string" default="INFO">
|
||||
Server logging verbosity. Options:
|
||||
- `"DEBUG"`: Detailed debugging information
|
||||
- `"INFO"`: General informational messages
|
||||
- `"WARNING"`: Warning messages
|
||||
- `"ERROR"`: Error messages only
|
||||
- `"CRITICAL"`: Critical errors only
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="env" type="object">
|
||||
Environment variables to set when running the server. Supports `${VAR_NAME}` syntax for runtime interpolation.
|
||||
```json
|
||||
"env": {
|
||||
"API_KEY": "secret-key",
|
||||
"DATABASE_URL": "postgres://${DB_USER}@${DB_HOST}/mydb"
|
||||
}
|
||||
```
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="cwd" type="string">
|
||||
Working directory for the server process. Relative paths are resolved from the config file location.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="args" type="list[str]">
|
||||
Command-line arguments to pass to the server, passed after `--` to the server's argument parser.
|
||||
```json
|
||||
"args": ["--config", "server-config.json"]
|
||||
```
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
</Card>
|
||||
|
||||
#### Environment Variable Interpolation
|
||||
|
||||
The `env` field in deployment configuration supports runtime interpolation of environment variables using `${VAR_NAME}` syntax. This enables dynamic configuration based on your deployment environment:
|
||||
|
||||
```json
|
||||
{
|
||||
"deployment": {
|
||||
"env": {
|
||||
"API_URL": "https://api.${ENVIRONMENT}.example.com",
|
||||
"DATABASE_URL": "postgres://${DB_USER}:${DB_PASS}@${DB_HOST}/myapp",
|
||||
"CACHE_KEY": "myapp_${ENVIRONMENT}_${VERSION}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When the server starts, FastMCP replaces `${ENVIRONMENT}`, `${DB_USER}`, etc. with values from your system's environment variables. If a variable doesn't exist, the placeholder is preserved as-is.
|
||||
|
||||
**Example**: If your system has `ENVIRONMENT=production` and `DB_HOST=db.example.com`:
|
||||
```json
|
||||
// Configuration
|
||||
{
|
||||
"deployment": {
|
||||
"env": {
|
||||
"API_URL": "https://api.${ENVIRONMENT}.example.com",
|
||||
"DB_HOST": "${DB_HOST}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Result at runtime
|
||||
{
|
||||
"API_URL": "https://api.production.example.com",
|
||||
"DB_HOST": "db.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
This feature is particularly useful for:
|
||||
- Deploying the same configuration across development, staging, and production
|
||||
- Keeping sensitive values out of configuration files
|
||||
- Building dynamic URLs and connection strings
|
||||
- Creating environment-specific prefixes or suffixes
|
||||
|
||||
## Usage with CLI Commands
|
||||
|
||||
FastMCP automatically detects and uses a file specifically named `fastmcp.json` in the current directory, making server execution simple and consistent. Files with FastMCP configuration format but different names are not auto-detected and must be specified explicitly:
|
||||
|
||||
```bash
|
||||
# Auto-detect fastmcp.json in current directory
|
||||
cd my-project
|
||||
fastmcp run # No arguments needed!
|
||||
|
||||
# Or specify a configuration file explicitly
|
||||
fastmcp run prod.fastmcp.json
|
||||
|
||||
# Skip environment setup when already in a uv environment
|
||||
fastmcp run fastmcp.json --skip-env
|
||||
|
||||
# Skip source preparation when source is already prepared
|
||||
fastmcp run fastmcp.json --skip-source
|
||||
|
||||
# Skip both environment and source preparation
|
||||
fastmcp run fastmcp.json --skip-env --skip-source
|
||||
```
|
||||
|
||||
### Pre-building Environments
|
||||
|
||||
You can use `fastmcp project prepare` to create a persistent uv project with all dependencies pre-installed:
|
||||
|
||||
```bash
|
||||
# Create a persistent environment
|
||||
fastmcp project prepare fastmcp.json --output-dir ./env
|
||||
|
||||
# Use the pre-built environment to run the server
|
||||
fastmcp run fastmcp.json --project ./env
|
||||
```
|
||||
|
||||
This pattern separates environment setup (slow) from server execution (fast), useful for deployment scenarios.
|
||||
|
||||
### Using an Existing Environment
|
||||
|
||||
By default, FastMCP creates an isolated environment with `uv` based on your configuration. When you already have a suitable Python environment, use the `--skip-env` flag to skip environment creation:
|
||||
|
||||
```bash
|
||||
fastmcp run fastmcp.json --skip-env
|
||||
```
|
||||
|
||||
**When you already have an environment:**
|
||||
- You're in an activated virtual environment with all dependencies installed
|
||||
- You're inside a Docker container with pre-installed dependencies
|
||||
- You're in a CI/CD pipeline that pre-builds the environment
|
||||
- You're using a system-wide installation with all required packages
|
||||
- You're in a uv-managed environment (prevents infinite recursion)
|
||||
|
||||
This flag tells FastMCP: "I already have everything installed, just run the server."
|
||||
|
||||
### Using an Existing Source
|
||||
|
||||
When working with source types that require preparation (future support for git repositories or cloud sources), use the `--skip-source` flag when you already have the source code available:
|
||||
|
||||
```bash
|
||||
fastmcp run fastmcp.json --skip-source
|
||||
```
|
||||
|
||||
**When you already have the source:**
|
||||
- You've previously cloned a git repository and don't need to re-fetch
|
||||
- You have a cached copy of a cloud-hosted server
|
||||
- You're in a CI/CD pipeline where source checkout is a separate step
|
||||
- You're iterating locally on already-downloaded code
|
||||
|
||||
This flag tells FastMCP: "I already have the source code, skip any download/clone steps."
|
||||
|
||||
Note: For filesystem sources (local Python files), this flag has no effect since they don't require preparation.
|
||||
|
||||
The configuration file works with all FastMCP commands:
|
||||
- **`run`** - Start the server in production mode
|
||||
- **`dev`** - Launch with the Inspector UI for development
|
||||
- **`inspect`** - View server capabilities and configuration
|
||||
- **`install`** - Install to Claude Desktop, Cursor, or other MCP clients
|
||||
|
||||
When no file argument is provided, FastMCP searches the current directory for `fastmcp.json`. This means you can simply navigate to your project directory and run `fastmcp run` to start your server with all its configured settings.
|
||||
|
||||
### CLI Override Behavior
|
||||
|
||||
Command-line arguments take precedence over configuration file values, allowing ad-hoc adjustments without modifying the file:
|
||||
|
||||
```bash
|
||||
# Config specifies port 3000, CLI overrides to 8080
|
||||
fastmcp run fastmcp.json --port 8080
|
||||
|
||||
# Config specifies stdio, CLI overrides to HTTP
|
||||
fastmcp run fastmcp.json --transport http
|
||||
|
||||
# Add extra dependencies not in config
|
||||
fastmcp run fastmcp.json --with requests --with httpx
|
||||
```
|
||||
|
||||
This precedence order enables:
|
||||
- Quick testing of different settings
|
||||
- Environment-specific overrides in deployment scripts
|
||||
- Debugging with increased log levels
|
||||
- Temporary configuration changes
|
||||
|
||||
### Custom Naming Patterns
|
||||
|
||||
You can use different configuration files for different environments:
|
||||
|
||||
- `fastmcp.json` - Default configuration
|
||||
- `dev.fastmcp.json` - Development settings
|
||||
- `prod.fastmcp.json` - Production settings
|
||||
- `test_fastmcp.json` - Test configuration
|
||||
|
||||
Any file with "fastmcp.json" in the name is recognized as a configuration file.
|
||||
|
||||
## Examples
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Basic Configuration">
|
||||
|
||||
A minimal configuration for a simple server:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
|
||||
"source": {
|
||||
"path": "server.py",
|
||||
"entrypoint": "mcp"
|
||||
}
|
||||
}
|
||||
```
|
||||
This configuration explicitly specifies the server entrypoint (`mcp`), making it clear which server instance or factory function to use. Uses all defaults: STDIO transport, no special dependencies, standard logging.
|
||||
</Tab>
|
||||
<Tab title="Development Configuration">
|
||||
|
||||
A configuration optimized for local development:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
|
||||
// WHERE does the server live?
|
||||
"source": {
|
||||
"path": "src/server.py",
|
||||
"entrypoint": "app"
|
||||
},
|
||||
// WHAT dependencies does it need?
|
||||
"environment": {
|
||||
"type": "uv",
|
||||
"python": "3.12",
|
||||
"dependencies": ["fastmcp[dev]"],
|
||||
"editable": "."
|
||||
},
|
||||
// HOW should it run?
|
||||
"deployment": {
|
||||
"transport": "http",
|
||||
"host": "127.0.0.1",
|
||||
"port": 8000,
|
||||
"log_level": "DEBUG",
|
||||
"env": {
|
||||
"DEBUG": "true",
|
||||
"ENV": "development"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Production Configuration">
|
||||
|
||||
A production-ready configuration with full dependency management:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
|
||||
// WHERE does the server live?
|
||||
"source": {
|
||||
"path": "app/main.py",
|
||||
"entrypoint": "mcp_server"
|
||||
},
|
||||
// WHAT dependencies does it need?
|
||||
"environment": {
|
||||
"python": "3.11",
|
||||
"requirements": "requirements/production.txt",
|
||||
"project": "."
|
||||
},
|
||||
// HOW should it run?
|
||||
"deployment": {
|
||||
"transport": "http",
|
||||
"host": "0.0.0.0",
|
||||
"port": 3000,
|
||||
"path": "/api/mcp/",
|
||||
"log_level": "INFO",
|
||||
"env": {
|
||||
"ENV": "production",
|
||||
"API_BASE_URL": "https://api.example.com",
|
||||
"DATABASE_URL": "postgresql://user:pass@db.example.com/prod"
|
||||
},
|
||||
"cwd": "/app",
|
||||
"args": ["--workers", "4"]
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Data Science Server">
|
||||
|
||||
Configuration for a data analysis server with scientific packages:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
|
||||
"source": {
|
||||
"path": "analysis_server.py",
|
||||
"entrypoint": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
"python": "3.11",
|
||||
"dependencies": [
|
||||
"pandas>=2.0",
|
||||
"numpy",
|
||||
"scikit-learn",
|
||||
"matplotlib",
|
||||
"jupyterlab"
|
||||
]
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "stdio",
|
||||
"env": {
|
||||
"MATPLOTLIB_BACKEND": "Agg",
|
||||
"DATA_PATH": "./datasets"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Multi-Environment Setup">
|
||||
|
||||
You can maintain multiple configuration files for different environments:
|
||||
|
||||
**dev.fastmcp.json**:
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
|
||||
"source": {
|
||||
"path": "server.py",
|
||||
"entrypoint": "mcp"
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "http",
|
||||
"log_level": "DEBUG"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**prod.fastmcp.json**:
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
|
||||
"source": {
|
||||
"path": "server.py",
|
||||
"entrypoint": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
"requirements": "requirements/production.txt"
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "http",
|
||||
"host": "0.0.0.0",
|
||||
"log_level": "WARNING"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Run different configurations:
|
||||
```bash
|
||||
fastmcp run dev.fastmcp.json # Development
|
||||
fastmcp run prod.fastmcp.json # Production
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Migrating from CLI Arguments
|
||||
|
||||
If you're currently using command-line arguments or shell scripts, migrating to `fastmcp.json` simplifies your workflow. Here's how common CLI patterns map to configuration:
|
||||
|
||||
**CLI Command**:
|
||||
```bash
|
||||
uv run --with pandas --with requests \
|
||||
fastmcp run server.py \
|
||||
--transport http \
|
||||
--port 8000 \
|
||||
--log-level INFO
|
||||
```
|
||||
|
||||
**Equivalent fastmcp.json**:
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
|
||||
"source": {
|
||||
"path": "server.py",
|
||||
"entrypoint": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
"dependencies": ["pandas", "requests"]
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "http",
|
||||
"port": 8000,
|
||||
"log_level": "INFO"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now simply run:
|
||||
```bash
|
||||
fastmcp run # Automatically finds and uses fastmcp.json
|
||||
```
|
||||
|
||||
The configuration file approach provides better documentation, easier sharing, and consistent execution across different environments while maintaining the flexibility to override settings when needed.
|
||||
198
docs/v3/development/contributing.mdx
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
---
|
||||
title: "Contributing"
|
||||
description: "Development workflow for FastMCP contributors"
|
||||
icon: code-pull-request
|
||||
---
|
||||
|
||||
Contributing to FastMCP means joining a community that values clean, maintainable code and thoughtful API design. All contributions are valued - from fixing typos in documentation to implementing major features.
|
||||
|
||||
## Design Principles
|
||||
|
||||
Every contribution should advance these principles:
|
||||
|
||||
- 🚀 **Fast** — High-level interfaces mean less code and faster development
|
||||
- 🍀 **Simple** — Minimal boilerplate; the obvious way should be the right way
|
||||
- 🐍 **Pythonic** — Feels natural to Python developers; no surprising patterns
|
||||
- 🔍 **Complete** — Everything needed for production: auth, testing, deployment, observability
|
||||
|
||||
PRs are evaluated against these principles. Code that makes FastMCP slower, harder to reason about, less Pythonic, or less complete will be rejected.
|
||||
|
||||
## Issues
|
||||
|
||||
### Issue First, Code Second
|
||||
|
||||
**Every pull request requires a corresponding issue - no exceptions.** This requirement creates a collaborative space where approach, scope, and alignment are established before code is written. Issues serve as design documents where maintainers and contributors discuss implementation strategy, identify potential conflicts with existing patterns, and ensure proposed changes advance FastMCP's vision.
|
||||
|
||||
**FastMCP is an opinionated framework, not a kitchen sink.** The maintainers have strong beliefs about what FastMCP should and shouldn't do. Just because something takes N lines of code and you want it in fewer lines doesn't mean FastMCP should take on the maintenance burden or endorse that pattern. This is judged at the maintainers' discretion.
|
||||
|
||||
Use issues to understand scope BEFORE opening PRs. The issue discussion determines whether a feature belongs in core, contrib, or not at all.
|
||||
|
||||
### Writing Good Issues
|
||||
|
||||
FastMCP is an extremely highly-trafficked repository maintained by a very small team. Issues that appear to transfer burden to maintainers without any effort to validate the problem will be closed. Please help the maintainers help you by always providing a minimal reproducible example and clearly describing the problem.
|
||||
|
||||
**LLM-generated issues will be closed immediately.** Issues that contain paragraphs of unnecessary explanation, verbose problem descriptions, or obvious LLM authorship patterns obfuscate the actual problem and transfer burden to maintainers.
|
||||
|
||||
Write clear, concise issues that:
|
||||
- State the problem directly
|
||||
- Provide a minimal reproducible example
|
||||
- Skip unnecessary background or context
|
||||
- Take responsibility for clear communication
|
||||
|
||||
Issues may be labeled "Invalid" simply due to confusion caused by verbosity or not adhering to the guidelines outlined here.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
PRs that deviate from FastMCP's core principles will be rejected regardless of implementation quality. **PRs are NOT for iterating on ideas** - they should only be opened for ideas that already have a bias toward acceptance based on issue discussion.
|
||||
|
||||
|
||||
### Development Environment
|
||||
|
||||
#### Installation
|
||||
|
||||
To contribute to FastMCP, you'll need to set up a development environment with all necessary tools and dependencies.
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/PrefectHQ/fastmcp.git
|
||||
cd fastmcp
|
||||
|
||||
# Install all dependencies including dev tools
|
||||
uv sync
|
||||
|
||||
# Install prek hooks
|
||||
uv run prek install
|
||||
```
|
||||
|
||||
In addition, some development commands require [just](https://github.com/casey/just) to be installed.
|
||||
|
||||
Prek hooks will run automatically on every commit to catch issues before they reach CI. If you see failures, fix them before committing - never commit broken code expecting to fix it later.
|
||||
|
||||
### Development Standards
|
||||
|
||||
#### Scope
|
||||
|
||||
Large pull requests create review bottlenecks and quality risks. Unless you're fixing a discrete bug or making an incredibly well-scoped change, keep PRs small and focused.
|
||||
|
||||
A PR that changes 50 lines across 3 files can be thoroughly reviewed in minutes. A PR that changes 500 lines across 20 files requires hours of careful analysis and often hides subtle issues.
|
||||
|
||||
Breaking large features into smaller PRs:
|
||||
- Creates better review experiences
|
||||
- Makes git history clear
|
||||
- Simplifies debugging with bisect
|
||||
- Reduces merge conflicts
|
||||
- Gets your code merged faster
|
||||
|
||||
#### Code Quality
|
||||
|
||||
FastMCP values clarity over cleverness. Every line you write will be maintained by someone else - possibly years from now, possibly without context about your decisions.
|
||||
|
||||
**PRs can be rejected for two opposing reasons:**
|
||||
1. **Insufficient quality** - Code that doesn't meet our standards for clarity, maintainability, or idiomaticity
|
||||
2. **Overengineering** - Code that is overbearing, unnecessarily complex, or tries to be too clever
|
||||
|
||||
The focus is on idiomatic, high-quality Python. FastMCP uses patterns like `NotSet` type as an alternative to `None` in certain situations - follow existing patterns.
|
||||
|
||||
#### Required Practices
|
||||
|
||||
**Full type annotations** on all functions and methods. They catch bugs before runtime and serve as inline documentation.
|
||||
|
||||
**Async/await patterns** for all I/O operations. Even if your specific use case doesn't need concurrency, consistency means users can compose features without worrying about blocking operations.
|
||||
|
||||
**Descriptive names** make code self-documenting. `auth_token` is clear; `tok` requires mental translation.
|
||||
|
||||
**Specific exception types** make error handling predictable. Catching `ValueError` tells readers exactly what error you expect. Never use bare `except` clauses.
|
||||
|
||||
#### Anti-Patterns to Avoid
|
||||
|
||||
**Complex one-liners** are hard to debug and modify. Break operations into clear steps.
|
||||
|
||||
**Mutable default arguments** cause subtle bugs. Use `None` as the default and create the mutable object inside the function.
|
||||
|
||||
**Breaking established patterns** confuses readers. If you must deviate, discuss in the issue first.
|
||||
|
||||
### Prek Checks
|
||||
|
||||
```bash
|
||||
# Runs automatically on commit, or manually:
|
||||
uv run prek run --all-files
|
||||
```
|
||||
|
||||
This runs three critical tools:
|
||||
- **Ruff**: Linting and formatting
|
||||
- **Prettier**: Code formatting
|
||||
- **ty**: Static type checking
|
||||
|
||||
Pytest runs separately as a distinct workflow step after prek checks pass. CI will reject PRs that fail these checks. Always run them locally first.
|
||||
|
||||
### Testing
|
||||
|
||||
Tests are documentation that shows how features work. Good tests give reviewers confidence and help future maintainers understand intent.
|
||||
|
||||
```bash
|
||||
# Run specific test directory
|
||||
uv run pytest tests/server/ -v
|
||||
|
||||
# Run all tests before submitting PR
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
Every new feature needs tests. See the [Testing Guide](/development/tests) for patterns and requirements.
|
||||
|
||||
### Documentation
|
||||
|
||||
A feature doesn't exist unless it's documented. Note that FastMCP's hosted documentation always tracks the main branch - users who want historical documentation can clone the repo, checkout a specific tag, and host it themselves.
|
||||
|
||||
```bash
|
||||
# Preview documentation locally
|
||||
just docs
|
||||
```
|
||||
|
||||
Documentation requirements:
|
||||
- **Explain concepts in prose first** - Code without context is just syntax
|
||||
- **Complete, runnable examples** - Every code block should be copy-pasteable
|
||||
- **Register in docs.json** - Makes pages appear in navigation
|
||||
- **Version badges** - Mark when features were added using `<VersionBadge />`
|
||||
|
||||
#### SDK Documentation
|
||||
|
||||
FastMCP's SDK documentation is auto-generated from the source code docstrings and type annotations. It is automatically updated on every merge to main by a GitHub Actions workflow, so users are *not* responsible for keeping the documentation up to date. However, to generate it proactively, you can use the following command:
|
||||
|
||||
```bash
|
||||
just api-ref-all
|
||||
```
|
||||
|
||||
### Submitting Your PR
|
||||
|
||||
#### Before Submitting
|
||||
|
||||
1. **Run all checks**: `uv run prek run --all-files && uv run pytest`
|
||||
2. **Keep scope small**: One feature or fix per PR
|
||||
3. **Write clear description**: Your PR description becomes permanent documentation
|
||||
4. **Update docs**: Include documentation for API changes
|
||||
|
||||
#### PR Description
|
||||
|
||||
Write PR descriptions that explain:
|
||||
- What problem you're solving
|
||||
- Why you chose this approach
|
||||
- Any trade-offs or alternatives considered
|
||||
- Migration path for breaking changes
|
||||
|
||||
Focus on the "why" - the code shows the "what". Keep it concise but complete.
|
||||
|
||||
#### What We Look For
|
||||
|
||||
**Framework Philosophy**: FastMCP is NOT trying to do all things or provide all shortcuts. Features are rejected when they don't align with the framework's vision, even if perfectly implemented. The burden of proof is on the PR to demonstrate value.
|
||||
|
||||
**Code Quality**: We verify code follows existing patterns. Consistency reduces cognitive load. When every module works similarly, developers understand new code quickly.
|
||||
|
||||
**Test Coverage**: Not every line needs testing, but every behavior does. Tests document intent and protect against regressions.
|
||||
|
||||
**Breaking Changes**: May be acceptable in minor versions but must be clearly documented. See the [versioning policy](/development/releases#versioning-policy).
|
||||
|
||||
## Special Modules
|
||||
|
||||
**`contrib`**: Community-maintained patterns and utilities. Original authors maintain their contributions. Not representative of the core framework.
|
||||
|
||||
**`experimental`**: Maintainer-developed features that may preview future functionality. Can break or be deleted at any time without notice. Pin your FastMCP version when using these features.
|
||||
79
docs/v3/development/releases.mdx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
title: "Releases"
|
||||
description: "FastMCP versioning and release process"
|
||||
icon: "truck-fast"
|
||||
---
|
||||
|
||||
FastMCP releases frequently to deliver features quickly in the rapidly evolving MCP ecosystem. We use semantic versioning pragmatically - the Model Context Protocol is young, patterns are still emerging, and waiting for perfect stability would mean missing opportunities to empower developers with better tools.
|
||||
|
||||
## Versioning Policy
|
||||
|
||||
### Semantic Versioning
|
||||
|
||||
**Major (x.0.0)**: Complete API redesigns
|
||||
|
||||
Major versions represent fundamental shifts. FastMCP 2.x is entirely different from 1.x in both implementation and design philosophy.
|
||||
|
||||
**Minor (2.x.0)**: New features and evolution
|
||||
|
||||
<Warning>
|
||||
Unlike traditional semantic versioning, minor versions **may** include [breaking changes](#breaking-changes) when necessary for the ecosystem's evolution. This flexibility is essential in a young ecosystem where perfect backwards compatibility would prevent important improvements.
|
||||
</Warning>
|
||||
|
||||
FastMCP always targets the most current MCP Protocol version. Breaking changes in the MCP spec or MCP SDK automatically flow through to FastMCP - we prioritize staying current with the latest features and conventions over maintaining compatibility with older protocol versions.
|
||||
|
||||
**Patch (2.0.x)**: Bug fixes and refinements
|
||||
|
||||
Patch versions contain only bug fixes without breaking changes. These are safe updates you can apply with confidence.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
We permit breaking changes in minor versions because the MCP ecosystem is rapidly evolving. Refusing to break problematic APIs would accumulate design debt that eventually makes the framework unusable. Each breaking change represents a deliberate decision to keep FastMCP aligned with the ecosystem's evolution.
|
||||
|
||||
When breaking changes occur:
|
||||
- They only happen in minor versions (e.g., 2.3.x to 2.4.0)
|
||||
- Release notes explain what changed and how to migrate
|
||||
- We provide deprecation warnings at least 1 minor version in advance when possible
|
||||
- Changes must substantially benefit users to justify disruption
|
||||
|
||||
The public API is what's covered by our compatibility guarantees - these are the parts of FastMCP you can rely on to remain stable within a minor version. The public API consists of:
|
||||
- `FastMCP` server class, `Client` class, and FastMCP `Context`
|
||||
- Core MCP components: `Tool`, `Prompt`, `Resource`, `ResourceTemplate`, and transports
|
||||
- Their public methods and documented behaviors
|
||||
|
||||
Everything else (utilities, private methods, internal modules) may change without notice. This boundary lets us refactor internals and improve implementation details without breaking your code. For production stability, pin to specific versions.
|
||||
|
||||
<Warning>
|
||||
The `fastmcp.server.auth` module was introduced in 2.12.0 and is exempted from this policy temporarily, meaning it is *expected* to have breaking changes even on patch versions. This is because auth is a rapidly evolving part of the MCP spec and it would be dangerous to be beholden to old decisions. Please pin your FastMCP version if using authentication in production.
|
||||
|
||||
We expect this exemption to last through at least the 2.12.x and 2.13.x release series.
|
||||
</Warning>
|
||||
|
||||
### Production Use
|
||||
|
||||
Pin to exact versions:
|
||||
```
|
||||
fastmcp==2.11.0 # Good
|
||||
fastmcp>=2.11.0 # Bad - will install breaking changes
|
||||
```
|
||||
|
||||
## Creating Releases
|
||||
|
||||
Our release process is intentionally simple:
|
||||
|
||||
1. Create GitHub release with tag `vMAJOR.MINOR.PATCH` (e.g., `v2.11.0`)
|
||||
2. Generate release notes automatically, and curate or add additional editorial information as needed
|
||||
3. GitHub releases automatically trigger PyPI deployments
|
||||
|
||||
This automation lets maintainers focus on code quality rather than release mechanics.
|
||||
|
||||
### Release Cadence
|
||||
|
||||
We follow a feature-driven release cadence rather than a fixed schedule. Minor versions ship approximately every 3-4 weeks when significant functionality is ready.
|
||||
|
||||
Patch releases ship promptly for:
|
||||
- Critical bug fixes
|
||||
- Security updates (immediate release)
|
||||
- Regression fixes
|
||||
|
||||
This approach means you get improvements as soon as they're ready rather than waiting for arbitrary release dates.
|
||||
396
docs/v3/development/tests.mdx
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
---
|
||||
title: "Tests"
|
||||
description: "Testing patterns and requirements for FastMCP"
|
||||
icon: vial
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
Good tests are the foundation of reliable software. In FastMCP, we treat tests as first-class documentation that demonstrates how features work while protecting against regressions. Every new capability needs comprehensive tests that demonstrate correctness.
|
||||
|
||||
## FastMCP Tests
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
uv run pytest
|
||||
|
||||
# Run specific test file
|
||||
uv run pytest tests/server/test_auth.py
|
||||
|
||||
# Run with coverage
|
||||
uv run pytest --cov=fastmcp
|
||||
|
||||
# Skip integration tests for faster runs
|
||||
uv run pytest -m "not integration"
|
||||
|
||||
# Skip tests that spawn processes
|
||||
uv run pytest -m "not integration and not client_process"
|
||||
```
|
||||
|
||||
Tests should complete in under 1 second unless marked as integration tests. This speed encourages running them frequently, catching issues early.
|
||||
|
||||
### Test Organization
|
||||
|
||||
Our test organization mirrors the source package structure, creating a predictable mapping between code and tests. When you're working on `fastmcp_slim/fastmcp/server/auth.py`, you'll find its tests in `tests/server/test_auth.py`. In rare cases tests are split further - for example, the OpenAPI tests are so comprehensive they're split across multiple files.
|
||||
|
||||
### Test Markers
|
||||
|
||||
We use pytest markers to categorize tests that require special resources or take longer to run:
|
||||
|
||||
```python
|
||||
@pytest.mark.integration
|
||||
async def test_github_api_integration():
|
||||
"""Test GitHub API integration with real service."""
|
||||
token = os.getenv("FASTMCP_GITHUB_TOKEN")
|
||||
if not token:
|
||||
pytest.skip("FASTMCP_GITHUB_TOKEN not available")
|
||||
|
||||
# Test against real GitHub API
|
||||
client = GitHubClient(token)
|
||||
repos = await client.list_repos("prefecthq")
|
||||
assert "fastmcp" in [repo.name for repo in repos]
|
||||
|
||||
@pytest.mark.client_process
|
||||
async def test_stdio_transport():
|
||||
"""Test STDIO transport with separate process."""
|
||||
# This spawns a subprocess
|
||||
async with Client("python examples/simple_echo.py") as client:
|
||||
result = await client.call_tool("echo", {"message": "test"})
|
||||
assert result.content[0].text == "test"
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
|
||||
### Test Requirements
|
||||
|
||||
Following these practices creates maintainable, debuggable test suites that serve as both documentation and regression protection.
|
||||
|
||||
#### Single Behavior Per Test
|
||||
|
||||
Each test should verify exactly one behavior. When it fails, you need to know immediately what broke. A test that checks five things gives you five potential failure points to investigate. A test that checks one thing points directly to the problem.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Good: Atomic Test
|
||||
async def test_tool_registration():
|
||||
"""Test that tools are properly registered with the server."""
|
||||
mcp = FastMCP("test-server")
|
||||
|
||||
@mcp.tool
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
|
||||
tools = mcp.list_tools()
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name == "add"
|
||||
```
|
||||
|
||||
```python Bad: Multi-Behavior Test
|
||||
async def test_server_functionality():
|
||||
"""Test multiple server features at once."""
|
||||
mcp = FastMCP("test-server")
|
||||
|
||||
# Tool registration
|
||||
@mcp.tool
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
|
||||
# Resource creation
|
||||
@mcp.resource("config://app")
|
||||
def get_config():
|
||||
return {"version": "1.0"}
|
||||
|
||||
# Authentication setup
|
||||
mcp.auth = BearerTokenProvider({"token": "user"})
|
||||
|
||||
# What exactly are we testing? If this fails, what broke?
|
||||
assert mcp.list_tools()
|
||||
assert mcp.list_resources()
|
||||
assert mcp.auth is not None
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
#### Self-Contained Setup
|
||||
|
||||
Every test must create its own setup. Tests should be runnable in any order, in parallel, or in isolation. When a test fails, you should be able to run just that test to reproduce the issue.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Good: Self-Contained
|
||||
async def test_tool_execution_with_error():
|
||||
"""Test that tool errors are properly handled."""
|
||||
mcp = FastMCP("test-server")
|
||||
|
||||
@mcp.tool
|
||||
def divide(a: int, b: int) -> float:
|
||||
if b == 0:
|
||||
raise ValueError("Cannot divide by zero")
|
||||
return a / b
|
||||
|
||||
async with Client(mcp) as client:
|
||||
with pytest.raises(Exception):
|
||||
await client.call_tool("divide", {"a": 10, "b": 0})
|
||||
```
|
||||
|
||||
```python Bad: Test Dependencies
|
||||
# Global state that tests depend on
|
||||
test_server = None
|
||||
|
||||
def test_setup_server():
|
||||
"""Setup for other tests."""
|
||||
global test_server
|
||||
test_server = FastMCP("shared-server")
|
||||
|
||||
def test_server_works():
|
||||
"""Test server functionality."""
|
||||
# Depends on test_setup_server running first
|
||||
assert test_server is not None
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
#### Clear Intent
|
||||
|
||||
Test names and assertions should make the verified behavior obvious. A developer reading your test should understand what feature it validates and how that feature should behave.
|
||||
|
||||
```python
|
||||
async def test_authenticated_tool_requires_valid_token():
|
||||
"""Test that authenticated users can access protected tools."""
|
||||
mcp = FastMCP("test-server")
|
||||
mcp.auth = BearerTokenProvider({"secret-token": "test-user"})
|
||||
|
||||
@mcp.tool
|
||||
def protected_action() -> str:
|
||||
return "success"
|
||||
|
||||
async with Client(mcp, auth=BearerAuth("secret-token")) as client:
|
||||
result = await client.call_tool("protected_action", {})
|
||||
assert result.content[0].text == "success"
|
||||
```
|
||||
|
||||
#### Using Fixtures
|
||||
|
||||
Use fixtures to create reusable data, server configurations, or other resources for your tests. Note that you should **not** open FastMCP clients in your fixtures as it can create hard-to-diagnose issues with event loops.
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from fastmcp import FastMCP, Client
|
||||
|
||||
@pytest.fixture
|
||||
def weather_server():
|
||||
server = FastMCP("WeatherServer")
|
||||
|
||||
@server.tool
|
||||
def get_temperature(city: str) -> dict:
|
||||
temps = {"NYC": 72, "LA": 85, "Chicago": 68}
|
||||
return {"city": city, "temp": temps.get(city, 70)}
|
||||
|
||||
return server
|
||||
|
||||
async def test_temperature_tool(weather_server):
|
||||
async with Client(weather_server) as client:
|
||||
result = await client.call_tool("get_temperature", {"city": "LA"})
|
||||
assert result.data == {"city": "LA", "temp": 85}
|
||||
```
|
||||
|
||||
#### Effective Assertions
|
||||
|
||||
Assertions should be specific and provide context on failure. When a test fails during CI, the assertion message should tell you exactly what went wrong.
|
||||
|
||||
```python
|
||||
# Basic assertion - minimal context on failure
|
||||
assert result.status == "success"
|
||||
|
||||
# Better - explains what was expected
|
||||
assert result.status == "success", f"Expected successful operation, got {result.status}: {result.error}"
|
||||
```
|
||||
|
||||
Try not to have too many assertions in a single test unless you truly need to check various aspects of the same behavior. In general, assertions of different behaviors should be in separate tests.
|
||||
|
||||
#### Inline Snapshots
|
||||
|
||||
FastMCP uses `inline-snapshot` for testing complex data structures. On first run of `pytest --inline-snapshot=create` with an empty `snapshot()`, pytest will auto-populate the expected value. To update snapshots after intentional changes, run `pytest --inline-snapshot=fix`. This is particularly useful for testing JSON schemas and API responses.
|
||||
|
||||
```python
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
async def test_tool_schema_generation():
|
||||
"""Test that tool schemas are generated correctly."""
|
||||
mcp = FastMCP("test-server")
|
||||
|
||||
@mcp.tool
|
||||
def calculate_tax(amount: float, rate: float = 0.1) -> dict:
|
||||
"""Calculate tax on an amount."""
|
||||
return {"amount": amount, "tax": amount * rate, "total": amount * (1 + rate)}
|
||||
|
||||
tools = mcp.list_tools()
|
||||
schema = tools[0].inputSchema
|
||||
|
||||
# First run: snapshot() is empty, gets auto-populated
|
||||
# Subsequent runs: compares against stored snapshot
|
||||
assert schema == snapshot({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {"type": "number"},
|
||||
"rate": {"type": "number", "default": 0.1}
|
||||
},
|
||||
"required": ["amount"]
|
||||
})
|
||||
```
|
||||
|
||||
### In-Memory Testing
|
||||
|
||||
FastMCP uses in-memory transport for testing, where servers and clients communicate directly. The majority of functionality can be tested in a deterministic fashion this way. We use more complex setups only when testing transports themselves.
|
||||
|
||||
The in-memory transport runs the real MCP protocol implementation without network overhead. Instead of deploying your server or managing network connections, you pass your server instance directly to the client. Everything runs in the same Python process - you can set breakpoints anywhere and step through with your debugger.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP, Client
|
||||
|
||||
# Create your server
|
||||
server = FastMCP("WeatherServer")
|
||||
|
||||
@server.tool
|
||||
def get_temperature(city: str) -> dict:
|
||||
"""Get current temperature for a city"""
|
||||
temps = {"NYC": 72, "LA": 85, "Chicago": 68}
|
||||
return {"city": city, "temp": temps.get(city, 70)}
|
||||
|
||||
async def test_weather_operations():
|
||||
# Pass server directly - no deployment needed
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("get_temperature", {"city": "NYC"})
|
||||
assert result.data == {"city": "NYC", "temp": 72}
|
||||
```
|
||||
|
||||
This pattern makes tests deterministic and fast - typically completing in milliseconds rather than seconds.
|
||||
|
||||
### Mocking External Dependencies
|
||||
|
||||
FastMCP servers are standard Python objects, so you can mock external dependencies using your preferred approach:
|
||||
|
||||
```python
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
async def test_database_tool():
|
||||
server = FastMCP("DataServer")
|
||||
|
||||
# Mock the database
|
||||
mock_db = AsyncMock()
|
||||
mock_db.fetch_users.return_value = [
|
||||
{"id": 1, "name": "Alice"},
|
||||
{"id": 2, "name": "Bob"}
|
||||
]
|
||||
|
||||
@server.tool
|
||||
async def list_users() -> list:
|
||||
return await mock_db.fetch_users()
|
||||
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("list_users", {})
|
||||
assert len(result.data) == 2
|
||||
assert result.data[0]["name"] == "Alice"
|
||||
mock_db.fetch_users.assert_called_once()
|
||||
```
|
||||
|
||||
### Testing Network Transports
|
||||
|
||||
While in-memory testing covers most unit testing needs, you'll occasionally need to test actual network transports like HTTP or SSE. FastMCP provides two approaches: in-process async servers (preferred), and separate subprocess servers (for special cases).
|
||||
|
||||
#### In-Process Network Testing (Preferred)
|
||||
|
||||
<VersionBadge version="2.13.0" />
|
||||
|
||||
For most network transport tests, use `run_server_async` as an async context manager. This runs the server as a task in the same process, providing fast, deterministic tests with full debugger support:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from fastmcp import FastMCP, Client
|
||||
from fastmcp.client.transports import StreamableHttpTransport
|
||||
from fastmcp.utilities.tests import run_server_async
|
||||
|
||||
def create_test_server() -> FastMCP:
|
||||
"""Create a test server instance."""
|
||||
server = FastMCP("TestServer")
|
||||
|
||||
@server.tool
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
return server
|
||||
|
||||
@pytest.fixture
|
||||
async def http_server() -> str:
|
||||
"""Start server in-process for testing."""
|
||||
server = create_test_server()
|
||||
async with run_server_async(server) as url:
|
||||
yield url
|
||||
|
||||
async def test_http_transport(http_server: str):
|
||||
"""Test actual HTTP transport behavior."""
|
||||
async with Client(
|
||||
transport=StreamableHttpTransport(http_server)
|
||||
) as client:
|
||||
result = await client.ping()
|
||||
assert result is True
|
||||
|
||||
greeting = await client.call_tool("greet", {"name": "World"})
|
||||
assert greeting.data == "Hello, World!"
|
||||
```
|
||||
|
||||
The `run_server_async` context manager automatically handles server lifecycle and cleanup. This approach is faster than subprocess-based testing and provides better error messages.
|
||||
|
||||
#### Subprocess Testing (Special Cases)
|
||||
|
||||
For tests that require complete process isolation (like STDIO transport or testing subprocess behavior), use `run_server_in_process`:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from fastmcp.utilities.tests import run_server_in_process
|
||||
from fastmcp import FastMCP, Client
|
||||
from fastmcp.client.transports import StreamableHttpTransport
|
||||
|
||||
def run_server(host: str, port: int) -> None:
|
||||
"""Function to run in subprocess."""
|
||||
server = FastMCP("TestServer")
|
||||
|
||||
@server.tool
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
server.run(host=host, port=port)
|
||||
|
||||
@pytest.fixture
|
||||
async def http_server():
|
||||
"""Fixture that runs server in subprocess."""
|
||||
with run_server_in_process(run_server, transport="http") as url:
|
||||
yield f"{url}/mcp"
|
||||
|
||||
async def test_http_transport(http_server: str):
|
||||
"""Test actual HTTP transport behavior."""
|
||||
async with Client(
|
||||
transport=StreamableHttpTransport(http_server)
|
||||
) as client:
|
||||
result = await client.ping()
|
||||
assert result is True
|
||||
```
|
||||
|
||||
The `run_server_in_process` utility handles server lifecycle, port allocation, and cleanup automatically. Use this only when subprocess isolation is truly necessary, as it's slower and harder to debug than in-process testing. FastMCP uses the `client_process` marker to isolate these tests in CI.
|
||||
|
||||
### Documentation Testing
|
||||
|
||||
Documentation requires the same validation as code. The `just docs` command launches a local Mintlify server that renders your documentation exactly as users will see it:
|
||||
|
||||
```bash
|
||||
# Start local documentation server with hot reload
|
||||
just docs
|
||||
|
||||
# Or run Mintlify directly
|
||||
mintlify dev
|
||||
```
|
||||
|
||||
The local server watches for changes and automatically refreshes. This preview catches formatting issues and helps you see documentation as users will experience it.
|
||||
73
docs/v3/development/v3-notes/auth-provider-env-vars.mdx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
title: Auth Provider Environment Variables
|
||||
---
|
||||
|
||||
## Decision: Remove automatic environment variable loading from auth providers
|
||||
|
||||
You can still use environment variables for configuration - you just read them yourself with `os.environ` instead of relying on FastMCP's automatic loading.
|
||||
|
||||
**Status:** Implemented in v3.0.0
|
||||
|
||||
### Background
|
||||
|
||||
Auth providers in v2.x used `pydantic-settings` to automatically load configuration from environment variables with a `FASTMCP_SERVER_AUTH_<PROVIDER>_` prefix. For example, `GitHubProvider` would read from:
|
||||
|
||||
- `FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID`
|
||||
- `FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET`
|
||||
- `FASTMCP_SERVER_AUTH_GITHUB_BASE_URL`
|
||||
- etc.
|
||||
|
||||
This was implemented via a `*ProviderSettings(BaseSettings)` class in each provider, combined with a `NotSet` sentinel pattern to distinguish between "not provided" and `None`.
|
||||
|
||||
### Why remove it
|
||||
|
||||
1. **Maintenance burden**: Every new provider needed to implement the settings class, validators, and the `NotSet` merging logic. This was ~50-100 lines of boilerplate per provider.
|
||||
|
||||
2. **Documentation complexity**: Each provider needed documentation explaining both the parameter and the corresponding environment variable. This doubled the surface area to document and maintain.
|
||||
|
||||
3. **Contributor friction**: New contributors adding providers had to understand and replicate this pattern, which was a source of inconsistency and bugs.
|
||||
|
||||
4. **Marginal user value**: Python developers are comfortable with `os.environ["VAR"]` or `os.environ.get("VAR", default)`. The automatic loading saved a single line of code per parameter while adding significant complexity.
|
||||
|
||||
5. **Implicit behavior**: Magic environment variable loading makes it harder to understand where values come from. Explicit `os.environ` calls are more traceable.
|
||||
|
||||
### Migration path
|
||||
|
||||
The migration is trivial - users add explicit environment variable reads:
|
||||
|
||||
```python
|
||||
# Before (v2.x)
|
||||
auth = GitHubProvider() # Relied on env vars
|
||||
|
||||
# After (v3.0)
|
||||
import os
|
||||
|
||||
auth = GitHubProvider(
|
||||
client_id=os.environ["GITHUB_CLIENT_ID"],
|
||||
client_secret=os.environ["GITHUB_CLIENT_SECRET"],
|
||||
base_url=os.environ["MY_BASE_URL"],
|
||||
)
|
||||
```
|
||||
|
||||
Users can also use `os.environ.get()` with defaults, or any other configuration library they prefer (dotenv, dynaconf, etc.).
|
||||
|
||||
### Backwards compatibility
|
||||
|
||||
We chose not to provide backwards compatibility because:
|
||||
|
||||
1. This is a major version bump (v3.0), which is the appropriate time for breaking changes
|
||||
2. The migration is straightforward (add `os.environ` calls)
|
||||
3. Maintaining compatibility would require keeping all the boilerplate we're trying to remove
|
||||
4. The pattern was likely not heavily used - most production deployments pass secrets explicitly rather than relying on magic prefixes
|
||||
|
||||
### What was removed
|
||||
|
||||
- `*ProviderSettings(BaseSettings)` classes from all auth providers
|
||||
- `NotSet` sentinel usage in provider constructors
|
||||
- `pydantic-settings` dependency for auth providers
|
||||
- Environment variable documentation from provider docs
|
||||
- Related test cases for env var loading
|
||||
|
||||
### Result
|
||||
|
||||
Provider constructors are now simple and explicit. Required parameters are actually required (Python raises `TypeError` if missing), and optional parameters have clear defaults. The code is more readable and easier to maintain.
|
||||
1481
docs/v3/development/v3-notes/v3-features.mdx
Normal file
118
docs/v3/getting-started/installation.mdx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
---
|
||||
title: Installation
|
||||
description: Install FastMCP and verify your setup
|
||||
icon: arrow-down-to-line
|
||||
---
|
||||
## Install FastMCP
|
||||
|
||||
We recommend using [uv](https://docs.astral.sh/uv/getting-started/installation/) to install and manage FastMCP.
|
||||
|
||||
```bash
|
||||
pip install fastmcp
|
||||
```
|
||||
|
||||
Or with uv:
|
||||
|
||||
```bash
|
||||
uv add fastmcp
|
||||
```
|
||||
|
||||
### Optional Dependencies
|
||||
|
||||
FastMCP provides optional extras for specific features. For example, to install the background tasks extra:
|
||||
|
||||
```bash
|
||||
pip install "fastmcp[tasks]"
|
||||
```
|
||||
|
||||
See [Background Tasks](/servers/tasks) for details on the task system.
|
||||
|
||||
### Verify Installation
|
||||
|
||||
To verify that FastMCP is installed correctly, you can run the following command:
|
||||
|
||||
```bash
|
||||
fastmcp version
|
||||
```
|
||||
|
||||
You should see output like the following:
|
||||
|
||||
```bash
|
||||
$ fastmcp version
|
||||
|
||||
FastMCP version: 3.0.0
|
||||
MCP version: 1.25.0
|
||||
Python version: 3.12.2
|
||||
Platform: macOS-15.3.1-arm64-arm-64bit
|
||||
FastMCP root path: ~/Developer/fastmcp
|
||||
```
|
||||
|
||||
### Dependency Licensing
|
||||
|
||||
<Info>
|
||||
FastMCP depends on Cyclopts for CLI functionality. Cyclopts v4 includes docutils as a transitive dependency, which has complex licensing that may trigger compliance reviews in some organizations.
|
||||
|
||||
If this is a concern, you can install Cyclopts v5 alpha which removes this dependency:
|
||||
|
||||
```bash
|
||||
pip install "cyclopts>=5.0.0a1"
|
||||
```
|
||||
|
||||
Alternatively, wait for the stable v5 release. See [this issue](https://github.com/BrianPugh/cyclopts/issues/672) for details.
|
||||
</Info>
|
||||
## Upgrading
|
||||
|
||||
### From FastMCP 2.0
|
||||
|
||||
See the [Upgrade Guide](/getting-started/upgrading/from-fastmcp-2) for a complete list of breaking changes and migration steps.
|
||||
|
||||
### From the MCP SDK
|
||||
|
||||
#### From FastMCP 1.0
|
||||
|
||||
If you're using FastMCP 1.0 via the `mcp` package (meaning you import FastMCP as `from mcp.server.fastmcp import FastMCP`), upgrading is straightforward — for most servers, it's a single import change. See the [full upgrade guide](/getting-started/upgrading/from-mcp-sdk) for details.
|
||||
|
||||
#### From the Low-Level Server API
|
||||
|
||||
If you built your server directly on the `mcp` package's `Server` class — with `list_tools()`/`call_tool()` handlers and hand-written JSON Schema — see the [migration guide](/getting-started/upgrading/from-low-level-sdk) for a full walkthrough.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `import fastmcp` fails after a pip upgrade
|
||||
|
||||
This affects one specific case: upgrading to FastMCP 3.3 or later from FastMCP 3.2 or earlier with `pip`. Fresh installs and `uv` upgrades are unaffected, so you can skip this unless you did exactly that.
|
||||
|
||||
If `import fastmcp` raises `ModuleNotFoundError`, or `from fastmcp import FastMCP` raises `ImportError`, immediately after the upgrade, your install is in a half-removed state. Reinstall in a single step:
|
||||
|
||||
```bash
|
||||
pip install --force-reinstall fastmcp
|
||||
```
|
||||
|
||||
If that doesn't resolve it, remove both distributions and reinstall from a clean state:
|
||||
|
||||
```bash
|
||||
pip uninstall -y fastmcp fastmcp-slim
|
||||
pip install fastmcp
|
||||
```
|
||||
|
||||
FastMCP 3.3 moved the importable code from the `fastmcp` distribution into `fastmcp-slim`. During a single-command `pip` upgrade, pip can install the new files and then delete them while uninstalling the old `fastmcp` distribution, whose file manifest still lists those paths. `uv` uninstalls before it installs, so it is unaffected.
|
||||
|
||||
## Versioning Policy
|
||||
|
||||
FastMCP follows semantic versioning with pragmatic adaptations for the rapidly evolving MCP ecosystem. Breaking changes may occur in minor versions (e.g., 2.3.x to 2.4.0) when necessary to stay current with the MCP Protocol.
|
||||
|
||||
For production use, always pin to exact versions:
|
||||
```
|
||||
fastmcp==3.0.0 # Good
|
||||
fastmcp>=3.0.0 # Bad - may install breaking changes
|
||||
```
|
||||
|
||||
See the full [versioning and release policy](/development/releases#versioning-policy) for details on our public API, deprecation practices, and breaking change philosophy.
|
||||
|
||||
## Contributing to FastMCP
|
||||
|
||||
Interested in contributing to FastMCP? See the [Contributing Guide](/development/contributing) for details on:
|
||||
- Setting up your development environment
|
||||
- Running tests and pre-commit hooks
|
||||
- Submitting issues and pull requests
|
||||
- Code standards and review process
|
||||
164
docs/v3/getting-started/quickstart.mdx
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
---
|
||||
title: Quickstart
|
||||
icon: rocket-launch
|
||||
---
|
||||
|
||||
Welcome! This guide will help you quickly set up FastMCP, run your first MCP server, give it a visual UI, and deploy it to Prefect Horizon.
|
||||
|
||||
If you haven't already installed FastMCP, follow the [installation instructions](/getting-started/installation).
|
||||
|
||||
## Create a FastMCP Server
|
||||
|
||||
A FastMCP server is a collection of tools, resources, and other MCP components. To create a server, start by instantiating the `FastMCP` class.
|
||||
|
||||
Create a new file called `my_server.py` and add the following code:
|
||||
|
||||
```python my_server.py
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("My MCP Server")
|
||||
```
|
||||
|
||||
|
||||
That's it! You've created a FastMCP server, albeit a very boring one. Let's add a tool to make it more interesting.
|
||||
|
||||
|
||||
## Add a Tool
|
||||
|
||||
To add a tool that returns a simple greeting, write a function and decorate it with `@mcp.tool` to register it with the server:
|
||||
|
||||
```python my_server.py {5-7}
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("My MCP Server")
|
||||
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
```
|
||||
|
||||
|
||||
## Run the Server
|
||||
|
||||
The simplest way to run your FastMCP server is to call its `run()` method. You can choose between different transports, like `stdio` for local servers, or `http` for remote access:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python my_server.py (stdio) {9, 10}
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("My MCP Server")
|
||||
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
```python my_server.py (HTTP) {9, 10}
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("My MCP Server")
|
||||
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="http", port=8000)
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
This lets us run the server with `python my_server.py`. The stdio transport is the traditional way to connect MCP servers to clients, while the HTTP transport enables remote connections.
|
||||
|
||||
<Tip>
|
||||
Why do we need the `if __name__ == "__main__":` block?
|
||||
|
||||
The `__main__` block is recommended for consistency and compatibility, ensuring your server works with all MCP clients that execute your server file as a script. Users who will exclusively run their server with the FastMCP CLI can omit it, as the CLI imports the server object directly.
|
||||
</Tip>
|
||||
|
||||
### Using the FastMCP CLI
|
||||
|
||||
You can also use the `fastmcp run` command to start your server. Note that the FastMCP CLI **does not** execute the `__main__` block of your server file. Instead, it imports your server object and runs it with whatever transport and options you provide.
|
||||
|
||||
For example, to run this server with the default stdio transport (no matter how you called `mcp.run()`), you can use the following command:
|
||||
```bash
|
||||
fastmcp run my_server.py:mcp
|
||||
```
|
||||
|
||||
To run this server with the HTTP transport, you can use the following command:
|
||||
```bash
|
||||
fastmcp run my_server.py:mcp --transport http --port 8000
|
||||
```
|
||||
|
||||
## Call Your Server
|
||||
|
||||
Once your server is running with HTTP transport, you can connect to it with a FastMCP client or any LLM client that supports the MCP protocol:
|
||||
|
||||
```python my_client.py
|
||||
import asyncio
|
||||
from fastmcp import Client
|
||||
|
||||
client = Client("http://localhost:8000/mcp")
|
||||
|
||||
async def call_tool(name: str):
|
||||
async with client:
|
||||
result = await client.call_tool("greet", {"name": name})
|
||||
print(result)
|
||||
|
||||
asyncio.run(call_tool("Ford"))
|
||||
```
|
||||
|
||||
Note that:
|
||||
- FastMCP clients are asynchronous, so we need to use `asyncio.run` to run the client
|
||||
- We must enter a client context (`async with client:`) before using the client
|
||||
- You can make multiple client calls within the same context
|
||||
|
||||
## Give Your Tool a UI
|
||||
|
||||
Tools normally return text, but any tool can return an interactive UI instead. Add `app=True` to your tool decorator and return a [Prefab](https://prefab.prefect.io) component — the host renders it as a chart, table, form, or any other visual element right in the conversation. This requires the `apps` extra (`pip install "fastmcp[apps]"`).
|
||||
|
||||
The `app=True` flag tells FastMCP to wire up the renderer and protocol metadata automatically. The tool still works like any other MCP tool — it receives arguments and returns a result — but the result is a component tree that the host displays visually instead of as plain text.
|
||||
|
||||
```python my_server.py
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, Heading, Text, Badge, Row
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("My MCP Server")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def greet(name: str) -> PrefabApp:
|
||||
"""Greet someone with a visual card."""
|
||||
with Column(gap=4, css_class="p-6") as view:
|
||||
Heading(f"Hello, {name}!")
|
||||
with Row(gap=2, align="center"):
|
||||
Text("Status")
|
||||
Badge("Greeted", variant="success")
|
||||
|
||||
return PrefabApp(view=view)
|
||||
```
|
||||
|
||||
You can preview app tools locally with `fastmcp dev apps my_server.py` — no MCP host required. See the [Apps overview](/apps/overview) for the full guide, including state management, forms, charts, and server-connected interactivity.
|
||||
|
||||
## Deploy to Prefect Horizon
|
||||
|
||||
[Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides managed hosting, authentication, access control, and observability for MCP servers.
|
||||
|
||||
<Info>
|
||||
Horizon is **free for personal projects** and offers enterprise governance for teams.
|
||||
</Info>
|
||||
|
||||
To deploy your server, you'll need a [GitHub account](https://github.com). Once you have one, you can deploy your server in three steps:
|
||||
|
||||
1. Push your `my_server.py` file to a GitHub repository
|
||||
2. Sign in to [Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) with your GitHub account
|
||||
3. Create a new project from your repository and enter `my_server.py:mcp` as the server entrypoint
|
||||
|
||||
That's it! Horizon will build and deploy your server, making it available at a URL like `https://your-project.fastmcp.app/mcp`. You can chat with it to test its functionality, or connect to it from any LLM client that supports the MCP protocol.
|
||||
|
||||
For more details, see the [Prefect Horizon guide](/deployment/prefect-horizon).
|
||||
444
docs/v3/getting-started/upgrading/from-fastmcp-2.mdx
Normal file
|
|
@ -0,0 +1,444 @@
|
|||
---
|
||||
title: Upgrading from FastMCP 2
|
||||
sidebarTitle: "From FastMCP 2"
|
||||
description: Migration instructions for upgrading between FastMCP versions
|
||||
icon: up
|
||||
---
|
||||
|
||||
This guide covers breaking changes and migration steps when upgrading FastMCP.
|
||||
|
||||
## v3.0.0
|
||||
|
||||
For most servers, upgrading to v3 is straightforward. The breaking changes below affect deprecated constructor kwargs, sync-to-async shifts, a few renamed methods, and some less commonly used features.
|
||||
|
||||
### Install
|
||||
|
||||
Since you already have `fastmcp` installed, you need to explicitly request the new version — `pip install fastmcp` won't upgrade an existing installation:
|
||||
|
||||
```bash
|
||||
pip install --upgrade fastmcp
|
||||
# or
|
||||
uv add --upgrade fastmcp
|
||||
```
|
||||
|
||||
If you pin versions in a requirements file or `pyproject.toml`, update your pin to `fastmcp>=3.0.0,<4`.
|
||||
|
||||
<Info>
|
||||
**New repository home.** As part of the v3 release, FastMCP's GitHub repository has moved from `jlowin/fastmcp` to [`PrefectHQ/fastmcp`](https://github.com/PrefectHQ/fastmcp) under [Prefect](https://prefect.io)'s stewardship. GitHub automatically redirects existing clones and bookmarks, so nothing breaks — but you can update your local remote whenever convenient:
|
||||
|
||||
```bash
|
||||
git remote set-url origin https://github.com/PrefectHQ/fastmcp.git
|
||||
```
|
||||
|
||||
If you reference the repository URL in dependency specifications (e.g., `git+https://github.com/jlowin/fastmcp.git`), update those to the new location.
|
||||
</Info>
|
||||
|
||||
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
|
||||
You are upgrading a FastMCP v2 server to FastMCP v3.0. Analyze the provided code and identify every change needed. The full upgrade guide is at https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2 and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context.
|
||||
|
||||
BREAKING CHANGES (will crash at import or runtime):
|
||||
|
||||
1. CONSTRUCTOR KWARGS REMOVED: FastMCP() no longer accepts these kwargs (raises TypeError):
|
||||
- Transport settings: host, port, log_level, debug, sse_path, streamable_http_path, json_response, stateless_http
|
||||
Fix: pass to run() or run_http_async() instead, e.g. mcp.run(transport="http", host="0.0.0.0", port=8080)
|
||||
- message_path: set via environment variable FASTMCP_MESSAGE_PATH only (not a run() kwarg)
|
||||
- Duplicate handling: on_duplicate_tools, on_duplicate_resources, on_duplicate_prompts
|
||||
Fix: use unified on_duplicate= parameter
|
||||
- Tool settings: tool_serializer, include_tags, exclude_tags, tool_transformations
|
||||
Fix: use ToolResult returns, server.enable()/disable(), server.add_transform()
|
||||
|
||||
2. COMPONENT METHODS REMOVED:
|
||||
- tool.enable()/disable() raises NotImplementedError
|
||||
Fix: server.disable(names={"tool_name"}, components={"tool"}) or server.disable(tags={"tag"})
|
||||
- get_tools()/get_resources()/get_prompts()/get_resource_templates() removed
|
||||
Fix: use list_tools()/list_resources()/list_prompts()/list_resource_templates() — these return lists, not dicts
|
||||
|
||||
3. ASYNC STATE: ctx.set_state() and ctx.get_state() are now async (must be awaited).
|
||||
State values must be JSON-serializable unless serializable=False is passed.
|
||||
Each FastMCP instance has its own state store, so serializable state set by parent middleware isn't visible to mounted tools by default.
|
||||
Fix: pass the same session_state_store to both servers, or use serializable=False (request-scoped state is always shared).
|
||||
|
||||
4. PROMPTS: mcp.types.PromptMessage replaced by fastmcp.prompts.Message.
|
||||
Before: PromptMessage(role="user", content=TextContent(type="text", text="Hello"))
|
||||
After: Message("Hello") # role defaults to "user", accepts plain strings
|
||||
Also: if prompts return raw dicts like `{"role": "user", "content": "..."}`, these must become Message objects.
|
||||
v2 silently coerced dicts; v3 requires typed Message objects or plain strings.
|
||||
|
||||
5. AUTH PROVIDERS: No longer auto-load from env vars. Pass client_id, client_secret explicitly via os.environ.
|
||||
|
||||
6. WSTRANSPORT: Removed. Use StreamableHttpTransport.
|
||||
|
||||
7. OPENAPI: timeout parameter removed from OpenAPIProvider. Set timeout on the httpx.AsyncClient instead.
|
||||
|
||||
8. METADATA: Namespace changed from "_fastmcp" to "fastmcp" in tool.meta. The include_fastmcp_meta parameter is removed (always included).
|
||||
|
||||
9. ENV VAR: FASTMCP_SHOW_CLI_BANNER renamed to FASTMCP_SHOW_SERVER_BANNER.
|
||||
|
||||
10. DECORATORS: @mcp.tool, @mcp.resource, @mcp.prompt now return the original function, not a component object. Code that accesses .name, .description, or other component attributes on the decorated result will crash with AttributeError.
|
||||
Fix: set FASTMCP_DECORATOR_MODE=object for v2 compat (itself deprecated).
|
||||
|
||||
11. OAUTH STORAGE: Default OAuth client storage changed from DiskStore to FileTreeStore due to pickle deserialization vulnerability in diskcache (CVE-2025-69872). Clients using default storage will re-register automatically on first connection. If using DiskStore explicitly, switch to FileTreeStore (with key/collection sanitization strategies) or add pip install 'py-key-value-aio[disk]'.
|
||||
|
||||
12. REPO MOVE: GitHub repository moved from jlowin/fastmcp to PrefectHQ/fastmcp. Update git remotes and dependency URLs that reference the old location.
|
||||
|
||||
13. BACKGROUND TASKS: FastMCP's background task system (SEP-1686) is now an optional dependency. If the code uses task=True or TaskConfig, add pip install "fastmcp[tasks]".
|
||||
|
||||
DEPRECATIONS (still work but emit warnings):
|
||||
|
||||
- mount(prefix="x") -> mount(namespace="x")
|
||||
- import_server(sub) -> mount(sub)
|
||||
- FastMCP.as_proxy(url) -> from fastmcp.server import create_proxy; create_proxy(url)
|
||||
- from fastmcp.server.proxy -> from fastmcp.server.providers.proxy
|
||||
- from fastmcp.server.openapi import FastMCPOpenAPI -> from fastmcp.server.providers.openapi import OpenAPIProvider; use FastMCP("name", providers=[OpenAPIProvider(...)])
|
||||
- mcp.add_tool_transformation(name, cfg) -> from fastmcp.server.transforms import ToolTransform; mcp.add_transform(ToolTransform(...))
|
||||
|
||||
For each issue found, show the original line, explain why it breaks, and provide the corrected code.
|
||||
</Prompt>
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
**Transport and server settings removed from constructor**
|
||||
|
||||
In v2, you could configure transport settings directly in the `FastMCP()` constructor. In v3, `FastMCP()` is purely about your server's identity and behavior — transport configuration happens when you actually start serving. Passing any of the old kwargs now raises `TypeError` with a migration hint.
|
||||
|
||||
```python
|
||||
# Before
|
||||
mcp = FastMCP("server", host="0.0.0.0", port=8080)
|
||||
mcp.run()
|
||||
|
||||
# After
|
||||
mcp = FastMCP("server")
|
||||
mcp.run(transport="http", host="0.0.0.0", port=8080)
|
||||
```
|
||||
|
||||
The full list of removed kwargs and their replacements:
|
||||
|
||||
- `host`, `port`, `log_level`, `debug`, `sse_path`, `streamable_http_path`, `json_response`, `stateless_http` — pass to `run()`, `run_http_async()`, or `http_app()`, or set via environment variables (e.g. `FASTMCP_HOST`)
|
||||
- `message_path` — set via environment variable `FASTMCP_MESSAGE_PATH` only (not a `run()` kwarg)
|
||||
- `on_duplicate_tools`, `on_duplicate_resources`, `on_duplicate_prompts` — consolidated into a single `on_duplicate=` parameter
|
||||
- `tool_serializer` — return [`ToolResult`](/servers/tools#custom-serialization) from your tools instead
|
||||
- `include_tags` / `exclude_tags` — use `server.enable(tags=..., only=True)` / `server.disable(tags=...)` after construction
|
||||
- `tool_transformations` — use `server.add_transform(ToolTransform(...))` after construction
|
||||
|
||||
**OAuth storage backend changed (diskcache CVE)**
|
||||
|
||||
The default OAuth client storage has moved from `DiskStore` to `FileTreeStore` to address a pickle deserialization vulnerability in diskcache ([CVE-2025-69872](https://github.com/PrefectHQ/fastmcp/issues/3166)).
|
||||
|
||||
If you were using the default storage (i.e., not passing an explicit `client_storage`), clients will need to re-register on their first connection after upgrading. This happens automatically — no user action required, and it's the same flow that already occurs whenever a server restarts with in-memory storage.
|
||||
|
||||
If you were passing a `DiskStore` explicitly, you can either [switch to `FileTreeStore`](/servers/storage-backends) (recommended) or keep using `DiskStore` by adding the dependency yourself.
|
||||
|
||||
<Warning>
|
||||
When switching to `FileTreeStore`, you **must** configure key and collection sanitization strategies. Without them, keys containing special characters (such as URL-based OAuth client IDs) will cause filesystem errors. See the [File Storage](/servers/storage-backends#file-storage) section for the recommended setup.
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
Keeping `DiskStore` requires `pip install 'py-key-value-aio[disk]'`, which re-introduces the vulnerable `diskcache` package into your dependency tree.
|
||||
</Warning>
|
||||
|
||||
**Component enable()/disable() moved to server**
|
||||
|
||||
In v2, you could enable or disable individual components by calling methods on the component object itself. In v3, visibility is controlled through the server (or provider), which lets you target components by name, tag, or type without needing a reference to the object:
|
||||
|
||||
```python
|
||||
# Before
|
||||
tool = await server.get_tool("my_tool")
|
||||
tool.disable()
|
||||
|
||||
# After
|
||||
server.disable(names={"my_tool"}, components={"tool"})
|
||||
```
|
||||
|
||||
Calling `.enable()` or `.disable()` on a component object now raises `NotImplementedError`. See [Visibility](/servers/visibility) for the full API, including tag-based filtering and per-session visibility.
|
||||
|
||||
**Listing methods renamed and return lists**
|
||||
|
||||
The `get_tools()`, `get_resources()`, `get_prompts()`, and `get_resource_templates()` methods have been renamed to `list_tools()`, `list_resources()`, `list_prompts()`, and `list_resource_templates()`. More importantly, they now return lists instead of dicts — so code that indexes by name needs to change:
|
||||
|
||||
```python
|
||||
# Before
|
||||
tools = await server.get_tools()
|
||||
tool = tools["my_tool"]
|
||||
|
||||
# After
|
||||
tools = await server.list_tools()
|
||||
tool = next((t for t in tools if t.name == "my_tool"), None)
|
||||
```
|
||||
|
||||
**Prompts use Message class**
|
||||
|
||||
Prompt functions now use FastMCP's `Message` class instead of `mcp.types.PromptMessage`. The new class is simpler — it accepts a plain string and defaults to `role="user"`, so most prompts become one-liners:
|
||||
|
||||
```python
|
||||
# Before
|
||||
from mcp.types import PromptMessage, TextContent
|
||||
|
||||
@mcp.prompt
|
||||
def my_prompt() -> PromptMessage:
|
||||
return PromptMessage(role="user", content=TextContent(type="text", text="Hello"))
|
||||
|
||||
# After
|
||||
from fastmcp.prompts import Message
|
||||
|
||||
@mcp.prompt
|
||||
def my_prompt() -> Message:
|
||||
return Message("Hello")
|
||||
```
|
||||
|
||||
If your prompt functions return raw dicts with `role` and `content` keys, those also need to change. v2 silently coerced dicts into prompt messages, but v3 requires typed `Message` objects (or plain strings for single user messages):
|
||||
|
||||
```python
|
||||
# Before (v2 accepted this)
|
||||
@mcp.prompt
|
||||
def my_prompt():
|
||||
return [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "How can I help?"},
|
||||
]
|
||||
|
||||
# After
|
||||
from fastmcp.prompts import Message
|
||||
|
||||
@mcp.prompt
|
||||
def my_prompt() -> list[Message]:
|
||||
return [
|
||||
Message("Hello"),
|
||||
Message("How can I help?", role="assistant"),
|
||||
]
|
||||
```
|
||||
|
||||
**Context state methods are async**
|
||||
|
||||
`ctx.set_state()` and `ctx.get_state()` are now async because state in v3 is session-scoped and backed by a pluggable storage backend (rather than a simple dict). This means state persists across multiple tool calls within the same session:
|
||||
|
||||
```python
|
||||
# Before
|
||||
ctx.set_state("key", "value")
|
||||
value = ctx.get_state("key")
|
||||
|
||||
# After
|
||||
await ctx.set_state("key", "value")
|
||||
value = await ctx.get_state("key")
|
||||
```
|
||||
|
||||
State values must also be JSON-serializable by default (dicts, lists, strings, numbers, etc.). If you need to store non-serializable values like an HTTP client, pass `serializable=False` — these values are request-scoped and only available during the current tool call:
|
||||
|
||||
```python
|
||||
await ctx.set_state("client", my_http_client, serializable=False)
|
||||
```
|
||||
|
||||
**Mounted servers have isolated state stores**
|
||||
|
||||
Each `FastMCP` instance has its own state store. In v2 this wasn't noticeable because mounted tools ran in the parent's context, but in v3's provider architecture each server is isolated. Non-serializable state (`serializable=False`) is request-scoped and automatically shared across mount boundaries. For serializable state, pass the same `session_state_store` to both servers:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from key_value.aio.stores.memory import MemoryStore
|
||||
|
||||
store = MemoryStore()
|
||||
parent = FastMCP("Parent", session_state_store=store)
|
||||
child = FastMCP("Child", session_state_store=store)
|
||||
parent.mount(child, namespace="child")
|
||||
```
|
||||
|
||||
**Auth provider environment variables removed**
|
||||
|
||||
In v2, auth providers like `GitHubProvider` could auto-load configuration from environment variables with a `FASTMCP_SERVER_AUTH_*` prefix. This magic has been removed — pass values explicitly:
|
||||
|
||||
```python
|
||||
# Before (v2) — client_id and client_secret loaded automatically
|
||||
# from FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID, etc.
|
||||
auth = GitHubProvider()
|
||||
|
||||
# After (v3) — pass values explicitly
|
||||
import os
|
||||
from fastmcp.server.auth.providers.github import GitHubProvider
|
||||
|
||||
auth = GitHubProvider(
|
||||
client_id=os.environ["GITHUB_CLIENT_ID"],
|
||||
client_secret=os.environ["GITHUB_CLIENT_SECRET"],
|
||||
)
|
||||
```
|
||||
|
||||
**WSTransport removed**
|
||||
|
||||
The deprecated WebSocket client transport has been removed. Use `StreamableHttpTransport` instead:
|
||||
|
||||
```python test="skip"
|
||||
# Before
|
||||
from fastmcp.client.transports import WSTransport
|
||||
transport = WSTransport("ws://localhost:8000/ws")
|
||||
|
||||
# After
|
||||
from fastmcp.client.transports import StreamableHttpTransport
|
||||
transport = StreamableHttpTransport("http://localhost:8000/mcp")
|
||||
```
|
||||
|
||||
**OpenAPI `timeout` parameter removed**
|
||||
|
||||
`OpenAPIProvider` no longer accepts a `timeout` parameter. Configure timeout on the httpx client directly. The `client` parameter is also now optional — when omitted, a default client is created from the spec's `servers` URL with a 30-second timeout:
|
||||
|
||||
```python
|
||||
# Before
|
||||
provider = OpenAPIProvider(spec, client, timeout=60)
|
||||
|
||||
# After
|
||||
client = httpx.AsyncClient(base_url="https://api.example.com", timeout=60)
|
||||
provider = OpenAPIProvider(spec, client)
|
||||
```
|
||||
|
||||
**Metadata namespace renamed**
|
||||
|
||||
The FastMCP metadata key in component `meta` dicts changed from `_fastmcp` to `fastmcp`. If you read metadata from tool or resource objects, update the key:
|
||||
|
||||
```python
|
||||
# Before
|
||||
tags = tool.meta.get("_fastmcp", {}).get("tags", [])
|
||||
|
||||
# After
|
||||
tags = tool.meta.get("fastmcp", {}).get("tags", [])
|
||||
```
|
||||
|
||||
Metadata is now always included — the `include_fastmcp_meta` parameter has been removed from `FastMCP()` and `to_mcp_tool()`, so there is no way to suppress it.
|
||||
|
||||
**Server banner environment variable renamed**
|
||||
|
||||
`FASTMCP_SHOW_CLI_BANNER` is now `FASTMCP_SHOW_SERVER_BANNER`.
|
||||
|
||||
**Decorators return functions**
|
||||
|
||||
In v2, `@mcp.tool` transformed your function into a `FunctionTool` object. In v3, decorators return your original function unchanged — which means decorated functions stay callable for testing, reuse, and composition:
|
||||
|
||||
```python
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
greet("World") # Works! Returns "Hello, World!"
|
||||
```
|
||||
|
||||
If you have code that treats the decorated result as a `FunctionTool` (e.g., accessing `.name` or `.description`), set `FASTMCP_DECORATOR_MODE=object` for v2 compatibility. This escape hatch is itself deprecated and will be removed in a future release.
|
||||
|
||||
**Background tasks require optional dependency**
|
||||
|
||||
FastMCP's background task system (SEP-1686) is now behind an optional extra. If your server uses background tasks, install with:
|
||||
|
||||
```bash
|
||||
pip install "fastmcp[tasks]"
|
||||
```
|
||||
|
||||
Without the extra, configuring a tool with `task=True` or `TaskConfig` will raise an import error at runtime. See [Background Tasks](/servers/tasks) for details.
|
||||
|
||||
### Deprecated Features
|
||||
|
||||
These still work but emit warnings. Update when convenient.
|
||||
|
||||
**mount() prefix → namespace**
|
||||
|
||||
```python
|
||||
# Deprecated
|
||||
main.mount(subserver, prefix="api")
|
||||
|
||||
# New
|
||||
main.mount(subserver, namespace="api")
|
||||
```
|
||||
|
||||
**import_server() → mount()**
|
||||
|
||||
```python
|
||||
# Deprecated
|
||||
main.import_server(subserver)
|
||||
|
||||
# New
|
||||
main.mount(subserver)
|
||||
```
|
||||
|
||||
**Module import paths for proxy and OpenAPI**
|
||||
|
||||
The proxy and OpenAPI modules have moved under `providers` to reflect v3's provider-based architecture:
|
||||
|
||||
```python test="skip"
|
||||
# Deprecated
|
||||
from fastmcp.server.proxy import FastMCPProxy
|
||||
from fastmcp.server.openapi import FastMCPOpenAPI
|
||||
|
||||
# New
|
||||
from fastmcp.server.providers.proxy import FastMCPProxy
|
||||
from fastmcp.server.providers.openapi import OpenAPIProvider
|
||||
```
|
||||
|
||||
`FastMCPOpenAPI` itself is deprecated — use `FastMCP` with an `OpenAPIProvider` instead:
|
||||
|
||||
```python test="skip"
|
||||
# Deprecated
|
||||
from fastmcp.server.openapi import FastMCPOpenAPI
|
||||
server = FastMCPOpenAPI(spec, client)
|
||||
|
||||
# New
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.providers.openapi import OpenAPIProvider
|
||||
server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)])
|
||||
```
|
||||
|
||||
**add_tool_transformation() → add_transform()**
|
||||
|
||||
```python
|
||||
# Deprecated
|
||||
mcp.add_tool_transformation("name", config)
|
||||
|
||||
# New
|
||||
from fastmcp.server.transforms import ToolTransform
|
||||
mcp.add_transform(ToolTransform({"name": config}))
|
||||
```
|
||||
|
||||
**FastMCP.as_proxy() → create_proxy()**
|
||||
|
||||
```python
|
||||
# Deprecated
|
||||
proxy = FastMCP.as_proxy("http://example.com/mcp")
|
||||
|
||||
# New
|
||||
from fastmcp.server import create_proxy
|
||||
proxy = create_proxy("http://example.com/mcp")
|
||||
```
|
||||
|
||||
## v2.14.0
|
||||
|
||||
### OpenAPI Parser Promotion
|
||||
|
||||
The experimental OpenAPI parser is now standard. Update imports:
|
||||
|
||||
```python test="skip"
|
||||
# Before
|
||||
from fastmcp.experimental.server.openapi import FastMCPOpenAPI
|
||||
|
||||
# After
|
||||
from fastmcp.server.openapi import FastMCPOpenAPI
|
||||
```
|
||||
|
||||
### Removed Deprecated Features
|
||||
|
||||
- `BearerAuthProvider` → use `JWTVerifier`
|
||||
- `Context.get_http_request()` → use `get_http_request()` from dependencies
|
||||
- `from fastmcp import Image` → use `from fastmcp.utilities.types import Image`
|
||||
- `FastMCP(dependencies=[...])` → use `fastmcp.json` configuration
|
||||
- `FastMCPProxy(client=...)` → use `client_factory=lambda: ...`
|
||||
- `output_schema=False` → use `output_schema=None`
|
||||
|
||||
## v2.13.0
|
||||
|
||||
### OAuth Token Key Management
|
||||
|
||||
The OAuth proxy now issues its own JWT tokens. For production, provide explicit keys:
|
||||
|
||||
```python
|
||||
auth = GitHubProvider(
|
||||
client_id=os.environ["GITHUB_CLIENT_ID"],
|
||||
client_secret=os.environ["GITHUB_CLIENT_SECRET"],
|
||||
base_url="https://your-server.com",
|
||||
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
|
||||
client_storage=RedisStore(host="redis.example.com"),
|
||||
)
|
||||
```
|
||||
|
||||
See [OAuth Token Security](/deployment/http#oauth-token-security) for details.
|
||||
592
docs/v3/getting-started/upgrading/from-low-level-sdk.mdx
Normal file
|
|
@ -0,0 +1,592 @@
|
|||
---
|
||||
title: Upgrading from the MCP Low-Level SDK
|
||||
sidebarTitle: "From MCP Low-Level SDK"
|
||||
description: Upgrade your MCP server from the low-level Python SDK's Server class to FastMCP
|
||||
icon: up
|
||||
---
|
||||
|
||||
If you've been building MCP servers directly on the `mcp` package's `Server` class — writing `list_tools()` and `call_tool()` handlers, hand-crafting JSON Schema dicts, and wiring up transport boilerplate — this guide is for you. FastMCP replaces all of that machinery with a declarative, Pythonic API where your functions *are* the protocol surface.
|
||||
|
||||
The core idea: instead of telling the SDK what your tools look like and then separately implementing them, you write ordinary Python functions and let FastMCP derive the protocol layer from your code. Type hints become JSON Schema. Docstrings become descriptions. Return values are serialized automatically. The plumbing you wrote to satisfy the protocol just disappears.
|
||||
|
||||
<Note>
|
||||
This guide covers upgrading from **v1** of the `mcp` package. We'll provide a separate guide when v2 ships.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
Already using FastMCP 1.0 via `from mcp.server.fastmcp import FastMCP`? Your upgrade is simpler — see the [FastMCP 1.0 upgrade guide](/getting-started/upgrading/from-mcp-sdk) instead.
|
||||
</Note>
|
||||
|
||||
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
|
||||
You are upgrading an MCP server from the `mcp` package's low-level Server class (v1) to FastMCP 3.0. The server currently uses `mcp.server.Server` (or `mcp.server.lowlevel.server.Server`) with manual handler registration. Analyze the provided code and rewrite it using FastMCP's high-level API. The full guide is at https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context.
|
||||
|
||||
UPGRADE RULES:
|
||||
|
||||
1. IMPORTS: Replace all `mcp.*` imports with FastMCP equivalents.
|
||||
- `from mcp.server import Server` or `from mcp.server.lowlevel.server import Server` → `from fastmcp import FastMCP`
|
||||
- `import mcp.types as types` → remove (not needed for most code)
|
||||
- `from mcp.server.stdio import stdio_server` → remove (handled by mcp.run())
|
||||
- `from mcp.server.sse import SseServerTransport` → remove (handled by mcp.run())
|
||||
|
||||
2. SERVER: Replace `Server("name")` with `FastMCP("name")`.
|
||||
|
||||
3. TOOLS: Replace the list_tools + call_tool handler pair with individual @mcp.tool decorators.
|
||||
- Delete the `@server.list_tools()` handler entirely
|
||||
- Delete the `@server.call_tool()` handler entirely
|
||||
- For each tool that was listed in list_tools and dispatched in call_tool, create a new function:
|
||||
- Decorate it with `@mcp.tool`
|
||||
- Use the tool name as the function name (or pass name= to the decorator)
|
||||
- Use the docstring for the description (or pass description= to the decorator)
|
||||
- Convert the inputSchema JSON Schema into typed Python parameters (e.g., `{"type": "integer"}` → `int`, `{"type": "string"}` → `str`, `{"type": "array", "items": {"type": "string"}}` → `list[str]`)
|
||||
- Return plain Python values (`str`, `int`, `dict`, etc.) instead of `list[types.TextContent(...)]`
|
||||
- If the tool returned `types.ImageContent` or `types.EmbeddedResource`, use `from fastmcp.utilities.types import Image` or return the appropriate type
|
||||
|
||||
4. RESOURCES: Replace the list_resources + list_resource_templates + read_resource handler trio with individual @mcp.resource decorators.
|
||||
- Delete all three handlers
|
||||
- For each static resource, create a function decorated with `@mcp.resource("uri://...")`
|
||||
- For each resource template, use `@mcp.resource("uri://{param}/path")` with `{param}` in the URI and a matching function parameter
|
||||
- Return str for text content, bytes for binary content
|
||||
- Set `mime_type=` in the decorator if needed
|
||||
|
||||
5. PROMPTS: Replace the list_prompts + get_prompt handler pair with individual @mcp.prompt decorators.
|
||||
- Delete both handlers
|
||||
- For each prompt, create a function decorated with `@mcp.prompt`
|
||||
- Convert PromptArgument definitions into typed function parameters
|
||||
- Return str for simple single-message prompts (auto-wrapped as user message)
|
||||
- Return `list[Message]` for multi-message prompts: `from fastmcp.prompts import Message`
|
||||
- `Message("text")` defaults to `role="user"`; use `Message("text", role="assistant")` for assistant messages
|
||||
|
||||
6. TRANSPORT: Replace all transport boilerplate with mcp.run().
|
||||
- `async with stdio_server() as (r, w): await server.run(r, w, ...)` → `mcp.run()` (`stdio` is the default)
|
||||
- SSE/Starlette setup → `mcp.run(transport="sse", host="...", port=...)`
|
||||
- Streamable HTTP setup → `mcp.run(transport="http", host="...", port=...)`
|
||||
- Delete asyncio.run(main()) boilerplate — use `if __name__ == "__main__": mcp.run()`
|
||||
|
||||
7. CONTEXT: Replace `server.request_context` with FastMCP's Context parameter.
|
||||
- Add `from fastmcp import Context` and add a `ctx: Context` parameter to any tool that needs it
|
||||
- `server.request_context.session.send_log_message(...)` → `await ctx.info("message")` or `await ctx.warning("message")`
|
||||
- Progress reporting → `await ctx.report_progress(current, total)`
|
||||
|
||||
For each change, show the original code, explain what it did, and provide the FastMCP equivalent.
|
||||
</Prompt>
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install --upgrade fastmcp
|
||||
# or
|
||||
uv add fastmcp
|
||||
```
|
||||
|
||||
FastMCP includes the `mcp` package as a transitive dependency, so you don't lose access to anything.
|
||||
|
||||
## Server and Transport
|
||||
|
||||
The `Server` class requires you to choose a transport, connect streams, build initialization options, and run an event loop. FastMCP collapses all of that into a constructor and a `run()` call.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
import asyncio
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
||||
server = Server("my-server")
|
||||
|
||||
# ... register handlers ...
|
||||
|
||||
async def main():
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
server.create_initialization_options(),
|
||||
)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```python After
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("my-server")
|
||||
|
||||
# ... register tools, resources, prompts ...
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Need HTTP instead of stdio? With the `Server` class, you'd wire up Starlette routes and `SseServerTransport` or `StreamableHTTPSessionManager`. With FastMCP:
|
||||
|
||||
```python
|
||||
mcp.run(transport="http", host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
This is where the difference is most dramatic. The `Server` class requires two handlers — one to describe your tools (with hand-written JSON Schema) and another to dispatch calls by name. FastMCP eliminates both by deriving everything from your function signature.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
import mcp.types as types
|
||||
from mcp.server import Server
|
||||
|
||||
server = Server("math")
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools() -> list[types.Tool]:
|
||||
return [
|
||||
types.Tool(
|
||||
name="add",
|
||||
description="Add two numbers",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": {"type": "number"},
|
||||
"b": {"type": "number"},
|
||||
},
|
||||
"required": ["a", "b"],
|
||||
},
|
||||
),
|
||||
types.Tool(
|
||||
name="multiply",
|
||||
description="Multiply two numbers",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": {"type": "number"},
|
||||
"b": {"type": "number"},
|
||||
},
|
||||
"required": ["a", "b"],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@server.call_tool()
|
||||
async def call_tool(
|
||||
name: str, arguments: dict
|
||||
) -> list[types.TextContent]:
|
||||
if name == "add":
|
||||
result = arguments["a"] + arguments["b"]
|
||||
return [types.TextContent(type="text", text=str(result))]
|
||||
elif name == "multiply":
|
||||
result = arguments["a"] * arguments["b"]
|
||||
return [types.TextContent(type="text", text=str(result))]
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
```
|
||||
|
||||
```python After
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("math")
|
||||
|
||||
@mcp.tool
|
||||
def add(a: float, b: float) -> float:
|
||||
"""Add two numbers"""
|
||||
return a + b
|
||||
|
||||
@mcp.tool
|
||||
def multiply(a: float, b: float) -> float:
|
||||
"""Multiply two numbers"""
|
||||
return a * b
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Each `@mcp.tool` function is self-contained: its name becomes the tool name, its docstring becomes the description, its type annotations become the JSON Schema, and its return value is serialized automatically. No routing. No schema dictionaries. No content-type wrappers.
|
||||
|
||||
### Type Mapping
|
||||
|
||||
When converting your `inputSchema` to Python type hints:
|
||||
|
||||
| JSON Schema | Python Type |
|
||||
|---|---|
|
||||
| `{"type": "string"}` | `str` |
|
||||
| `{"type": "number"}` | `float` |
|
||||
| `{"type": "integer"}` | `int` |
|
||||
| `{"type": "boolean"}` | `bool` |
|
||||
| `{"type": "array", "items": {"type": "string"}}` | `list[str]` |
|
||||
| `{"type": "object"}` | `dict` |
|
||||
| Optional property (not in `required`) | `param: str \| None = None` |
|
||||
|
||||
### Return Values
|
||||
|
||||
With the `Server` class, tools return `list[types.TextContent | types.ImageContent | ...]`. In FastMCP, return plain Python values — strings, numbers, dicts, lists, dataclasses, Pydantic models — and serialization is handled for you.
|
||||
|
||||
For images or other non-text content, FastMCP provides helpers:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.utilities.types import Image
|
||||
|
||||
mcp = FastMCP("media")
|
||||
|
||||
@mcp.tool
|
||||
def create_chart(data: list[float]) -> Image:
|
||||
"""Generate a chart from data."""
|
||||
png_bytes = generate_chart(data) # your logic
|
||||
return Image(data=png_bytes, format="png")
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
The `Server` class uses three handlers for resources: `list_resources()` to enumerate them, `list_resource_templates()` for URI templates, and `read_resource()` to serve content — all with manual routing by URI. FastMCP replaces all three with per-resource decorators.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
import json
|
||||
import mcp.types as types
|
||||
from mcp.server import Server
|
||||
from pydantic import AnyUrl
|
||||
|
||||
server = Server("data")
|
||||
|
||||
@server.list_resources()
|
||||
async def list_resources() -> list[types.Resource]:
|
||||
return [
|
||||
types.Resource(
|
||||
uri=AnyUrl("config://app"),
|
||||
name="app_config",
|
||||
description="Application configuration",
|
||||
mimeType="application/json",
|
||||
),
|
||||
types.Resource(
|
||||
uri=AnyUrl("config://features"),
|
||||
name="feature_flags",
|
||||
description="Active feature flags",
|
||||
mimeType="application/json",
|
||||
),
|
||||
]
|
||||
|
||||
@server.list_resource_templates()
|
||||
async def list_resource_templates() -> list[types.ResourceTemplate]:
|
||||
return [
|
||||
types.ResourceTemplate(
|
||||
uriTemplate="users://{user_id}/profile",
|
||||
name="user_profile",
|
||||
description="User profile by ID",
|
||||
),
|
||||
types.ResourceTemplate(
|
||||
uriTemplate="projects://{project_id}/status",
|
||||
name="project_status",
|
||||
description="Project status by ID",
|
||||
),
|
||||
]
|
||||
|
||||
@server.read_resource()
|
||||
async def read_resource(uri: AnyUrl) -> str:
|
||||
uri_str = str(uri)
|
||||
if uri_str == "config://app":
|
||||
return json.dumps({"debug": False, "version": "1.0"})
|
||||
if uri_str == "config://features":
|
||||
return json.dumps({"dark_mode": True, "beta": False})
|
||||
if uri_str.startswith("users://"):
|
||||
user_id = uri_str.split("/")[2]
|
||||
return json.dumps({"id": user_id, "name": f"User {user_id}"})
|
||||
if uri_str.startswith("projects://"):
|
||||
project_id = uri_str.split("/")[2]
|
||||
return json.dumps({"id": project_id, "status": "active"})
|
||||
raise ValueError(f"Unknown resource: {uri}")
|
||||
```
|
||||
|
||||
```python After
|
||||
import json
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("data")
|
||||
|
||||
@mcp.resource("config://app", mime_type="application/json")
|
||||
def app_config() -> str:
|
||||
"""Application configuration"""
|
||||
return json.dumps({"debug": False, "version": "1.0"})
|
||||
|
||||
@mcp.resource("config://features", mime_type="application/json")
|
||||
def feature_flags() -> str:
|
||||
"""Active feature flags"""
|
||||
return json.dumps({"dark_mode": True, "beta": False})
|
||||
|
||||
@mcp.resource("users://{user_id}/profile")
|
||||
def user_profile(user_id: str) -> str:
|
||||
"""User profile by ID"""
|
||||
return json.dumps({"id": user_id, "name": f"User {user_id}"})
|
||||
|
||||
@mcp.resource("projects://{project_id}/status")
|
||||
def project_status(project_id: str) -> str:
|
||||
"""Project status by ID"""
|
||||
return json.dumps({"id": project_id, "status": "active"})
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Static resources and URI templates use the same `@mcp.resource` decorator — FastMCP detects `{placeholders}` in the URI and automatically registers a template. The function parameter `user_id` maps directly to the `{user_id}` placeholder.
|
||||
|
||||
## Prompts
|
||||
|
||||
Same pattern: the `Server` class uses `list_prompts()` and `get_prompt()` with manual routing. FastMCP uses one decorator per prompt.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
import mcp.types as types
|
||||
from mcp.server import Server
|
||||
|
||||
server = Server("prompts")
|
||||
|
||||
@server.list_prompts()
|
||||
async def list_prompts() -> list[types.Prompt]:
|
||||
return [
|
||||
types.Prompt(
|
||||
name="review_code",
|
||||
description="Review code for issues",
|
||||
arguments=[
|
||||
types.PromptArgument(
|
||||
name="code",
|
||||
description="The code to review",
|
||||
required=True,
|
||||
),
|
||||
types.PromptArgument(
|
||||
name="language",
|
||||
description="Programming language",
|
||||
required=False,
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
@server.get_prompt()
|
||||
async def get_prompt(
|
||||
name: str, arguments: dict[str, str] | None
|
||||
) -> types.GetPromptResult:
|
||||
if name == "review_code":
|
||||
code = (arguments or {}).get("code", "")
|
||||
language = (arguments or {}).get("language", "")
|
||||
lang_note = f" (written in {language})" if language else ""
|
||||
return types.GetPromptResult(
|
||||
description="Code review prompt",
|
||||
messages=[
|
||||
types.PromptMessage(
|
||||
role="user",
|
||||
content=types.TextContent(
|
||||
type="text",
|
||||
text=f"Please review this code{lang_note}:\n\n{code}",
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
raise ValueError(f"Unknown prompt: {name}")
|
||||
```
|
||||
|
||||
```python After
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("prompts")
|
||||
|
||||
@mcp.prompt
|
||||
def review_code(code: str, language: str | None = None) -> str:
|
||||
"""Review code for issues"""
|
||||
lang_note = f" (written in {language})" if language else ""
|
||||
return f"Please review this code{lang_note}:\n\n{code}"
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Returning a `str` from a prompt function automatically wraps it as a user message. For multi-turn prompts, return a `list[Message]`:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.prompts import Message
|
||||
|
||||
mcp = FastMCP("prompts")
|
||||
|
||||
@mcp.prompt
|
||||
def debug_session(error: str) -> list[Message]:
|
||||
"""Start a debugging conversation"""
|
||||
return [
|
||||
Message(f"I'm seeing this error:\n\n{error}"),
|
||||
Message("I'll help you debug that. Can you share the relevant code?", role="assistant"),
|
||||
]
|
||||
```
|
||||
|
||||
## Request Context
|
||||
|
||||
The `Server` class exposes request context through `server.request_context`, which gives you the raw `ServerSession` for sending notifications. FastMCP replaces this with a typed `Context` object injected into any function that declares it.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
import mcp.types as types
|
||||
from mcp.server import Server
|
||||
|
||||
server = Server("worker")
|
||||
|
||||
@server.call_tool()
|
||||
async def call_tool(name: str, arguments: dict):
|
||||
if name == "process_data":
|
||||
ctx = server.request_context
|
||||
await ctx.session.send_log_message(
|
||||
level="info", data="Starting processing..."
|
||||
)
|
||||
# ... do work ...
|
||||
await ctx.session.send_log_message(
|
||||
level="info", data="Done!"
|
||||
)
|
||||
return [types.TextContent(type="text", text="Processed")]
|
||||
```
|
||||
|
||||
```python After
|
||||
from fastmcp import FastMCP, Context
|
||||
|
||||
mcp = FastMCP("worker")
|
||||
|
||||
@mcp.tool
|
||||
async def process_data(ctx: Context) -> str:
|
||||
"""Process data with progress logging"""
|
||||
await ctx.info("Starting processing...")
|
||||
# ... do work ...
|
||||
await ctx.info("Done!")
|
||||
return "Processed"
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
The `Context` object provides logging (`ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`), progress reporting (`ctx.report_progress()`), resource subscriptions, session state, and more. See [Context](/servers/context) for the full API.
|
||||
|
||||
## Complete Example
|
||||
|
||||
A full server upgrade, showing how all the pieces fit together:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before expandable
|
||||
import asyncio
|
||||
import json
|
||||
import mcp.types as types
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from pydantic import AnyUrl
|
||||
|
||||
server = Server("demo")
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools() -> list[types.Tool]:
|
||||
return [
|
||||
types.Tool(
|
||||
name="greet",
|
||||
description="Greet someone by name",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
@server.call_tool()
|
||||
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
||||
if name == "greet":
|
||||
return [types.TextContent(type="text", text=f"Hello, {arguments['name']}!")]
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
@server.list_resources()
|
||||
async def list_resources() -> list[types.Resource]:
|
||||
return [
|
||||
types.Resource(
|
||||
uri=AnyUrl("info://version"),
|
||||
name="version",
|
||||
description="Server version",
|
||||
)
|
||||
]
|
||||
|
||||
@server.read_resource()
|
||||
async def read_resource(uri: AnyUrl) -> str:
|
||||
if str(uri) == "info://version":
|
||||
return json.dumps({"version": "1.0.0"})
|
||||
raise ValueError(f"Unknown resource: {uri}")
|
||||
|
||||
@server.list_prompts()
|
||||
async def list_prompts() -> list[types.Prompt]:
|
||||
return [
|
||||
types.Prompt(
|
||||
name="summarize",
|
||||
description="Summarize text",
|
||||
arguments=[
|
||||
types.PromptArgument(name="text", required=True)
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
@server.get_prompt()
|
||||
async def get_prompt(
|
||||
name: str, arguments: dict[str, str] | None
|
||||
) -> types.GetPromptResult:
|
||||
if name == "summarize":
|
||||
return types.GetPromptResult(
|
||||
description="Summarize text",
|
||||
messages=[
|
||||
types.PromptMessage(
|
||||
role="user",
|
||||
content=types.TextContent(
|
||||
type="text",
|
||||
text=f"Summarize:\n\n{(arguments or {}).get('text', '')}",
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
raise ValueError(f"Unknown prompt: {name}")
|
||||
|
||||
async def main():
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(
|
||||
read_stream, write_stream,
|
||||
server.create_initialization_options(),
|
||||
)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```python After
|
||||
import json
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("demo")
|
||||
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone by name"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
@mcp.resource("info://version")
|
||||
def version() -> str:
|
||||
"""Server version"""
|
||||
return json.dumps({"version": "1.0.0"})
|
||||
|
||||
@mcp.prompt
|
||||
def summarize(text: str) -> str:
|
||||
"""Summarize text"""
|
||||
return f"Summarize:\n\n{text}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## What's Next
|
||||
|
||||
Once you've upgraded, you have access to everything FastMCP provides beyond the basics:
|
||||
|
||||
- **[Server composition](/servers/composition)** — Mount sub-servers to build modular applications
|
||||
- **[Middleware](/servers/middleware)** — Add logging, rate limiting, error handling, and caching
|
||||
- **[Proxy servers](/servers/providers/proxy)** — Create a proxy to any existing MCP server
|
||||
- **[OpenAPI integration](/integrations/openapi)** — Generate an MCP server from an OpenAPI spec
|
||||
- **[Authentication](/servers/auth/authentication)** — Built-in OAuth and token verification
|
||||
- **[Testing](/servers/testing)** — Test your server directly in Python without running a subprocess
|
||||
|
||||
Explore the full documentation at [gofastmcp.com](https://gofastmcp.com).
|
||||
166
docs/v3/getting-started/upgrading/from-mcp-sdk.mdx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
---
|
||||
title: Upgrading from the MCP SDK
|
||||
sidebarTitle: "From MCP SDK"
|
||||
description: Upgrade from FastMCP in the MCP Python SDK to the standalone FastMCP framework
|
||||
icon: up
|
||||
---
|
||||
|
||||
If your server starts with `from mcp.server.fastmcp import FastMCP`, you're using FastMCP 1.0 — the version bundled with v1 of the `mcp` package. Upgrading to the standalone FastMCP framework is easy. **For most servers, it's a single import change.**
|
||||
|
||||
```python
|
||||
# Before
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# After
|
||||
from fastmcp import FastMCP
|
||||
```
|
||||
|
||||
That's it. Your `@mcp.tool`, `@mcp.resource`, and `@mcp.prompt` decorators, your `mcp.run()` call, and the rest of your server code all work as-is.
|
||||
|
||||
<Tip>
|
||||
**Why upgrade?** FastMCP 1.0 pioneered the Pythonic MCP server experience, and we're proud it was bundled into the `mcp` package. The standalone FastMCP project has since grown into a full framework for taking MCP servers from prototype to production — with composition, middleware, proxy servers, authentication, and much more. Upgrading gives you access to all of that, plus ongoing updates and fixes.
|
||||
</Tip>
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install --upgrade fastmcp
|
||||
# or
|
||||
uv add fastmcp
|
||||
```
|
||||
|
||||
FastMCP includes the `mcp` package as a dependency, so you don't lose access to anything. Update your import, run your server, and if your tools work, you're done.
|
||||
|
||||
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
|
||||
You are upgrading an MCP server from FastMCP 1.0 (bundled in the `mcp` package v1) to standalone FastMCP 3.0. Analyze the provided code and identify every change needed. The full upgrade guide is at https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context.
|
||||
|
||||
STEP 1 — IMPORT (required for all servers):
|
||||
Change "from mcp.server.fastmcp import FastMCP" to "from fastmcp import FastMCP".
|
||||
|
||||
STEP 2 — CONSTRUCTOR KWARGS (only if FastMCP() receives transport settings):
|
||||
FastMCP() no longer accepts: host, port, log_level, debug, sse_path, streamable_http_path, json_response, stateless_http.
|
||||
Fix: pass these to run() instead.
|
||||
Before: `mcp = FastMCP("server", host="0.0.0.0", port=8080); mcp.run()`
|
||||
After: `mcp = FastMCP("server"); mcp.run(transport="http", host="0.0.0.0", port=8080)`
|
||||
|
||||
STEP 3 — PROMPTS (only if using PromptMessage directly or returning dicts):
|
||||
mcp.types.PromptMessage is replaced by fastmcp.prompts.Message.
|
||||
Before: `PromptMessage(role="user", content=TextContent(type="text", text="Hello"))`
|
||||
After: `Message("Hello")` — role defaults to "user", accepts plain strings.
|
||||
Also: if prompts return raw dicts like `{"role": "user", "content": "..."}`, these must become Message objects or plain strings.
|
||||
The MCP SDK's FastMCP 1.0 silently coerced dicts; standalone FastMCP requires typed returns.
|
||||
|
||||
STEP 4 — OTHER MCP IMPORTS (only if importing from mcp.* directly):
|
||||
Direct imports from the `mcp` package (e.g., `import mcp.types`, `from mcp.server.stdio import stdio_server`) still work because FastMCP includes `mcp` as a dependency. However, prefer FastMCP's own APIs where equivalents exist:
|
||||
- mcp.types.TextContent for tool returns → just return plain Python values (str, int, dict, etc.)
|
||||
- mcp.types.ImageContent → fastmcp.utilities.types.Image
|
||||
- from mcp.server.stdio import stdio_server → not needed, mcp.run() handles transport
|
||||
|
||||
STEP 5 — DECORATORS (only if treating decorated functions as objects):
|
||||
@mcp.tool, @mcp.resource, @mcp.prompt now return the original function, not a component object. Code that accesses .name or .description on the decorated result needs updating. Set FASTMCP_DECORATOR_MODE=object temporarily to restore v1 behavior (this compat setting is itself deprecated).
|
||||
|
||||
For each issue found, show the original line, explain what changed, and provide the corrected code.
|
||||
</Prompt>
|
||||
|
||||
## What Might Need Updating
|
||||
|
||||
Most servers need nothing beyond the import change. Skim the sections below to see if any apply.
|
||||
|
||||
### Constructor Settings
|
||||
|
||||
If you passed transport settings like `host` or `port` directly to `FastMCP()`, those now belong on `run()`. This keeps your server definition independent of how it's deployed:
|
||||
|
||||
```python
|
||||
# Before
|
||||
mcp = FastMCP("my-server", host="0.0.0.0", port=8080)
|
||||
mcp.run()
|
||||
|
||||
# After
|
||||
mcp = FastMCP("my-server")
|
||||
mcp.run(transport="http", host="0.0.0.0", port=8080)
|
||||
```
|
||||
|
||||
If you pass the old kwargs, you'll get a clear `TypeError` with a migration hint.
|
||||
|
||||
### Prompts
|
||||
|
||||
If your prompt functions return `mcp.types.PromptMessage` objects or raw dicts with `role`/`content` keys, you'll need to upgrade to FastMCP's `Message` class. Or just return a plain string — it's automatically wrapped as a user message. The MCP SDK's bundled FastMCP 1.0 silently coerced dicts into messages; standalone FastMCP requires typed `Message` objects or strings.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("prompts")
|
||||
|
||||
@mcp.prompt
|
||||
def review(code: str) -> str:
|
||||
"""Review code for issues"""
|
||||
return f"Please review this code:\n\n{code}"
|
||||
```
|
||||
|
||||
For multi-turn prompts:
|
||||
|
||||
```python
|
||||
from fastmcp.prompts import Message
|
||||
|
||||
@mcp.prompt
|
||||
def debug(error: str) -> list[Message]:
|
||||
"""Start a debugging session"""
|
||||
return [
|
||||
Message(f"I'm seeing this error:\n\n{error}"),
|
||||
Message("I'll help debug that. Can you share the relevant code?", role="assistant"),
|
||||
]
|
||||
```
|
||||
|
||||
### Other `mcp.*` Imports
|
||||
|
||||
If your server imports directly from the `mcp` package — like `import mcp.types` or `from mcp.server.stdio import stdio_server` — those still work. FastMCP includes `mcp` as a dependency, so nothing breaks.
|
||||
|
||||
Where FastMCP provides its own API for the same thing, it's worth switching over:
|
||||
|
||||
| mcp Package | FastMCP Equivalent |
|
||||
|---|---|
|
||||
| `mcp.types.TextContent(type="text", text=str(x))` | Just return `x` from your tool |
|
||||
| `mcp.types.ImageContent(...)` | `from fastmcp.utilities.types import Image` |
|
||||
| `mcp.types.PromptMessage(...)` | `from fastmcp.prompts import Message` |
|
||||
| `from mcp.server.stdio import stdio_server` | Not needed — `mcp.run()` handles transport |
|
||||
|
||||
For anything without a FastMCP equivalent (e.g., specific protocol types you use directly), the `mcp.*` import is fine to keep.
|
||||
|
||||
### Decorated Functions
|
||||
|
||||
In FastMCP 1.0, `@mcp.tool` returned a `FunctionTool` object. Now decorators return your original function unchanged — so decorated functions stay callable for testing, reuse, and composition:
|
||||
|
||||
```python
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
# This works now — the function is still a regular function
|
||||
assert greet("World") == "Hello, World!"
|
||||
```
|
||||
|
||||
If you have code that accesses `.name`, `.description`, or other attributes on the decorated result, that will need updating. This is uncommon — most servers don't interact with the tool object directly. If you need the old behavior temporarily, set `FASTMCP_DECORATOR_MODE=object` to restore it (this compatibility setting is itself deprecated and will be removed in a future release).
|
||||
|
||||
## Verify the Upgrade
|
||||
|
||||
```bash
|
||||
# Install
|
||||
pip install --upgrade fastmcp
|
||||
|
||||
# Check version
|
||||
fastmcp version
|
||||
|
||||
# Run your server
|
||||
python my_server.py
|
||||
```
|
||||
|
||||
You can also inspect your server's registered components with the FastMCP CLI:
|
||||
|
||||
```bash
|
||||
fastmcp inspect my_server.py
|
||||
```
|
||||
|
||||
## Looking Ahead
|
||||
|
||||
The MCP ecosystem is evolving fast. Part of FastMCP's job is to absorb that complexity on your behalf — as the protocol and its tooling grow, we do the work so your server code doesn't have to change.
|
||||
134
docs/v3/getting-started/welcome.mdx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
---
|
||||
title: "Welcome to FastMCP"
|
||||
sidebarTitle: "Welcome!"
|
||||
description: The fast, Pythonic way to build MCP servers, clients, and applications.
|
||||
icon: hand-wave
|
||||
mode: center
|
||||
---
|
||||
{/* <img
|
||||
src="/assets/brand/f-watercolor-waves-4.png"
|
||||
|
||||
alt="'F' logo on a watercolor background"
|
||||
noZoom
|
||||
className="rounded-2xl block dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src="/assets/brand/f-watercolor-waves-4-dark.png"
|
||||
alt="'F' logo on a watercolor background"
|
||||
noZoom
|
||||
className="rounded-2xl hidden dark:block"
|
||||
/>
|
||||
|
||||
|
||||
*/}
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
className="rounded-2xl block dark:hidden"
|
||||
src="/assets/brand/f-watercolor-waves-4-animated.mp4"
|
||||
></video>
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
className="rounded-2xl hidden dark:block"
|
||||
src="/assets/brand/f-watercolor-waves-4-dark-animated.mp4"
|
||||
></video>
|
||||
|
||||
|
||||
**FastMCP is the standard framework for building MCP applications.** The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP gives you everything you need to go from prototype to production — build servers that expose capabilities, connect clients to any MCP service, and give your tools interactive UIs:
|
||||
|
||||
```python {1}
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Demo 🚀")
|
||||
|
||||
@mcp.tool
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers"""
|
||||
return a + b
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
|
||||
## Move Fast and Make Things
|
||||
|
||||
The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) lets you give agents access to your tools and data. But building an effective MCP application is harder than it looks.
|
||||
|
||||
FastMCP handles all of it. Declare a tool with a Python function, and the schema, validation, and documentation are generated automatically. Connect to a server with a URL, and transport negotiation, authentication, and protocol lifecycle are managed for you. You focus on your logic, and the MCP part just works: **with FastMCP, best practices are built in.**
|
||||
|
||||
**That's why FastMCP is the standard framework for working with MCP.** FastMCP 1.0 was incorporated into the official MCP Python SDK in 2024. Today, the actively maintained standalone project is downloaded a million times a day, and some version of FastMCP powers 70% of MCP servers across all languages.
|
||||
|
||||
FastMCP has three pillars:
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Servers" img="/assets/images/servers-card.png" href="/servers/server">
|
||||
Expose tools, resources, and prompts to LLMs.
|
||||
</Card>
|
||||
<Card title="Apps" img="/assets/images/apps-card.png" href="/apps/overview">
|
||||
Give your tools interactive UIs rendered directly in the conversation.
|
||||
</Card>
|
||||
<Card title="Clients" img="/assets/images/clients-card.png" href="/clients/client">
|
||||
Connect to any MCP server — local or remote, programmatic or CLI.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
**[Servers](/servers/server)** wrap your Python functions into MCP-compliant tools, resources, and prompts. **[Clients](/clients/client)** connect to any server with full protocol support. And **[Apps](/apps/overview)** give your tools interactive UIs rendered directly in the conversation.
|
||||
|
||||
Ready to build? Start with the [installation guide](/getting-started/installation) or jump straight to the [quickstart](/getting-started/quickstart).
|
||||
|
||||
FastMCP is made with 💙 by [Prefect](https://www.prefect.io/).
|
||||
|
||||
## Run FastMCP in production with Horizon
|
||||
|
||||
FastMCP is the standard way to build MCP servers. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_welcome&utm_content=welcome_body)** is the enterprise MCP gateway for running them safely.
|
||||
|
||||
Built by the FastMCP team, Horizon packages the best practices we've learned shipping the world's most popular MCP framework.
|
||||
|
||||
Deploy FastMCP servers from GitHub with branch previews and instant rollback. Create a private registry of every MCP your company uses. Secure access with SSO and tool-level RBAC. Get audit logs, observability, and governance across your MCP stack. Remix approved tools into purpose-built endpoints for teams and agents.
|
||||
|
||||
Start with FastMCP. [Scale with Horizon →](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_welcome&utm_content=welcome_cta)
|
||||
|
||||
<Tip>
|
||||
**This documentation reflects FastMCP's `main` branch**, meaning it always reflects the latest development version. Features are generally marked with version badges (e.g. `New in version: 3.0.0`) to indicate when they were introduced. Note that this may include features that are not yet released.
|
||||
</Tip>
|
||||
|
||||
## LLM-Friendly Docs
|
||||
|
||||
The FastMCP documentation is available in multiple LLM-friendly formats:
|
||||
|
||||
### MCP Server
|
||||
|
||||
The FastMCP docs are accessible via MCP! The server URL is `https://gofastmcp.com/mcp`.
|
||||
|
||||
In fact, you can use FastMCP to search the FastMCP docs:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from fastmcp import Client
|
||||
|
||||
async def main():
|
||||
async with Client("https://gofastmcp.com/mcp") as client:
|
||||
result = await client.call_tool(
|
||||
name="search_fast_mcp",
|
||||
arguments={"query": "deploy a FastMCP server"}
|
||||
)
|
||||
print(result)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### Text Formats
|
||||
|
||||
The docs are also available in [llms.txt format](https://llmstxt.org/):
|
||||
- [llms.txt](https://gofastmcp.com/llms.txt) - A sitemap listing all documentation pages
|
||||
- [llms-full.txt](https://gofastmcp.com/llms-full.txt) - The entire documentation in one file (may exceed context windows)
|
||||
|
||||
Any page can be accessed as markdown by appending `.md` to the URL. For example, this page becomes `https://gofastmcp.com/getting-started/welcome.md`.
|
||||
|
||||
You can also copy any page as markdown by pressing "Cmd+C" (or "Ctrl+C" on Windows) on your keyboard.
|
||||
228
docs/v3/integrations/anthropic.mdx
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
---
|
||||
title: Anthropic API 🤝 FastMCP
|
||||
sidebarTitle: Anthropic API
|
||||
description: Connect FastMCP servers to the Anthropic API
|
||||
icon: message-code
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
|
||||
Anthropic's [Messages API](https://docs.anthropic.com/en/api/messages) supports MCP servers as remote tool sources. This tutorial will show you how to create a FastMCP server and deploy it to a public URL, then how to call it from the Messages API.
|
||||
|
||||
<Tip>
|
||||
Currently, the MCP connector only accesses **tools** from MCP servers—it queries the `list_tools` endpoint and exposes those functions to Claude. Other MCP features like resources and prompts are not currently supported. You can read more about the MCP connector in the [Anthropic documentation](https://docs.anthropic.com/en/docs/agents-and-tools/mcp-connector).
|
||||
</Tip>
|
||||
|
||||
## Create a Server
|
||||
|
||||
First, create a FastMCP server with the tools you want to expose. For this example, we'll create a server with a single tool that rolls dice.
|
||||
|
||||
```python server.py
|
||||
import random
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP(name="Dice Roller")
|
||||
|
||||
@mcp.tool
|
||||
def roll_dice(n_dice: int) -> list[int]:
|
||||
"""Roll `n_dice` 6-sided dice and return the results."""
|
||||
return [random.randint(1, 6) for _ in range(n_dice)]
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="http", port=8000)
|
||||
```
|
||||
|
||||
## Deploy the Server
|
||||
|
||||
Your server must be deployed to a public URL in order for Anthropic to access it. The MCP connector supports both SSE and Streamable HTTP transports.
|
||||
|
||||
For development, you can use tools like `ngrok` to temporarily expose a locally-running server to the internet. We'll do that for this example (you may need to install `ngrok` and create a free account), but you can use any other method to deploy your server.
|
||||
|
||||
Assuming you saved the above code as `server.py`, you can run the following two commands in two separate terminals to deploy your server and expose it to the internet:
|
||||
|
||||
<CodeGroup>
|
||||
```bash FastMCP server
|
||||
python server.py
|
||||
```
|
||||
|
||||
```bash ngrok
|
||||
ngrok http 8000
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Warning>
|
||||
This exposes your unauthenticated server to the internet. Only run this command in a safe environment if you understand the risks.
|
||||
</Warning>
|
||||
|
||||
## Call the Server
|
||||
|
||||
To use the Messages API with MCP servers, you'll need to install the Anthropic Python SDK (not included with FastMCP):
|
||||
|
||||
```bash
|
||||
pip install anthropic
|
||||
```
|
||||
|
||||
You'll also need to authenticate with Anthropic. You can do this by setting the `ANTHROPIC_API_KEY` environment variable. Consult the Anthropic SDK documentation for more information.
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
Here is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment. **At this time you must also include the `extra_headers` parameter with the `anthropic-beta` header.**
|
||||
|
||||
```python {5, 13-22}
|
||||
import anthropic
|
||||
from rich import print
|
||||
|
||||
# Your server URL (replace with your actual URL)
|
||||
url = 'https://your-server-url.com'
|
||||
|
||||
client = anthropic.Anthropic()
|
||||
|
||||
response = client.beta.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=1000,
|
||||
messages=[{"role": "user", "content": "Roll a few dice!"}],
|
||||
mcp_servers=[
|
||||
{
|
||||
"type": "url",
|
||||
"url": f"{url}/mcp/",
|
||||
"name": "dice-server",
|
||||
}
|
||||
],
|
||||
extra_headers={
|
||||
"anthropic-beta": "mcp-client-2025-04-04"
|
||||
}
|
||||
)
|
||||
|
||||
print(response.content)
|
||||
```
|
||||
|
||||
If you run this code, you'll see something like the following output:
|
||||
|
||||
```text
|
||||
I'll roll some dice for you! Let me use the dice rolling tool.
|
||||
|
||||
I rolled 3 dice and got: 4, 2, 6
|
||||
|
||||
The results were 4, 2, and 6. Would you like me to roll again or roll a different number of dice?
|
||||
```
|
||||
|
||||
|
||||
## Authentication
|
||||
|
||||
<VersionBadge version="2.6.0" />
|
||||
|
||||
The MCP connector supports OAuth authentication through authorization tokens, which means you can secure your server while still allowing Anthropic to access it.
|
||||
|
||||
### Server Authentication
|
||||
|
||||
The simplest way to add authentication to the server is to use a bearer token scheme.
|
||||
|
||||
For this example, we'll quickly generate our own tokens with FastMCP's `RSAKeyPair` utility, but this may not be appropriate for production use. For more details, see the complete server-side [Token Verification](/servers/auth/token-verification) documentation.
|
||||
|
||||
We'll start by creating an RSA key pair to sign and verify tokens.
|
||||
|
||||
```python
|
||||
from fastmcp.server.auth.providers.jwt import RSAKeyPair
|
||||
|
||||
key_pair = RSAKeyPair.generate()
|
||||
access_token = key_pair.create_token(audience="dice-server")
|
||||
```
|
||||
|
||||
<Warning>
|
||||
FastMCP's `RSAKeyPair` utility is for development and testing only.
|
||||
</Warning>
|
||||
|
||||
Next, we'll create a `JWTVerifier` to authenticate the server.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth import JWTVerifier
|
||||
|
||||
auth = JWTVerifier(
|
||||
public_key=key_pair.public_key,
|
||||
audience="dice-server",
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Dice Roller", auth=auth)
|
||||
```
|
||||
|
||||
Here is a complete example that you can copy/paste. For simplicity and the purposes of this example only, it will print the token to the console. **Do NOT do this in production!**
|
||||
|
||||
```python server.py [expandable]
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth import JWTVerifier
|
||||
from fastmcp.server.auth.providers.jwt import RSAKeyPair
|
||||
import random
|
||||
|
||||
key_pair = RSAKeyPair.generate()
|
||||
access_token = key_pair.create_token(audience="dice-server")
|
||||
|
||||
auth = JWTVerifier(
|
||||
public_key=key_pair.public_key,
|
||||
audience="dice-server",
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Dice Roller", auth=auth)
|
||||
|
||||
@mcp.tool
|
||||
def roll_dice(n_dice: int) -> list[int]:
|
||||
"""Roll `n_dice` 6-sided dice and return the results."""
|
||||
return [random.randint(1, 6) for _ in range(n_dice)]
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"\n---\n\n🔑 Dice Roller access token:\n\n{access_token}\n\n---\n")
|
||||
mcp.run(transport="http", port=8000)
|
||||
```
|
||||
|
||||
### Client Authentication
|
||||
|
||||
If you try to call the authenticated server with the same Anthropic code we wrote earlier, you'll get an error indicating that the server rejected the request because it's not authenticated.
|
||||
|
||||
```text
|
||||
Error code: 400 - {
|
||||
"type": "error",
|
||||
"error": {
|
||||
"type": "invalid_request_error",
|
||||
"message": "MCP server 'dice-server' requires authentication. Please provide an authorization_token.",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
To authenticate the client, you can pass the token using the `authorization_token` parameter in your MCP server configuration:
|
||||
|
||||
```python {8, 21}
|
||||
import anthropic
|
||||
from rich import print
|
||||
|
||||
# Your server URL (replace with your actual URL)
|
||||
url = 'https://your-server-url.com'
|
||||
|
||||
# Your access token (replace with your actual token)
|
||||
access_token = 'your-access-token'
|
||||
|
||||
client = anthropic.Anthropic()
|
||||
|
||||
response = client.beta.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=1000,
|
||||
messages=[{"role": "user", "content": "Roll a few dice!"}],
|
||||
mcp_servers=[
|
||||
{
|
||||
"type": "url",
|
||||
"url": f"{url}/mcp/",
|
||||
"name": "dice-server",
|
||||
"authorization_token": access_token
|
||||
}
|
||||
],
|
||||
extra_headers={
|
||||
"anthropic-beta": "mcp-client-2025-04-04"
|
||||
}
|
||||
)
|
||||
|
||||
print(response.content)
|
||||
```
|
||||
|
||||
You should now see the dice roll results in the output.
|
||||
195
docs/v3/integrations/auth0.mdx
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
---
|
||||
title: Auth0 OAuth 🤝 FastMCP
|
||||
sidebarTitle: Auth0
|
||||
description: Secure your FastMCP server with Auth0 OAuth
|
||||
icon: shield-check
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="2.12.4" />
|
||||
|
||||
This guide shows you how to secure your FastMCP server using **Auth0 OAuth**. While Auth0 does have support for Dynamic Client Registration, it is not enabled by default so this integration uses the [**OIDC Proxy**](/servers/auth/oidc-proxy) pattern to bridge Auth0's dynamic OIDC configuration with MCP's authentication requirements.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before you begin, you will need:
|
||||
1. An **[Auth0 Account](https://auth0.com/)** with access to create Applications
|
||||
2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
|
||||
|
||||
### Step 1: Create an Auth0 Application
|
||||
|
||||
Create an Application in your Auth0 settings to get the credentials needed for authentication:
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to Applications">
|
||||
Go to **Applications → Applications** in your Auth0 account.
|
||||
|
||||
Click **"+ Create Application"** to create a new application.
|
||||
</Step>
|
||||
|
||||
<Step title="Create Your Application">
|
||||
- **Name**: Choose a name users will recognize (e.g., "My FastMCP Server")
|
||||
- **Choose an application type**: Choose "Single Page Web Applications"
|
||||
- Click **Create** to create the application
|
||||
</Step>
|
||||
|
||||
<Step title="Configure Your Application">
|
||||
Select the "Settings" tab for your application, then find the "Application URIs" section.
|
||||
|
||||
- **Allowed Callback URLs**: Your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)
|
||||
- Click **Save** to save your changes
|
||||
|
||||
<Warning>
|
||||
The callback URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter.
|
||||
</Warning>
|
||||
|
||||
<Tip>
|
||||
If you want to use a custom callback path (e.g., `/auth/auth0/callback`), make sure to set the same path in both your Auth0 Application settings and the `redirect_path` parameter when configuring the Auth0Provider.
|
||||
</Tip>
|
||||
</Step>
|
||||
|
||||
<Step title="Save Your Credentials">
|
||||
After creating the app, in the "Basic Information" section you'll see:
|
||||
|
||||
- **Client ID**: A public identifier like `tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB`
|
||||
- **Client Secret**: A private hidden value that should always be stored securely
|
||||
|
||||
<Tip>
|
||||
Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.
|
||||
</Tip>
|
||||
</Step>
|
||||
|
||||
<Step title="Select Your Audience">
|
||||
Go to **Applications → APIs** in your Auth0 account.
|
||||
|
||||
- Find the API that you want to use for your application
|
||||
- **API Audience**: A URL that uniquely identifies the API
|
||||
|
||||
<Tip>
|
||||
Store this along with of the credentials above. Never commit this to version control. Use environment variables or a secrets manager in production.
|
||||
</Tip>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Step 2: FastMCP Configuration
|
||||
|
||||
Create your FastMCP server using the `Auth0Provider`.
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.auth0 import Auth0Provider
|
||||
|
||||
# The Auth0Provider utilizes Auth0 OIDC configuration
|
||||
auth_provider = Auth0Provider(
|
||||
config_url="https://.../.well-known/openid-configuration", # Your Auth0 configuration URL
|
||||
client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB", # Your Auth0 application Client ID
|
||||
client_secret="vPYqbjemq...", # Your Auth0 application Client Secret
|
||||
audience="https://...", # Your Auth0 API audience
|
||||
base_url="http://localhost:8000", # Must match your application configuration
|
||||
# redirect_path="/auth/callback" # Default value, customize if needed
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Auth0 Secured App", auth=auth_provider)
|
||||
|
||||
# Add a protected tool to test authentication
|
||||
@mcp.tool
|
||||
async def get_token_info() -> dict:
|
||||
"""Returns information about the Auth0 token."""
|
||||
from fastmcp.server.dependencies import get_access_token
|
||||
|
||||
token = get_access_token()
|
||||
|
||||
return {
|
||||
"issuer": token.claims.get("iss"),
|
||||
"audience": token.claims.get("aud"),
|
||||
"scope": token.claims.get("scope")
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Running the Server
|
||||
|
||||
Start your FastMCP server with HTTP transport to enable OAuth flows:
|
||||
|
||||
```bash
|
||||
fastmcp run server.py --transport http --port 8000
|
||||
```
|
||||
|
||||
Your server is now running and protected by Auth0 authentication.
|
||||
|
||||
### Testing with a Client
|
||||
|
||||
Create a test client that authenticates with your Auth0-protected server:
|
||||
|
||||
```python test_client.py
|
||||
from fastmcp import Client
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
# The client will automatically handle Auth0 OAuth flows
|
||||
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
|
||||
# First-time connection will open Auth0 login in your browser
|
||||
print("✓ Authenticated with Auth0!")
|
||||
|
||||
# Test the protected tool
|
||||
result = await client.call_tool("get_token_info")
|
||||
print(f"Auth0 audience: {result['audience']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
When you run the client for the first time:
|
||||
1. Your browser will open to Auth0's authorization page
|
||||
2. After you authorize the app, you'll be redirected back
|
||||
3. The client receives the token and can make authenticated requests
|
||||
|
||||
## Production Configuration
|
||||
|
||||
<VersionBadge version="2.13.0" />
|
||||
|
||||
For production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:
|
||||
|
||||
```python server.py
|
||||
import os
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.auth0 import Auth0Provider
|
||||
from key_value.aio.stores.redis import RedisStore
|
||||
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
# Production setup with encrypted persistent token storage
|
||||
auth_provider = Auth0Provider(
|
||||
config_url="https://.../.well-known/openid-configuration",
|
||||
client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB",
|
||||
client_secret="vPYqbjemq...",
|
||||
audience="https://...",
|
||||
base_url="https://your-production-domain.com",
|
||||
|
||||
# Production token management
|
||||
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
|
||||
client_storage=FernetEncryptionWrapper(
|
||||
key_value=RedisStore(
|
||||
host=os.environ["REDIS_HOST"],
|
||||
port=int(os.environ["REDIS_PORT"])
|
||||
),
|
||||
fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
|
||||
)
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Production Auth0 App", auth=auth_provider)
|
||||
```
|
||||
|
||||
<Note>
|
||||
Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.
|
||||
|
||||
For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
|
||||
</Note>
|
||||
|
||||
<Info>
|
||||
The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
|
||||
</Info>
|
||||
106
docs/v3/integrations/authkit.mdx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
---
|
||||
title: AuthKit 🤝 FastMCP
|
||||
sidebarTitle: AuthKit
|
||||
description: Secure your FastMCP server with AuthKit by WorkOS
|
||||
icon: shield-check
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="2.11.0" />
|
||||
|
||||
This guide shows you how to secure your FastMCP server using WorkOS's **AuthKit**, a complete authentication and user management solution. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern with [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html) resource indicators: AuthKit issues tokens whose `aud` claim is bound to your server's resource URL, and FastMCP validates that claim automatically.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before you begin, you will need:
|
||||
1. A **[WorkOS Account](https://workos.com/)** and a new **Project**.
|
||||
2. An **[AuthKit](https://www.authkit.com/)** instance configured within your WorkOS project.
|
||||
3. Your FastMCP server's URL (can be localhost for development, e.g., `http://127.0.0.1:8000`).
|
||||
|
||||
### Step 1: WorkOS Dashboard
|
||||
|
||||
In the WorkOS Dashboard, go to **Connect → Configuration** and configure:
|
||||
|
||||
<Steps>
|
||||
<Step title="MCP Auth">
|
||||
Enable **Dynamic Client Registration** (DCR) so MCP clients can register themselves. Alternatively, enable **Client ID Metadata Document** (CIMD) if your clients support it.
|
||||
</Step>
|
||||
|
||||
<Step title="MCP resource indicators">
|
||||
Add your FastMCP server's resource URL (e.g., `http://127.0.0.1:8000/mcp`) as a valid resource indicator.
|
||||
|
||||
This must exactly match what FastMCP advertises in its protected resource metadata. Start your server first and it will log the correct URL on startup — copy that value.
|
||||
|
||||
Without this step, AuthKit falls back to a default environment-scoped audience and audience validation will fail with a 401.
|
||||
</Step>
|
||||
|
||||
<Step title="Note Your AuthKit Domain">
|
||||
Find your **AuthKit Domain** on the configuration page. It will look like `https://your-project-12345.authkit.app`. You'll need this for your FastMCP server configuration.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Step 2: FastMCP Configuration
|
||||
|
||||
Create your FastMCP server file and use the `AuthKitProvider` to handle all the OAuth integration automatically:
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.workos import AuthKitProvider
|
||||
|
||||
# AuthKitProvider automatically discovers WorkOS endpoints, configures JWT
|
||||
# validation, and binds the token audience to this server's resource URL.
|
||||
auth_provider = AuthKitProvider(
|
||||
authkit_domain="https://your-project-12345.authkit.app",
|
||||
base_url="http://127.0.0.1:8000", # Use your actual server URL
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="AuthKit Secured App", auth=auth_provider)
|
||||
```
|
||||
|
||||
When the server starts, it logs the resource URL it is validating against. Paste that URL into your Dashboard's **MCP resource indicators** list.
|
||||
|
||||
## Testing
|
||||
|
||||
To test your server, you can use the `fastmcp` CLI to run it locally. Assuming you've saved the above code to `server.py` (after replacing the `authkit_domain` and `base_url` with your actual values!), you can run the following command:
|
||||
|
||||
```bash
|
||||
fastmcp run server.py --transport http --port 8000
|
||||
```
|
||||
|
||||
AuthKit defaults DCR clients to `client_secret_basic` for token exchange, which conflicts with how some MCP clients send credentials. To avoid token exchange errors, register as a public client by setting `token_endpoint_auth_method` to `"none"`:
|
||||
|
||||
```python client.py
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import OAuth
|
||||
import asyncio
|
||||
|
||||
auth = OAuth(additional_client_metadata={"token_endpoint_auth_method": "none"})
|
||||
|
||||
async def main():
|
||||
async with Client("http://127.0.0.1:8000/mcp", auth=auth) as client:
|
||||
assert await client.ping()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Production Configuration
|
||||
|
||||
For production deployments, load sensitive configuration from environment variables:
|
||||
|
||||
```python server.py
|
||||
import os
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.workos import AuthKitProvider
|
||||
|
||||
# Load configuration from environment variables
|
||||
auth = AuthKitProvider(
|
||||
authkit_domain=os.environ.get("AUTHKIT_DOMAIN"),
|
||||
base_url=os.environ.get("BASE_URL", "https://your-server.com"),
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="AuthKit Secured App", auth=auth)
|
||||
```
|
||||
278
docs/v3/integrations/aws-cognito.mdx
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
---
|
||||
title: AWS Cognito OAuth 🤝 FastMCP
|
||||
sidebarTitle: AWS Cognito
|
||||
description: Secure your FastMCP server with AWS Cognito user pools
|
||||
icon: aws
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="2.12.4" />
|
||||
|
||||
This guide shows you how to secure your FastMCP server using **AWS Cognito user pools**. Since AWS Cognito doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge AWS Cognito's traditional OAuth with MCP's authentication requirements. It also includes robust JWT token validation, ensuring enterprise-grade authentication.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before you begin, you will need:
|
||||
1. An **[AWS Account](https://aws.amazon.com/)** with access to create AWS Cognito user pools
|
||||
2. Basic familiarity with AWS Cognito concepts (user pools, app clients)
|
||||
3. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
|
||||
|
||||
### Step 1: Create an AWS Cognito User Pool and App Client
|
||||
|
||||
Set up AWS Cognito user pool with an app client to get the credentials needed for authentication:
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to AWS Cognito">
|
||||
Go to the **[AWS Cognito Console](https://console.aws.amazon.com/cognito/)** and ensure you're in your desired AWS region.
|
||||
|
||||
Select **"User pools"** from the side navigation (click on the hamburger icon at the top left in case you don't see any), and click **"Create user pool"** to create a new user pool.
|
||||
</Step>
|
||||
|
||||
<Step title="Define Your Application">
|
||||
AWS Cognito now provides a streamlined setup experience:
|
||||
|
||||
1. **Application type**: Select **"Traditional web application"** (this is the correct choice for FastMCP server-side authentication)
|
||||
2. **Name your application**: Enter a descriptive name (e.g., `FastMCP Server`)
|
||||
|
||||
The traditional web application type automatically configures:
|
||||
- Server-side authentication with client secrets
|
||||
- Authorization code grant flow
|
||||
- Appropriate security settings for confidential clients
|
||||
|
||||
<Info>
|
||||
Choose "Traditional web application" rather than SPA, Mobile app, or Machine-to-machine options. This ensures proper OAuth 2.0 configuration for FastMCP.
|
||||
</Info>
|
||||
</Step>
|
||||
|
||||
<Step title="Configure Options">
|
||||
AWS will guide you through configuration options:
|
||||
|
||||
- **Sign-in identifiers**: Choose how users will sign in (email, username, or phone)
|
||||
- **Required attributes**: Select any additional user information you need
|
||||
- **Return URL**: Add your callback URL (e.g., `http://localhost:8000/auth/callback` for development)
|
||||
|
||||
<Tip>
|
||||
The simplified interface handles most OAuth security settings automatically based on your application type selection.
|
||||
</Tip>
|
||||
</Step>
|
||||
|
||||
<Step title="Review and Create">
|
||||
Review your configuration and click **"Create user pool"**.
|
||||
|
||||
After creation, you'll see your user pool details. Save these important values:
|
||||
- **User pool ID** (format: `eu-central-1_XXXXXXXXX`)
|
||||
- **Client ID** (found under → "Applications" → "App clients" in the side navigation → \<Your application name, e.g., `FastMCP Server`\> → "App client information")
|
||||
- **Client Secret** (found under → "Applications" → "App clients" in the side navigation → \<Your application name, e.g., `FastMCP Server`\> → "App client information")
|
||||
|
||||
<Tip>
|
||||
The user pool ID and app client credentials are all you need for FastMCP configuration.
|
||||
</Tip>
|
||||
</Step>
|
||||
|
||||
<Step title="Configure OAuth Settings">
|
||||
Under "Login pages" in your app client's settings, you can double check and adjust the OAuth configuration:
|
||||
|
||||
- **Allowed callback URLs**: Add your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)
|
||||
- **Allowed sign-out URLs**: Optional, for logout functionality
|
||||
- **OAuth 2.0 grant types**: Ensure "Authorization code grant" is selected
|
||||
- **OpenID Connect scopes**: Select scopes your application needs (e.g., `openid`, `email`, `profile`)
|
||||
|
||||
<Tip>
|
||||
For local development, you can use `http://localhost` URLs. For production, you must use HTTPS.
|
||||
</Tip>
|
||||
</Step>
|
||||
|
||||
<Step title="Configure Resource Server">
|
||||
AWS Cognito requires a resource server entry to support OAuth with protected resources. Without this, token exchange will fail with an `invalid_grant` error.
|
||||
|
||||
Navigate to **"Branding" → "Domain"** in the side navigation, then:
|
||||
|
||||
1. Click **"Create resource server"**
|
||||
2. **Resource server name**: Enter a descriptive name (e.g., `My MCP Server`)
|
||||
3. **Resource server identifier**: Enter your MCP endpoint URL exactly as it will be accessed (e.g., `http://localhost:8000/mcp` for development, or `https://your-server.com/mcp` for production)
|
||||
4. Click **"Create resource server"**
|
||||
|
||||
<Warning>
|
||||
The resource server identifier must exactly match your `base_url + mcp_path`. For the default configuration with `base_url="http://localhost:8000"` and `path="/mcp"`, use `http://localhost:8000/mcp`.
|
||||
</Warning>
|
||||
</Step>
|
||||
|
||||
<Step title="Save Your Credentials">
|
||||
After setup, you'll have:
|
||||
|
||||
- **User Pool ID**: Format like `eu-central-1_XXXXXXXXX`
|
||||
- **Client ID**: Your application's client identifier
|
||||
- **Client Secret**: Generated client secret (keep secure)
|
||||
- **AWS Region**: Where Your AWS Cognito user pool is located
|
||||
|
||||
<Tip>
|
||||
Store these credentials securely. Never commit them to version control. Use environment variables or AWS Secrets Manager in production.
|
||||
</Tip>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Step 2: FastMCP Configuration
|
||||
|
||||
Create your FastMCP server using the `AWSCognitoProvider`, which handles AWS Cognito's JWT tokens and user claims automatically:
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.aws import AWSCognitoProvider
|
||||
from fastmcp.server.dependencies import get_access_token
|
||||
|
||||
# The AWSCognitoProvider handles JWT validation and user claims
|
||||
auth_provider = AWSCognitoProvider(
|
||||
user_pool_id="eu-central-1_XXXXXXXXX", # Your AWS Cognito user pool ID
|
||||
aws_region="eu-central-1", # AWS region (defaults to eu-central-1)
|
||||
client_id="your-app-client-id", # Your app client ID
|
||||
client_secret="your-app-client-secret", # Your app client Secret
|
||||
base_url="http://localhost:8000", # Must match your callback URL
|
||||
# redirect_path="/auth/callback" # Default value, customize if needed
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="AWS Cognito Secured App", auth=auth_provider)
|
||||
|
||||
# Add a protected tool to test authentication
|
||||
@mcp.tool
|
||||
async def get_access_token_claims() -> dict:
|
||||
"""Get the authenticated user's access token claims."""
|
||||
token = get_access_token()
|
||||
return {
|
||||
"sub": token.claims.get("sub"),
|
||||
"username": token.claims.get("username"),
|
||||
"cognito:groups": token.claims.get("cognito:groups", []),
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Running the Server
|
||||
|
||||
Start your FastMCP server with HTTP transport to enable OAuth flows:
|
||||
|
||||
```bash
|
||||
fastmcp run server.py --transport http --port 8000
|
||||
```
|
||||
|
||||
Your server is now running and protected by AWS Cognito OAuth authentication.
|
||||
|
||||
### Testing with a Client
|
||||
|
||||
Create a test client that authenticates with Your AWS Cognito-protected server:
|
||||
|
||||
```python test_client.py
|
||||
from fastmcp import Client
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
# The client will automatically handle AWS Cognito OAuth
|
||||
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
|
||||
# First-time connection will open AWS Cognito login in your browser
|
||||
print("✓ Authenticated with AWS Cognito!")
|
||||
|
||||
# Test the protected tool
|
||||
print("Calling protected tool: get_access_token_claims")
|
||||
result = await client.call_tool("get_access_token_claims")
|
||||
user_data = result.data
|
||||
print("Available access token claims:")
|
||||
print(f"- sub: {user_data.get('sub', 'N/A')}")
|
||||
print(f"- username: {user_data.get('username', 'N/A')}")
|
||||
print(f"- cognito:groups: {user_data.get('cognito:groups', [])}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
When you run the client for the first time:
|
||||
1. Your browser will open to AWS Cognito's hosted UI login page
|
||||
2. After you sign in (or sign up), you'll be redirected back to your MCP server
|
||||
3. The client receives the JWT token and can make authenticated requests
|
||||
|
||||
<Info>
|
||||
The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
|
||||
</Info>
|
||||
|
||||
## Production Configuration
|
||||
|
||||
<VersionBadge version="2.13.0" />
|
||||
|
||||
For production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:
|
||||
|
||||
```python server.py
|
||||
import os
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.aws import AWSCognitoProvider
|
||||
from key_value.aio.stores.redis import RedisStore
|
||||
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
# Production setup with encrypted persistent token storage
|
||||
auth_provider = AWSCognitoProvider(
|
||||
user_pool_id="eu-central-1_XXXXXXXXX",
|
||||
aws_region="eu-central-1",
|
||||
client_id="your-app-client-id",
|
||||
client_secret="your-app-client-secret",
|
||||
base_url="https://your-production-domain.com",
|
||||
|
||||
# Production token management
|
||||
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
|
||||
client_storage=FernetEncryptionWrapper(
|
||||
key_value=RedisStore(
|
||||
host=os.environ["REDIS_HOST"],
|
||||
port=int(os.environ["REDIS_PORT"])
|
||||
),
|
||||
fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
|
||||
)
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Production AWS Cognito App", auth=auth_provider)
|
||||
```
|
||||
|
||||
<Note>
|
||||
Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.
|
||||
|
||||
For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
|
||||
</Note>
|
||||
|
||||
## Features
|
||||
|
||||
### JWT Token Validation
|
||||
|
||||
The AWS Cognito provider includes robust JWT token validation:
|
||||
|
||||
- **Signature Verification**: Validates tokens against AWS Cognito's public keys (JWKS)
|
||||
- **Expiration Checking**: Automatically rejects expired tokens
|
||||
- **Issuer Validation**: Ensures tokens come from your specific AWS Cognito user pool
|
||||
- **Scope Enforcement**: Verifies required OAuth scopes are present
|
||||
|
||||
### User Claims and Groups
|
||||
|
||||
Access rich user information from AWS Cognito JWT tokens:
|
||||
|
||||
```python
|
||||
from fastmcp.server.dependencies import get_access_token
|
||||
|
||||
@mcp.tool
|
||||
async def admin_only_tool() -> str:
|
||||
"""A tool only available to admin users."""
|
||||
token = get_access_token()
|
||||
user_groups = token.claims.get("cognito:groups", [])
|
||||
|
||||
if "admin" not in user_groups:
|
||||
raise ValueError("This tool requires admin access")
|
||||
|
||||
return "Admin access granted!"
|
||||
```
|
||||
|
||||
### Enterprise Integration
|
||||
|
||||
Perfect for enterprise environments with:
|
||||
|
||||
- **Single Sign-On (SSO)**: Integrate with corporate identity providers
|
||||
- **Multi-Factor Authentication (MFA)**: Leverage AWS Cognito's built-in MFA
|
||||
- **User Groups**: Role-based access control through AWS Cognito groups
|
||||
- **Custom Attributes**: Access custom user attributes defined in your AWS Cognito user pool
|
||||
- **Compliance**: Meet enterprise security and compliance requirements
|
||||
542
docs/v3/integrations/azure.mdx
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
---
|
||||
title: Azure (Microsoft Entra ID) OAuth 🤝 FastMCP
|
||||
sidebarTitle: Azure (Entra ID)
|
||||
description: Secure your FastMCP server with Azure/Microsoft Entra OAuth
|
||||
icon: microsoft
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="2.13.0" />
|
||||
|
||||
This guide shows you how to secure your FastMCP server using **Azure OAuth** (Microsoft Entra ID). Since Azure doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge Azure's traditional OAuth with MCP's authentication requirements. FastMCP validates Azure JWTs against your application's client_id.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before you begin, you will need:
|
||||
1. An **[Azure Account](https://portal.azure.com/)** with access to create App registrations
|
||||
2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
|
||||
3. Your Azure tenant ID (found in Azure Portal under Microsoft Entra ID)
|
||||
|
||||
### Step 1: Create an Azure App Registration
|
||||
|
||||
Create an App registration in Azure Portal to get the credentials needed for authentication:
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to App registrations">
|
||||
Go to the [Azure Portal](https://portal.azure.com) and navigate to **Microsoft Entra ID → App registrations**.
|
||||
|
||||
Click **"New registration"** to create a new application.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure Your Application">
|
||||
Fill in the application details:
|
||||
|
||||
- **Name**: Choose a name users will recognize (e.g., "My FastMCP Server")
|
||||
- **Supported account types**: Choose based on your needs:
|
||||
- **Single tenant**: Only users in your organization
|
||||
- **Multitenant**: Users in any Microsoft Entra directory
|
||||
- **Multitenant + personal accounts**: Any Microsoft account
|
||||
- **Redirect URI**: Select "Web" and enter your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)
|
||||
|
||||
<Warning>
|
||||
The redirect URI must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. For local development, Azure allows `http://localhost` URLs. For production, you must use HTTPS.
|
||||
</Warning>
|
||||
|
||||
<Tip>
|
||||
If you want to use a custom callback path (e.g., `/auth/azure/callback`), make sure to set the same path in both your Azure App registration and the `redirect_path` parameter when configuring the AzureProvider.
|
||||
</Tip>
|
||||
|
||||
- **Expose an API**: Configure your Application ID URI and define scopes
|
||||
- Go to **Expose an API** in the App registration sidebar.
|
||||
- Click **Set** next to "Application ID URI" and choose one of:
|
||||
- Keep the default `api://{client_id}`
|
||||
- Set a custom value, following the supported formats (see [Identifier URI restrictions](https://learn.microsoft.com/en-us/entra/identity-platform/identifier-uri-restrictions))
|
||||
- Click **Add a scope** and create a scope your app will require, for example:
|
||||
- Scope name: `read` (or `write`, etc.)
|
||||
- Admin consent display name/description: as appropriate for your org
|
||||
- Who can consent: as needed (Admins only or Admins and users)
|
||||
|
||||
- **Configure Access Token Version**: Ensure your app uses access token v2
|
||||
- Go to **Manifest** in the App registration sidebar.
|
||||
- Find the `requestedAccessTokenVersion` property and set it to `2`:
|
||||
```json
|
||||
"api": {
|
||||
"requestedAccessTokenVersion": 2
|
||||
}
|
||||
```
|
||||
- Click **Save** at the top of the manifest editor.
|
||||
|
||||
<Warning>
|
||||
Access token v2 is required for FastMCP's Azure integration to work correctly. If this is not set, you may encounter authentication errors.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
In FastMCP's `AzureProvider`, set `identifier_uri` to your Application ID URI (optional; defaults to `api://{client_id}`) and set `required_scopes` to the unprefixed scope names (e.g., `read`, `write`). During authorization, FastMCP automatically prefixes scopes with your `identifier_uri`.
|
||||
</Note>
|
||||
|
||||
|
||||
</Step>
|
||||
|
||||
|
||||
<Step title="Create Client Secret">
|
||||
After registration, navigate to **Certificates & secrets** in your app's settings.
|
||||
|
||||
- Click **"New client secret"**
|
||||
- Add a description (e.g., "FastMCP Server")
|
||||
- Choose an expiration period
|
||||
- Click **"Add"**
|
||||
|
||||
<Warning>
|
||||
Copy the secret value immediately - it won't be shown again! You'll need to create a new secret if you lose it.
|
||||
</Warning>
|
||||
</Step>
|
||||
|
||||
<Step title="Note Your Credentials">
|
||||
From the **Overview** page of your app registration, note:
|
||||
|
||||
- **Application (client) ID**: A UUID like `835f09b6-0f0f-40cc-85cb-f32c5829a149`
|
||||
- **Directory (tenant) ID**: A UUID like `08541b6e-646d-43de-a0eb-834e6713d6d5`
|
||||
- **Client Secret**: The value you copied in the previous step
|
||||
|
||||
<Tip>
|
||||
Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.
|
||||
</Tip>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Step 2: FastMCP Configuration
|
||||
|
||||
Create your FastMCP server using the `AzureProvider`, which handles Azure's OAuth flow automatically:
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.azure import AzureProvider
|
||||
|
||||
# The AzureProvider handles Azure's token format and validation
|
||||
auth_provider = AzureProvider(
|
||||
client_id="835f09b6-0f0f-40cc-85cb-f32c5829a149", # Your Azure App Client ID
|
||||
client_secret="your-client-secret", # Your Azure App Client Secret
|
||||
tenant_id="08541b6e-646d-43de-a0eb-834e6713d6d5", # Your Azure Tenant ID (REQUIRED)
|
||||
base_url="http://localhost:8000", # Must match your App registration
|
||||
required_scopes=["your-scope"], # At least one scope REQUIRED - name of scope from your App
|
||||
# identifier_uri defaults to api://{client_id}
|
||||
# identifier_uri="api://your-api-id",
|
||||
# Optional: request additional upstream scopes in the authorize request
|
||||
# additional_authorize_scopes=["User.Read", "openid", "email"],
|
||||
# redirect_path="/auth/callback" # Default value, customize if needed
|
||||
# base_authority="login.microsoftonline.us" # For Azure Government (default: login.microsoftonline.com)
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Azure Secured App", auth=auth_provider)
|
||||
|
||||
# Add a protected tool to test authentication
|
||||
@mcp.tool
|
||||
async def get_user_info() -> dict:
|
||||
"""Returns information about the authenticated Azure user."""
|
||||
from fastmcp.server.dependencies import get_access_token
|
||||
|
||||
token = get_access_token()
|
||||
# The AzureProvider stores user data in token claims
|
||||
return {
|
||||
"azure_id": token.claims.get("sub"),
|
||||
"email": token.claims.get("email"),
|
||||
"name": token.claims.get("name"),
|
||||
"job_title": token.claims.get("job_title"),
|
||||
"office_location": token.claims.get("office_location")
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
**Important**: The `tenant_id` parameter is **REQUIRED**. Azure no longer supports using "common" for new applications due to security requirements. You must use one of:
|
||||
|
||||
- **Your specific tenant ID**: Found in Azure Portal (e.g., `08541b6e-646d-43de-a0eb-834e6713d6d5`)
|
||||
- **"organizations"**: For work and school accounts only
|
||||
- **"consumers"**: For personal Microsoft accounts only
|
||||
|
||||
Using your specific tenant ID is recommended for better security and control.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
**Important**: The `required_scopes` parameter is **REQUIRED** and must include at least one scope. Azure's OAuth API requires the `scope` parameter in all authorization requests - you cannot authenticate without specifying at least one scope. Use the unprefixed scope names from your Azure App registration (e.g., `["read", "write"]`). These scopes must be created under **Expose an API** in your App registration.
|
||||
</Note>
|
||||
|
||||
### Scope Handling
|
||||
|
||||
FastMCP automatically prefixes `required_scopes` with your `identifier_uri` (e.g., `api://your-client-id`) since these are your custom API scopes. Scopes in `additional_authorize_scopes` are sent as-is since they target external resources like Microsoft Graph.
|
||||
|
||||
**`required_scopes`** — Your custom API scopes, defined in Azure "Expose an API":
|
||||
|
||||
| You write | Sent to Azure | Validated on tokens |
|
||||
|-----------|---------------|---------------------|
|
||||
| `mcp-read` | `api://xxx/mcp-read` | ✓ |
|
||||
| `my.scope` | `api://xxx/my.scope` | ✓ |
|
||||
| `openid` | `openid` | ✗ (OIDC scope) |
|
||||
| `api://xxx/read` | `api://xxx/read` | ✓ |
|
||||
|
||||
**`additional_authorize_scopes`** — External scopes (e.g., Microsoft Graph) for server-side use:
|
||||
|
||||
| You write | Sent to Azure | Validated on tokens |
|
||||
|-----------|---------------|---------------------|
|
||||
| `User.Read` | `User.Read` | ✗ |
|
||||
| `Mail.Send` | `Mail.Send` | ✗ |
|
||||
|
||||
<Note>
|
||||
`offline_access` is automatically included to obtain refresh tokens. FastMCP manages token refreshing automatically.
|
||||
</Note>
|
||||
|
||||
<Info>
|
||||
**Why aren't `additional_authorize_scopes` validated?** Azure issues separate tokens per resource. The access token FastMCP receives is for *your API*—Graph scopes aren't in its `scp` claim. To call Graph APIs, your server uses the upstream Azure token in an on-behalf-of (OBO) flow.
|
||||
</Info>
|
||||
|
||||
<Note>
|
||||
OIDC scopes (`openid`, `profile`, `email`, `offline_access`) are never prefixed and excluded from validation because Azure doesn't include them in access token `scp` claims.
|
||||
</Note>
|
||||
|
||||
## Testing
|
||||
|
||||
### Running the Server
|
||||
|
||||
Start your FastMCP server with HTTP transport to enable OAuth flows:
|
||||
|
||||
```bash
|
||||
fastmcp run server.py --transport http --port 8000
|
||||
```
|
||||
|
||||
Your server is now running and protected by Azure OAuth authentication.
|
||||
|
||||
### Testing with a Client
|
||||
|
||||
Create a test client that authenticates with your Azure-protected server:
|
||||
|
||||
```python test_client.py
|
||||
from fastmcp import Client
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
# The client will automatically handle Azure OAuth
|
||||
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
|
||||
# First-time connection will open Azure login in your browser
|
||||
print("✓ Authenticated with Azure!")
|
||||
|
||||
# Test the protected tool
|
||||
result = await client.call_tool("get_user_info")
|
||||
print(f"Azure user: {result['email']}")
|
||||
print(f"Name: {result['name']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
When you run the client for the first time:
|
||||
1. Your browser will open to Microsoft's authorization page
|
||||
2. Sign in with your Microsoft account (work, school, or personal based on your tenant configuration)
|
||||
3. Grant the requested permissions
|
||||
4. After authorization, you'll be redirected back
|
||||
5. The client receives the token and can make authenticated requests
|
||||
|
||||
<Info>
|
||||
The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
|
||||
</Info>
|
||||
|
||||
## Production Configuration
|
||||
|
||||
<VersionBadge version="2.13.0" />
|
||||
|
||||
For production deployments with persistent token management across server restarts, configure `jwt_signing_key` and `client_storage`:
|
||||
|
||||
```python server.py
|
||||
import os
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.azure import AzureProvider
|
||||
from key_value.aio.stores.redis import RedisStore
|
||||
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
# Production setup with encrypted persistent token storage
|
||||
auth_provider = AzureProvider(
|
||||
client_id="835f09b6-0f0f-40cc-85cb-f32c5829a149",
|
||||
client_secret="your-client-secret",
|
||||
tenant_id="08541b6e-646d-43de-a0eb-834e6713d6d5",
|
||||
base_url="https://your-production-domain.com",
|
||||
required_scopes=["your-scope"],
|
||||
|
||||
# Production token management
|
||||
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
|
||||
client_storage=FernetEncryptionWrapper(
|
||||
key_value=RedisStore(
|
||||
host=os.environ["REDIS_HOST"],
|
||||
port=int(os.environ["REDIS_PORT"])
|
||||
),
|
||||
fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
|
||||
)
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Production Azure App", auth=auth_provider)
|
||||
```
|
||||
|
||||
<Note>
|
||||
Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.
|
||||
|
||||
For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
|
||||
</Note>
|
||||
|
||||
## Token Verification Only (Managed Identity)
|
||||
|
||||
<VersionBadge version="2.15.0" />
|
||||
|
||||
For deployments where your server only needs to **validate incoming tokens** — such as Azure Container Apps with Managed Identity — use `AzureJWTVerifier` with `RemoteAuthProvider` instead of the full `AzureProvider`.
|
||||
|
||||
This pattern is ideal when:
|
||||
- Your infrastructure handles authentication (e.g., Managed Identity)
|
||||
- You don't need the OAuth proxy flow (no `client_secret` required)
|
||||
- You just need to verify that incoming Azure AD tokens are valid
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth import RemoteAuthProvider
|
||||
from fastmcp.server.auth.providers.azure import AzureJWTVerifier
|
||||
from pydantic import AnyHttpUrl
|
||||
|
||||
tenant_id = "your-tenant-id"
|
||||
client_id = "your-client-id"
|
||||
|
||||
# AzureJWTVerifier auto-configures JWKS, issuer, and audience
|
||||
verifier = AzureJWTVerifier(
|
||||
client_id=client_id,
|
||||
tenant_id=tenant_id,
|
||||
required_scopes=["access_as_user"], # Scope names from Azure Portal
|
||||
)
|
||||
|
||||
auth = RemoteAuthProvider(
|
||||
token_verifier=verifier,
|
||||
authorization_servers=[
|
||||
AnyHttpUrl(f"https://login.microsoftonline.com/{tenant_id}/v2.0")
|
||||
],
|
||||
base_url="https://your-container-app.azurecontainerapps.io",
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Azure MI App", auth=auth)
|
||||
```
|
||||
|
||||
`AzureJWTVerifier` handles Azure's scope format automatically. You write scope names exactly as they appear in Azure Portal under **Expose an API** (e.g., `access_as_user`). The verifier validates tokens using the short-form scopes that Azure puts in the `scp` claim, while advertising the full URI scopes (e.g., `api://your-client-id/access_as_user`) in OAuth metadata so MCP clients know what to request.
|
||||
|
||||
<Note>
|
||||
For Azure Government, pass `base_authority="login.microsoftonline.us"` to `AzureJWTVerifier`.
|
||||
</Note>
|
||||
|
||||
## On-Behalf-Of (OBO)
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
The On-Behalf-Of (OBO) flow allows your FastMCP server to call downstream Microsoft APIs—like Microsoft Graph—using the authenticated user's identity. When a user authenticates to your MCP server, you receive a token for your API. OBO exchanges that token for a new token that can call other services, maintaining the user's identity and permissions throughout the chain.
|
||||
|
||||
This pattern is useful when your tools need to access user-specific data from Microsoft services: reading emails, accessing calendar events, querying SharePoint, or any other Graph API operation that requires user context.
|
||||
|
||||
<Note>
|
||||
OBO features require the `azure` extra:
|
||||
|
||||
```bash
|
||||
pip install 'fastmcp[azure]'
|
||||
```
|
||||
</Note>
|
||||
|
||||
### Azure Portal Setup
|
||||
|
||||
OBO requires additional configuration in your Azure App registration beyond basic authentication.
|
||||
|
||||
<Steps>
|
||||
<Step title="Add API Permissions">
|
||||
In your App registration, navigate to **API permissions** and add the Microsoft Graph permissions your tools will need.
|
||||
|
||||
- Click **Add a permission** → **Microsoft Graph** → **Delegated permissions**
|
||||
- Select the permissions required for your use case (e.g., `Mail.Read`, `Calendars.Read`, `User.Read`)
|
||||
- Repeat for any other APIs you need to call
|
||||
|
||||
<Warning>
|
||||
Only add delegated permissions for OBO. Application permissions bypass user context entirely and are inappropriate for the OBO flow.
|
||||
</Warning>
|
||||
</Step>
|
||||
|
||||
<Step title="Grant Admin Consent">
|
||||
OBO requires admin consent for the permissions you've added. In the **API permissions** page, click **Grant admin consent for [Your Organization]**.
|
||||
|
||||
Without admin consent, OBO token exchanges will fail with an `AADSTS65001` error indicating the user or administrator hasn't consented to use the application.
|
||||
|
||||
<Tip>
|
||||
For development, you can grant consent for just your own account. For production, an Azure AD administrator must grant tenant-wide consent.
|
||||
</Tip>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Configure AzureProvider for OBO
|
||||
|
||||
The `additional_authorize_scopes` parameter tells Azure which downstream API permissions to include during the initial authorization. These scopes establish what your server can request through OBO later.
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.azure import AzureProvider
|
||||
|
||||
auth_provider = AzureProvider(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
tenant_id="your-tenant-id",
|
||||
base_url="http://localhost:8000",
|
||||
required_scopes=["mcp-access"], # Your API scope
|
||||
# Include Graph scopes for OBO
|
||||
additional_authorize_scopes=[
|
||||
"https://graph.microsoft.com/Mail.Read",
|
||||
"https://graph.microsoft.com/User.Read",
|
||||
"offline_access", # Enables refresh tokens
|
||||
],
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Graph-Enabled Server", auth=auth_provider)
|
||||
```
|
||||
|
||||
Scopes listed in `additional_authorize_scopes` are requested during the initial OAuth flow but aren't validated on incoming tokens. They establish permission for your server to later exchange the user's token for downstream API access.
|
||||
|
||||
<Info>
|
||||
Use fully-qualified scope URIs for downstream APIs (e.g., `https://graph.microsoft.com/Mail.Read`). Short forms like `Mail.Read` work for authorization requests, but fully-qualified URIs are clearer and avoid ambiguity.
|
||||
</Info>
|
||||
|
||||
### EntraOBOToken Dependency
|
||||
|
||||
The `EntraOBOToken` dependency handles the complete OBO flow automatically. Declare it as a parameter default with the scopes you need, and FastMCP exchanges the user's token for a downstream API token before your function runs.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.azure import AzureProvider, EntraOBOToken
|
||||
import httpx
|
||||
|
||||
auth_provider = AzureProvider(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
tenant_id="your-tenant-id",
|
||||
base_url="http://localhost:8000",
|
||||
required_scopes=["mcp-access"],
|
||||
additional_authorize_scopes=[
|
||||
"https://graph.microsoft.com/Mail.Read",
|
||||
"https://graph.microsoft.com/User.Read",
|
||||
],
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Email Reader", auth=auth_provider)
|
||||
|
||||
@mcp.tool
|
||||
async def get_recent_emails(
|
||||
count: int = 10,
|
||||
graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"]),
|
||||
) -> list[dict]:
|
||||
"""Get the user's recent emails from Microsoft Graph."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"https://graph.microsoft.com/v1.0/me/messages?$top={count}",
|
||||
headers={"Authorization": f"Bearer {graph_token}"},
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
return [
|
||||
{"subject": msg["subject"], "from": msg["from"]["emailAddress"]["address"]}
|
||||
for msg in data.get("value", [])
|
||||
]
|
||||
```
|
||||
|
||||
The `graph_token` parameter receives a ready-to-use access token for Microsoft Graph. FastMCP handles the OBO exchange transparently—your function just uses the token to call the API.
|
||||
|
||||
<Warning>
|
||||
**Scope alignment is critical.** The scopes passed to `EntraOBOToken` must be a subset of the scopes in `additional_authorize_scopes`. If you request a scope during OBO that wasn't included in the initial authorization, the exchange will fail.
|
||||
</Warning>
|
||||
|
||||
<Tip>
|
||||
For advanced OBO scenarios, use `CurrentAccessToken()` to get the user's token, then construct an `azure.identity.aio.OnBehalfOfCredential` directly with your Azure credentials.
|
||||
</Tip>
|
||||
|
||||
<Tip>
|
||||
For a complete working example of Azure OBO with FastMCP, see [Pamela Fox's blog post on OBO flow for Entra-based MCP servers](https://blog.pamelafox.org/2026/01/using-on-behalf-of-flow-for-entra-based.html).
|
||||
</Tip>
|
||||
|
||||
## Azure AD B2C
|
||||
|
||||
<VersionBadge version="3.3.0" />
|
||||
|
||||
Azure AD B2C (Business-to-Consumer) uses different endpoints, scope URIs, and
|
||||
token issuers than standard Microsoft Entra ID. The `AzureProvider.from_b2c()`
|
||||
factory handles all of these differences automatically.
|
||||
|
||||
<Warning>
|
||||
Azure AD B2C does **not** support the On-Behalf-Of (OBO) flow. If you need
|
||||
OBO for downstream API calls, use `AzureProvider` with standard Entra ID
|
||||
instead.
|
||||
</Warning>
|
||||
|
||||
### Quick Start
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.azure import AzureProvider
|
||||
|
||||
auth = AzureProvider.from_b2c(
|
||||
tenant_name="mytenant",
|
||||
policy_name="B2C_1_susi",
|
||||
client_id="00000000-0000-0000-0000-000000000000",
|
||||
client_secret="my-secret",
|
||||
required_scopes=["mcp-access"],
|
||||
base_url="https://myserver.com",
|
||||
)
|
||||
|
||||
mcp = FastMCP("My App", auth=auth)
|
||||
```
|
||||
|
||||
`from_b2c()` derives the following values automatically:
|
||||
|
||||
| Derived value | Formula |
|
||||
|---|---|
|
||||
| Authority host | `{tenant_name}.b2clogin.com` |
|
||||
| Authorization endpoint | `https://{tenant_name}.b2clogin.com/{tenant_name}.onmicrosoft.com/{policy_name}/oauth2/v2.0/authorize` |
|
||||
| Token endpoint | `https://{tenant_name}.b2clogin.com/{tenant_name}.onmicrosoft.com/{policy_name}/oauth2/v2.0/token` |
|
||||
| Scope identifier URI | `https://{tenant_name}.onmicrosoft.com/{client_id}` |
|
||||
|
||||
### Token Issuer Validation
|
||||
|
||||
B2C access tokens carry the **tenant GUID** (not the `.onmicrosoft.com` name)
|
||||
in the `iss` claim, and the exact format varies by policy and custom-domain
|
||||
configuration. `from_b2c()` therefore **disables issuer validation by
|
||||
default**; **audience validation still enforces that tokens target the correct
|
||||
application**.
|
||||
|
||||
Once you have confirmed a successful end-to-end login, read the actual `iss`
|
||||
value from the decoded claims and enable strict validation:
|
||||
|
||||
```python
|
||||
auth = AzureProvider.from_b2c(
|
||||
tenant_name="mytenant",
|
||||
policy_name="B2C_1_susi",
|
||||
client_id="00000000-0000-0000-0000-000000000000",
|
||||
client_secret="my-secret",
|
||||
required_scopes=["mcp-access"],
|
||||
base_url="https://myserver.com",
|
||||
token_issuer="https://mytenant.b2clogin.com/11111111-2222-3333-4444-555555555555/v2.0/",
|
||||
)
|
||||
```
|
||||
|
||||
### Custom Domains
|
||||
|
||||
If your B2C tenant uses a [custom domain](https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-domain)
|
||||
(e.g. `auth.mycompany.com` instead of `mytenant.b2clogin.com`), pass it via
|
||||
`custom_domain`:
|
||||
|
||||
```python
|
||||
auth = AzureProvider.from_b2c(
|
||||
tenant_name="mytenant",
|
||||
policy_name="B2C_1_susi",
|
||||
client_id="00000000-0000-0000-0000-000000000000",
|
||||
client_secret="my-secret",
|
||||
required_scopes=["mcp-access"],
|
||||
base_url="https://myserver.com",
|
||||
custom_domain="auth.mycompany.com",
|
||||
)
|
||||
```
|
||||
157
docs/v3/integrations/chatgpt.mdx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
---
|
||||
title: ChatGPT 🤝 FastMCP
|
||||
sidebarTitle: ChatGPT
|
||||
description: Connect FastMCP servers to ChatGPT in Chat and Deep Research modes
|
||||
icon: message-smile
|
||||
---
|
||||
|
||||
[ChatGPT](https://chatgpt.com/) supports MCP servers through remote HTTP connections in two modes: **Chat mode** for interactive conversations and **Deep Research mode** for comprehensive information retrieval.
|
||||
|
||||
<Tip>
|
||||
**Developer Mode Required for Chat Mode**: To use MCP servers in regular ChatGPT conversations, you must first enable Developer Mode in your ChatGPT settings. This feature is available for ChatGPT Pro, Team, Enterprise, and Edu users.
|
||||
</Tip>
|
||||
|
||||
<Note>
|
||||
OpenAI's official MCP documentation and examples are built with **FastMCP v2**! Learn more from their [MCP documentation](https://platform.openai.com/docs/mcp) and [Developer Mode guide](https://platform.openai.com/docs/guides/developer-mode).
|
||||
</Note>
|
||||
|
||||
## Build a Server
|
||||
|
||||
First, let's create a simple FastMCP server:
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
import random
|
||||
|
||||
mcp = FastMCP("Demo Server")
|
||||
|
||||
@mcp.tool
|
||||
def roll_dice(sides: int = 6) -> int:
|
||||
"""Roll a dice with the specified number of sides."""
|
||||
return random.randint(1, sides)
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="http", port=8000)
|
||||
```
|
||||
|
||||
### Deploy Your Server
|
||||
|
||||
Your server must be accessible from the internet. For development, use `ngrok`:
|
||||
|
||||
<CodeGroup>
|
||||
```bash Terminal 1
|
||||
python server.py
|
||||
```
|
||||
|
||||
```bash Terminal 2
|
||||
ngrok http 8000
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Note your public URL (e.g., `https://abc123.ngrok.io`) for the next steps.
|
||||
|
||||
## Chat Mode
|
||||
|
||||
Chat mode lets you use MCP tools directly in ChatGPT conversations. See [OpenAI's Developer Mode guide](https://platform.openai.com/docs/guides/developer-mode) for the latest requirements.
|
||||
|
||||
### Add to ChatGPT
|
||||
|
||||
#### 1. Enable Developer Mode
|
||||
|
||||
1. Open ChatGPT and go to **Settings** → **Connectors**
|
||||
2. Under **Advanced**, toggle **Developer Mode** to enabled
|
||||
|
||||
#### 2. Create Connector
|
||||
|
||||
1. In **Settings** → **Connectors**, click **Create**
|
||||
2. Enter:
|
||||
- **Name**: Your server name
|
||||
- **Server URL**: `https://your-server.ngrok.io/mcp/`
|
||||
3. Check **I trust this provider**
|
||||
4. Add authentication if needed
|
||||
5. Click **Create**
|
||||
|
||||
<Note>
|
||||
**Without Developer Mode**: If you don't have search/fetch tools, ChatGPT will reject the server. With Developer Mode enabled, you don't need search/fetch tools for Chat mode.
|
||||
</Note>
|
||||
|
||||
#### 3. Use in Chat
|
||||
|
||||
1. Start a new chat
|
||||
2. Click the **+** button → **More** → **Developer Mode**
|
||||
3. **Enable your MCP server connector** (required - the connector must be explicitly added to each chat)
|
||||
4. Now you can use your tools:
|
||||
|
||||
Example usage:
|
||||
- "Roll a 20-sided dice"
|
||||
- "Roll dice" (uses default 6 sides)
|
||||
|
||||
<Tip>
|
||||
The connector must be explicitly enabled in each chat session through Developer Mode. Once added, it remains active for the entire conversation.
|
||||
</Tip>
|
||||
|
||||
### Skip Confirmations
|
||||
|
||||
Use `annotations=ToolAnnotations(readOnlyHint=True)` to skip confirmation prompts for read-only tools:
|
||||
|
||||
```python
|
||||
from mcp.types import ToolAnnotations
|
||||
|
||||
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
||||
def get_status() -> str:
|
||||
"""Check system status."""
|
||||
return "All systems operational"
|
||||
|
||||
@mcp.tool() # No annotation - ChatGPT may ask for confirmation
|
||||
def delete_item(id: str) -> str:
|
||||
"""Delete an item."""
|
||||
return f"Deleted {id}"
|
||||
```
|
||||
|
||||
## Deep Research Mode
|
||||
|
||||
Deep Research mode provides systematic information retrieval with citations. See [OpenAI's MCP documentation](https://platform.openai.com/docs/mcp) for the latest Deep Research specifications.
|
||||
|
||||
<Warning>
|
||||
**Search and Fetch Required**: Without Developer Mode, ChatGPT will reject any server that doesn't have both `search` and `fetch` tools. Even in Developer Mode, Deep Research only uses these two tools.
|
||||
</Warning>
|
||||
|
||||
### Tool Implementation
|
||||
|
||||
Deep Research tools must follow this pattern:
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
def search(query: str) -> dict:
|
||||
"""
|
||||
Search for records matching the query.
|
||||
Must return {"ids": [list of string IDs]}
|
||||
"""
|
||||
# Your search logic
|
||||
matching_ids = ["id1", "id2", "id3"]
|
||||
return {"ids": matching_ids}
|
||||
|
||||
@mcp.tool()
|
||||
def fetch(id: str) -> dict:
|
||||
"""
|
||||
Fetch a complete record by ID.
|
||||
Return the full record data for ChatGPT to analyze.
|
||||
"""
|
||||
# Your fetch logic
|
||||
return {
|
||||
"id": id,
|
||||
"title": "Record Title",
|
||||
"content": "Full record content...",
|
||||
"metadata": {"author": "Jane Doe", "date": "2024"}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Deep Research
|
||||
|
||||
1. Ensure your server is added to ChatGPT's connectors (same as Chat mode)
|
||||
2. Start a new chat
|
||||
3. Click **+** → **Deep Research**
|
||||
4. Select your MCP server as a source
|
||||
5. Ask research questions
|
||||
|
||||
ChatGPT will use your `search` and `fetch` tools to find and cite relevant information.
|
||||
177
docs/v3/integrations/claude-code.mdx
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
---
|
||||
title: Claude Code 🤝 FastMCP
|
||||
sidebarTitle: Claude Code
|
||||
description: Install and use FastMCP servers in Claude Code
|
||||
icon: message-smile
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
import { LocalFocusTip } from "/snippets/local-focus.mdx"
|
||||
|
||||
<LocalFocusTip />
|
||||
|
||||
[Claude Code](https://docs.anthropic.com/en/docs/claude-code) supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers.
|
||||
|
||||
## Requirements
|
||||
|
||||
This integration uses STDIO transport to run your FastMCP server locally. For remote deployments, you can run your FastMCP server with HTTP or SSE transport and configure it directly using Claude Code's built-in MCP management commands.
|
||||
|
||||
## Create a Server
|
||||
|
||||
The examples in this guide will use the following simple dice-rolling server, saved as `server.py`.
|
||||
|
||||
```python server.py
|
||||
import random
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP(name="Dice Roller")
|
||||
|
||||
@mcp.tool
|
||||
def roll_dice(n_dice: int) -> list[int]:
|
||||
"""Roll `n_dice` 6-sided dice and return the results."""
|
||||
return [random.randint(1, 6) for _ in range(n_dice)]
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
## Install the Server
|
||||
|
||||
### FastMCP CLI
|
||||
<VersionBadge version="2.10.3" />
|
||||
|
||||
The easiest way to install a FastMCP server in Claude Code is using the `fastmcp install claude-code` command. This automatically handles the configuration, dependency management, and calls Claude Code's built-in MCP management system.
|
||||
|
||||
```bash
|
||||
fastmcp install claude-code server.py
|
||||
```
|
||||
|
||||
The install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:
|
||||
|
||||
```bash
|
||||
# These are equivalent if your server object is named 'mcp'
|
||||
fastmcp install claude-code server.py
|
||||
fastmcp install claude-code server.py:mcp
|
||||
|
||||
# Use explicit object name if your server has a different name
|
||||
fastmcp install claude-code server.py:my_custom_server
|
||||
```
|
||||
|
||||
The command will automatically configure the server with Claude Code's `claude mcp add` command.
|
||||
|
||||
#### Dependencies
|
||||
|
||||
FastMCP provides flexible dependency management options for your Claude Code servers:
|
||||
|
||||
**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-code server.py --with pandas --with requests
|
||||
```
|
||||
|
||||
**Requirements file**: If you maintain a `requirements.txt` file with all your dependencies, use `--with-requirements` to install them:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-code server.py --with-requirements requirements.txt
|
||||
```
|
||||
|
||||
**Editable packages**: For local packages under development, use `--with-editable` to install them in editable mode:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-code server.py --with-editable ./my-local-package
|
||||
```
|
||||
|
||||
Alternatively, you can use a `fastmcp.json` configuration file (recommended):
|
||||
|
||||
```json fastmcp.json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
|
||||
"source": {
|
||||
"path": "server.py",
|
||||
"entrypoint": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
"dependencies": ["pandas", "requests"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### Python Version and Project Configuration
|
||||
|
||||
Control the Python environment for your server with these options:
|
||||
|
||||
**Python version**: Use `--python` to specify which Python version your server requires. This ensures compatibility when your server needs specific Python features:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-code server.py --python 3.11
|
||||
```
|
||||
|
||||
**Project directory**: Use `--project` to run your server within a specific project context. This tells `uv` to use the project's configuration files and virtual environment:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-code server.py --project /path/to/my-project
|
||||
```
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
If your server needs environment variables (like API keys), you must include them:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-code server.py --server-name "Weather Server" \
|
||||
--env API_KEY=your-api-key \
|
||||
--env DEBUG=true
|
||||
```
|
||||
|
||||
Or load them from a `.env` file:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-code server.py --server-name "Weather Server" --env-file .env
|
||||
```
|
||||
|
||||
<Warning>
|
||||
**Claude Code must be installed**. The integration looks for the Claude Code CLI at the default installation location (`~/.claude/local/claude`) and uses the `claude mcp add` command to register servers.
|
||||
</Warning>
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
For more control over the configuration, you can manually use Claude Code's built-in MCP management commands. This gives you direct control over how your server is launched:
|
||||
|
||||
```bash
|
||||
# Add a server with custom configuration
|
||||
claude mcp add dice-roller -- uv run --with fastmcp fastmcp run server.py
|
||||
|
||||
# Add with environment variables
|
||||
claude mcp add weather-server -e API_KEY=secret -e DEBUG=true -- uv run --with fastmcp fastmcp run server.py
|
||||
|
||||
# Add with specific scope (local, user, or project)
|
||||
claude mcp add my-server --scope user -- uv run --with fastmcp fastmcp run server.py
|
||||
```
|
||||
|
||||
You can also manually specify Python versions and project directories in your Claude Code commands:
|
||||
|
||||
```bash
|
||||
# With specific Python version
|
||||
claude mcp add ml-server -- uv run --python 3.11 --with fastmcp fastmcp run server.py
|
||||
|
||||
# Within a project directory
|
||||
claude mcp add project-server -- uv run --project /path/to/project --with fastmcp fastmcp run server.py
|
||||
```
|
||||
|
||||
## Using the Server
|
||||
|
||||
Once your server is installed, you can start using your FastMCP server with Claude Code.
|
||||
|
||||
Try asking Claude something like:
|
||||
|
||||
> "Roll some dice for me"
|
||||
|
||||
Claude will automatically detect your `roll_dice` tool and use it to fulfill your request, returning something like:
|
||||
|
||||
> I'll roll some dice for you! Here are your results: [4, 2, 6]
|
||||
>
|
||||
> You rolled three dice and got a 4, a 2, and a 6!
|
||||
|
||||
Claude Code can now access all the tools, resources, and prompts you've defined in your FastMCP server.
|
||||
|
||||
If your server provides resources, you can reference them with `@` mentions using the format `@server:protocol://resource/path`. If your server provides prompts, you can use them as slash commands with `/mcp__servername__promptname`.
|
||||
299
docs/v3/integrations/claude-desktop.mdx
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
---
|
||||
title: Claude Desktop 🤝 FastMCP
|
||||
sidebarTitle: Claude Desktop
|
||||
description: Connect FastMCP servers to Claude Desktop
|
||||
icon: message-smile
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
import { LocalFocusTip } from "/snippets/local-focus.mdx"
|
||||
|
||||
<LocalFocusTip />
|
||||
|
||||
[Claude Desktop](https://www.claude.com/download) supports MCP servers through local STDIO connections and remote servers (beta), allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers.
|
||||
|
||||
<Note>
|
||||
Remote MCP server support is currently in beta and available for users on Claude Pro, Max, Team, and Enterprise plans (as of June 2025). Most users will still need to use local STDIO connections.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
This guide focuses specifically on using FastMCP servers with Claude Desktop. For general Claude Desktop MCP setup and official examples, see the [official Claude Desktop quickstart guide](https://modelcontextprotocol.io/quickstart/user).
|
||||
</Note>
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
Claude Desktop traditionally requires MCP servers to run locally using STDIO transport, where your server communicates with Claude through standard input/output rather than HTTP. However, users on certain plans now have access to remote server support as well.
|
||||
|
||||
<Tip>
|
||||
If you don't have access to remote server support or need to connect to remote servers, you can create a **proxy server** that runs locally via STDIO and forwards requests to remote HTTP servers. See the [Proxy Servers](#proxy-servers) section below.
|
||||
</Tip>
|
||||
|
||||
## Create a Server
|
||||
|
||||
The examples in this guide will use the following simple dice-rolling server, saved as `server.py`.
|
||||
|
||||
```python server.py
|
||||
import random
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP(name="Dice Roller")
|
||||
|
||||
@mcp.tool
|
||||
def roll_dice(n_dice: int) -> list[int]:
|
||||
"""Roll `n_dice` 6-sided dice and return the results."""
|
||||
return [random.randint(1, 6) for _ in range(n_dice)]
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
## Install the Server
|
||||
|
||||
### FastMCP CLI
|
||||
<VersionBadge version="2.10.3" />
|
||||
|
||||
The easiest way to install a FastMCP server in Claude Desktop is using the `fastmcp install claude-desktop` command. This automatically handles the configuration and dependency management.
|
||||
|
||||
<Tip>
|
||||
Prior to version 2.10.3, Claude Desktop could be managed by running `fastmcp install <path>` without specifying the client.
|
||||
</Tip>
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop server.py
|
||||
```
|
||||
|
||||
The install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:
|
||||
|
||||
```bash
|
||||
# These are equivalent if your server object is named 'mcp'
|
||||
fastmcp install claude-desktop server.py
|
||||
fastmcp install claude-desktop server.py:mcp
|
||||
|
||||
# Use explicit object name if your server has a different name
|
||||
fastmcp install claude-desktop server.py:my_custom_server
|
||||
```
|
||||
|
||||
After installation, restart Claude Desktop completely. You should see a hammer icon (🔨) in the bottom left of the input box, indicating that MCP tools are available.
|
||||
|
||||
#### Dependencies
|
||||
|
||||
FastMCP provides several ways to manage your server's dependencies when installing in Claude Desktop:
|
||||
|
||||
**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop server.py --with pandas --with requests
|
||||
```
|
||||
|
||||
**Requirements file**: If you have a `requirements.txt` file listing all your dependencies, use `--with-requirements` to install them all at once:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop server.py --with-requirements requirements.txt
|
||||
```
|
||||
|
||||
**Editable packages**: For local packages in development, use `--with-editable` to install them in editable mode:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop server.py --with-editable ./my-local-package
|
||||
```
|
||||
|
||||
Alternatively, you can use a `fastmcp.json` configuration file (recommended):
|
||||
|
||||
```json fastmcp.json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
|
||||
"source": {
|
||||
"path": "server.py",
|
||||
"entrypoint": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
"dependencies": ["pandas", "requests"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### Python Version and Project Directory
|
||||
|
||||
FastMCP allows you to control the Python environment for your server:
|
||||
|
||||
**Python version**: Use `--python` to specify which Python version your server should run with. This is particularly useful when your server requires a specific Python version:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop server.py --python 3.11
|
||||
```
|
||||
|
||||
**Project directory**: Use `--project` to run your server within a specific project directory. This ensures that `uv` will discover all `pyproject.toml`, `uv.toml`, and `.python-version` files from that project:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop server.py --project /path/to/my-project
|
||||
```
|
||||
|
||||
When you specify a project directory, all relative paths in your server will be resolved from that directory, and the project's virtual environment will be used.
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
<Warning>
|
||||
Claude Desktop runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.
|
||||
</Warning>
|
||||
|
||||
If your server needs environment variables (like API keys), you must include them:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop server.py --server-name "Weather Server" \
|
||||
--env API_KEY=your-api-key \
|
||||
--env DEBUG=true
|
||||
```
|
||||
|
||||
Or load them from a `.env` file:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop server.py --server-name "Weather Server" --env-file .env
|
||||
```
|
||||
<Warning>
|
||||
- **`uv` must be installed and available in your system PATH**. Claude Desktop runs in its own isolated environment and needs `uv` to manage dependencies.
|
||||
- **On macOS, it is recommended to install `uv` globally with Homebrew** so that Claude Desktop will detect it: `brew install uv`. Installing `uv` with other methods may not make it accessible to Claude Desktop.
|
||||
</Warning>
|
||||
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
For more control over the configuration, you can manually edit Claude Desktop's configuration file. You can open the configuration file from Claude's developer settings, or find it in the following locations:
|
||||
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
|
||||
The configuration file is a JSON object with a `mcpServers` key, which contains the configuration for each MCP server.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"dice-roller": {
|
||||
"command": "python",
|
||||
"args": ["path/to/your/server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After updating the configuration file, restart Claude Desktop completely. Look for the hammer icon (🔨) to confirm your server is loaded.
|
||||
|
||||
#### Dependencies
|
||||
|
||||
If your server has dependencies, you can use `uv` or another package manager to set up the environment.
|
||||
|
||||
|
||||
When manually configuring dependencies, the recommended approach is to use `uv` with FastMCP. The configuration uses `uv run` to create an isolated environment with your specified packages:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"dice-roller": {
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"--with", "fastmcp",
|
||||
"--with", "pandas",
|
||||
"--with", "requests",
|
||||
"fastmcp",
|
||||
"run",
|
||||
"path/to/your/server.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also manually specify Python versions and project directories in your configuration. Add `--python` to use a specific Python version, or `--project` to run within a project directory:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"dice-roller": {
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"--python", "3.11",
|
||||
"--project", "/path/to/project",
|
||||
"--with", "fastmcp",
|
||||
"fastmcp",
|
||||
"run",
|
||||
"path/to/your/server.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The order of arguments matters: Python version and project settings come before package specifications, which come before the actual command to run.
|
||||
|
||||
<Warning>
|
||||
- **`uv` must be installed and available in your system PATH**. Claude Desktop runs in its own isolated environment and needs `uv` to manage dependencies.
|
||||
- **On macOS, it is recommended to install `uv` globally with Homebrew** so that Claude Desktop will detect it: `brew install uv`. Installing `uv` with other methods may not make it accessible to Claude Desktop.
|
||||
</Warning>
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
You can also specify environment variables in the configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"weather-server": {
|
||||
"command": "python",
|
||||
"args": ["path/to/weather_server.py"],
|
||||
"env": {
|
||||
"API_KEY": "your-api-key",
|
||||
"DEBUG": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
<Warning>
|
||||
Claude Desktop runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.
|
||||
</Warning>
|
||||
|
||||
|
||||
## Remote Servers
|
||||
|
||||
|
||||
Users on Claude Pro, Max, Team, and Enterprise plans have first-class remote server support via integrations. For other users, or as an alternative approach, FastMCP can create a proxy server that forwards requests to a remote HTTP server. You can install the proxy server in Claude Desktop.
|
||||
|
||||
Create a proxy server that connects to a remote HTTP server:
|
||||
|
||||
```python proxy_server.py
|
||||
from fastmcp.server import create_proxy
|
||||
|
||||
# Create a proxy to a remote server
|
||||
proxy = create_proxy(
|
||||
"https://example.com/mcp/sse",
|
||||
name="Remote Server Proxy"
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
proxy.run() # Runs via STDIO for Claude Desktop
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
For authenticated remote servers, create an authenticated client following the guidance in the [client auth documentation](/clients/auth/bearer) and pass it to the proxy:
|
||||
|
||||
```python auth_proxy_server.py {7}
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import BearerAuth
|
||||
from fastmcp.server import create_proxy
|
||||
|
||||
# Create authenticated client
|
||||
client = Client(
|
||||
"https://api.example.com/mcp/sse",
|
||||
auth=BearerAuth(token="your-access-token")
|
||||
)
|
||||
|
||||
# Create proxy using the authenticated client
|
||||
proxy = create_proxy(client, name="Authenticated Proxy")
|
||||
|
||||
if __name__ == "__main__":
|
||||
proxy.run()
|
||||
```
|
||||
|
||||