From 40ce687d0d62fdd042227cee442cf43fe8f6abc3 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:39:55 -0400 Subject: [PATCH 01/41] Fix JWT issuer validation to support string values per RFC 7519 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves issue where BearerAuthProvider rejected tokens with non-URL issuer claims. Works around the underlying SDK's URL validation while maintaining RFC 7519 compliance for JWT processing. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/server/auth/providers/bearer.py | 12 ++++- tests/auth/providers/test_bearer.py | 53 +++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/server/auth/providers/bearer.py b/src/fastmcp/server/auth/providers/bearer.py index 763f90f4f..e544802f9 100644 --- a/src/fastmcp/server/auth/providers/bearer.py +++ b/src/fastmcp/server/auth/providers/bearer.py @@ -17,7 +17,7 @@ from mcp.shared.auth import ( OAuthClientInformationFull, OAuthToken, ) -from pydantic import SecretStr +from pydantic import AnyHttpUrl, SecretStr from fastmcp.server.auth.auth import ( ClientRegistrationOptions, @@ -179,8 +179,16 @@ class BearerAuthProvider(OAuthProvider): if public_key and jwks_uri: raise ValueError("Provide either public_key or jwks_uri, not both") + # Only pass issuer to parent if it's a valid URL, otherwise use default + # This allows the issuer claim validation to work with string issuers per RFC 7519 + try: + issuer_url = AnyHttpUrl(issuer) if issuer else "https://fastmcp.example.com" + except Exception: + # Issuer is not a valid URL, use default for parent class + issuer_url = "https://fastmcp.example.com" + super().__init__( - issuer_url=issuer or "https://fastmcp.example.com", + issuer_url=issuer_url, client_registration_options=ClientRegistrationOptions(enabled=False), revocation_options=RevocationOptions(enabled=False), required_scopes=required_scopes, diff --git a/tests/auth/providers/test_bearer.py b/tests/auth/providers/test_bearer.py index 6f59c96fe..31d623d2c 100644 --- a/tests/auth/providers/test_bearer.py +++ b/tests/auth/providers/test_bearer.py @@ -539,6 +539,59 @@ class TestBearerToken: assert access_token is not None assert access_token.client_id == "app456" # Should prefer client_id over sub + async def test_string_issuer_validation(self, rsa_key_pair: RSAKeyPair): + """Test that string (non-URL) issuers are supported per RFC 7519.""" + # Create provider with string issuer + provider = BearerAuthProvider( + public_key=rsa_key_pair.public_key, + issuer="my-service", # String issuer, not a URL + ) + + # Create token with matching string issuer + token = rsa_key_pair.create_token( + subject="test-user", + issuer="my-service", # Same string issuer + ) + + access_token = await provider.load_access_token(token) + assert access_token is not None + assert access_token.client_id == "test-user" + + async def test_string_issuer_mismatch_rejection(self, rsa_key_pair: RSAKeyPair): + """Test that mismatched string issuers are rejected.""" + # Create provider with one string issuer + provider = BearerAuthProvider( + public_key=rsa_key_pair.public_key, + issuer="my-service", + ) + + # Create token with different string issuer + token = rsa_key_pair.create_token( + subject="test-user", + issuer="other-service", # Different string issuer + ) + + access_token = await provider.load_access_token(token) + assert access_token is None + + async def test_url_issuer_still_works(self, rsa_key_pair: RSAKeyPair): + """Test that URL issuers still work after the fix.""" + # Create provider with URL issuer + provider = BearerAuthProvider( + public_key=rsa_key_pair.public_key, + issuer="https://my-auth-server.com", # URL issuer + ) + + # Create token with matching URL issuer + token = rsa_key_pair.create_token( + subject="test-user", + issuer="https://my-auth-server.com", # Same URL issuer + ) + + access_token = await provider.load_access_token(token) + assert access_token is not None + assert access_token.client_id == "test-user" + class TestFastMCPBearerAuth: def test_bearer_auth(self): From 7b5ef3e5e5e0316b90ddabb25d3879908bbed75b Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:50:16 -0400 Subject: [PATCH 02/41] Use specific ValidationError instead of broad Exception catch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses PR feedback to use more specific exception handling for URL validation failures. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/server/auth/providers/bearer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/server/auth/providers/bearer.py b/src/fastmcp/server/auth/providers/bearer.py index e544802f9..a4f3a8e48 100644 --- a/src/fastmcp/server/auth/providers/bearer.py +++ b/src/fastmcp/server/auth/providers/bearer.py @@ -17,7 +17,7 @@ from mcp.shared.auth import ( OAuthClientInformationFull, OAuthToken, ) -from pydantic import AnyHttpUrl, SecretStr +from pydantic import AnyHttpUrl, SecretStr, ValidationError from fastmcp.server.auth.auth import ( ClientRegistrationOptions, @@ -183,7 +183,7 @@ class BearerAuthProvider(OAuthProvider): # This allows the issuer claim validation to work with string issuers per RFC 7519 try: issuer_url = AnyHttpUrl(issuer) if issuer else "https://fastmcp.example.com" - except Exception: + except ValidationError: # Issuer is not a valid URL, use default for parent class issuer_url = "https://fastmcp.example.com" From 42460480de275257348911f1e77643115bc140b2 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:13:00 -0400 Subject: [PATCH 03/41] Fix BearerAuthProvider audience type annotations to support List[str] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The audience parameter was typed as `str | None` but the implementation already supported `List[str]`. This fix aligns the type annotations with the actual functionality and adds comprehensive validation logic for all audience type combinations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/server/auth/providers/bearer.py | 30 ++++++++++++----- tests/auth/providers/test_bearer.py | 37 +++++++++++++++++++++ 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/fastmcp/server/auth/providers/bearer.py b/src/fastmcp/server/auth/providers/bearer.py index a4f3a8e48..a18d30b16 100644 --- a/src/fastmcp/server/auth/providers/bearer.py +++ b/src/fastmcp/server/auth/providers/bearer.py @@ -89,7 +89,7 @@ class RSAKeyPair: self, subject: str = "fastmcp-user", issuer: str = "https://fastmcp.example.com", - audience: str | None = None, + audience: str | list[str] | None = None, scopes: list[str] | None = None, expires_in_seconds: int = 3600, additional_claims: dict[str, Any] | None = None, @@ -102,7 +102,7 @@ class RSAKeyPair: private_key_pem: RSA private key in PEM format subject: Subject claim (usually user ID) issuer: Issuer claim - audience: Audience claim (optional) + audience: Audience claim - can be a string or list of strings (optional) scopes: List of scopes to include expires_in_seconds: Token expiration time in seconds additional_claims: Any additional claims to include @@ -161,7 +161,7 @@ class BearerAuthProvider(OAuthProvider): public_key: str | None = None, jwks_uri: str | None = None, issuer: str | None = None, - audience: str | None = None, + audience: str | list[str] | None = None, required_scopes: list[str] | None = None, ): """ @@ -171,7 +171,7 @@ class BearerAuthProvider(OAuthProvider): public_key: RSA public key in PEM format (for static key) jwks_uri: URI to fetch keys from (for key rotation) issuer: Expected issuer claim (optional) - audience: Expected audience claim (optional) + audience: Expected audience claim - can be a string or list of strings (optional) required_scopes: List of required scopes for access (optional) """ if not (public_key or jwks_uri): @@ -312,11 +312,25 @@ class BearerAuthProvider(OAuthProvider): # Validate audience if configured if self.audience: aud = claims.get("aud") - if isinstance(aud, list): - if self.audience not in aud: + + # Handle different combinations of audience types + if isinstance(self.audience, list): + # self.audience is a list - check if any expected audience is present + if isinstance(aud, list): + # Both are lists - check for intersection + if not any(expected in aud for expected in self.audience): + return None + else: + # aud is a string - check if it's in our expected list + if aud not in self.audience: + return None + else: + # self.audience is a string - use original logic + if isinstance(aud, list): + if self.audience not in aud: + return None + elif aud != self.audience: return None - elif aud != self.audience: - return None # Extract claims - prefer client_id over sub for OAuth application identification client_id = claims.get("client_id") or claims.get("sub") or "unknown" diff --git a/tests/auth/providers/test_bearer.py b/tests/auth/providers/test_bearer.py index 31d623d2c..ba6692c54 100644 --- a/tests/auth/providers/test_bearer.py +++ b/tests/auth/providers/test_bearer.py @@ -446,6 +446,43 @@ class TestBearerToken: access_token = await provider.load_access_token(token) assert access_token is not None + async def test_provider_with_multiple_expected_audiences(self, rsa_key_pair: RSAKeyPair): + """Test provider configured with multiple expected audiences.""" + provider = BearerAuthProvider( + public_key=rsa_key_pair.public_key, + issuer="https://test.example.com", + audience=["https://api.example.com", "https://other-api.example.com"], + ) + + # Token with single audience that matches one of the expected + token1 = rsa_key_pair.create_token( + subject="test-user", + issuer="https://test.example.com", + audience="https://api.example.com", + ) + access_token1 = await provider.load_access_token(token1) + assert access_token1 is not None + + # Token with multiple audiences, one of which matches + token2 = rsa_key_pair.create_token( + subject="test-user", + issuer="https://test.example.com", + additional_claims={ + "aud": ["https://api.example.com", "https://third-party.example.com"] + }, + ) + access_token2 = await provider.load_access_token(token2) + assert access_token2 is not None + + # Token with audience that doesn't match any expected + token3 = rsa_key_pair.create_token( + subject="test-user", + issuer="https://test.example.com", + audience="https://wrong-api.example.com", + ) + access_token3 = await provider.load_access_token(token3) + assert access_token3 is None + async def test_scope_extraction_string( self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider ): From a2f406faa4bac1b23f449cd5dae0e35d45061a56 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:17:47 -0400 Subject: [PATCH 04/41] Apply pre-commit formatting fixes --- src/fastmcp/server/auth/providers/bearer.py | 2 +- tests/auth/providers/test_bearer.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/server/auth/providers/bearer.py b/src/fastmcp/server/auth/providers/bearer.py index a18d30b16..6ffffa909 100644 --- a/src/fastmcp/server/auth/providers/bearer.py +++ b/src/fastmcp/server/auth/providers/bearer.py @@ -312,7 +312,7 @@ class BearerAuthProvider(OAuthProvider): # Validate audience if configured if self.audience: aud = claims.get("aud") - + # Handle different combinations of audience types if isinstance(self.audience, list): # self.audience is a list - check if any expected audience is present diff --git a/tests/auth/providers/test_bearer.py b/tests/auth/providers/test_bearer.py index ba6692c54..efed070d4 100644 --- a/tests/auth/providers/test_bearer.py +++ b/tests/auth/providers/test_bearer.py @@ -446,7 +446,9 @@ class TestBearerToken: access_token = await provider.load_access_token(token) assert access_token is not None - async def test_provider_with_multiple_expected_audiences(self, rsa_key_pair: RSAKeyPair): + async def test_provider_with_multiple_expected_audiences( + self, rsa_key_pair: RSAKeyPair + ): """Test provider configured with multiple expected audiences.""" provider = BearerAuthProvider( public_key=rsa_key_pair.public_key, From ac252fc28f97182c1794e70cbd1cf2687d85c81d Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:18:51 -0400 Subject: [PATCH 05/41] Fix CORS documentation example to properly handle preflight requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous example only allowed GET methods, causing browser preflight requests to fail with "Disallowed CORS method" errors. Updated to include the required parameters for proper CORS support with MCP clients. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 7 ++++++- docs/deployment/asgi.mdx | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 24f9846b1..1da059260 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,4 +27,9 @@ Only use HTTP transport when testing network-specific features. Prefer Streamabl # Only when network testing is required async with Client(transport=StreamableHttpTransport(server_url)) as client: result = await client.ping() -``` \ No newline at end of file +``` + +## Development Workflow + +- You must always run pre-commit if you open a PR, because it is run as part of a required check. +- When opening PRs, apply labels appropriately for bugs/breaking changes/enhancements/features. Generally, improvements are enhancements (not features) unless told otherwise. \ No newline at end of file diff --git a/docs/deployment/asgi.mdx b/docs/deployment/asgi.mdx index 06da46374..56947cde9 100644 --- a/docs/deployment/asgi.mdx +++ b/docs/deployment/asgi.mdx @@ -96,7 +96,13 @@ mcp = FastMCP("MyServer") # Define custom middleware custom_middleware = [ - Middleware(CORSMiddleware, allow_origins=["*"]), + Middleware( + CORSMiddleware, + allow_origins=["https://example.com", "https://app.example.com"], + allow_credentials=True, + allow_methods=["GET", "POST", "OPTIONS"], + allow_headers=["Content-Type", "Authorization"], + ), ] # Create ASGI app with custom middleware From 4b303f1ae00257d310c6b11da37e639897fcb12f Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:29:25 -0400 Subject: [PATCH 06/41] Fix StreamableHTTP redirect issue for MCP spec compliance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves 307 redirect from /mcp to /mcp/ by replacing Mount with explicit Route entries for both path variations. The MCP specification requires a single endpoint to handle both GET and POST requests without redirects. Changes: - Replace Mount with Route for both /mcp and /mcp/ paths - Add PathNormalizingASGIApp wrapper to normalize trailing slashes for MCP SDK - Apply same pattern to SSE transport for consistency - Ensure both auth and non-auth configurations work correctly This fixes the redirect issue mentioned in GitHub issue #828. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/server/http.py | 65 ++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index b9121dfe8..696f861fd 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -157,6 +157,11 @@ def create_sse_app( Returns: A Starlette application with RequestContextMiddleware """ + + # Ensure the message_path ends with a trailing slash to avoid automatic redirects + # when mounting the application. sse_path uses Route instead of Mount so no fix needed. + if not message_path.endswith("/"): + message_path = message_path + "/" server_routes: list[BaseRoute] = [] server_middleware: list[Middleware] = [] @@ -305,7 +310,28 @@ def create_streamable_http_app( # Re-raise other RuntimeErrors if they don't match the specific message raise - # Add StreamableHTTP routes with or without auth + # Create an ASGI app wrapper that normalizes paths and handles both /mcp and /mcp/ + class PathNormalizingASGIApp: + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + # Normalize path to remove trailing slash for MCP SDK + if scope["type"] == "http": + path = scope["path"] + if path.endswith("/") and len(path) > 1: + scope = dict(scope) + scope["path"] = path.rstrip("/") + + await self.app(scope, receive, send) + + # Create the path-normalizing wrapper + normalized_handler = PathNormalizingASGIApp(handle_streamable_http) + + # Create path pattern without trailing slash + path_pattern = streamable_http_path.rstrip("/") + + # Add StreamableHTTP routes with or without auth - add both with and without trailing slash if auth: auth_middleware, auth_routes, required_scopes = ( setup_auth_middleware_and_routes(auth) @@ -314,19 +340,38 @@ def create_streamable_http_app( server_routes.extend(auth_routes) server_middleware.extend(auth_middleware) - # Auth is enabled, wrap endpoint with RequireAuthMiddleware + # Auth is enabled, wrap app with RequireAuthMiddleware + wrapped_app = RequireAuthMiddleware(normalized_handler, required_scopes) + + # Add routes for both with and without trailing slash server_routes.append( - Mount( - streamable_http_path, - app=RequireAuthMiddleware(handle_streamable_http, required_scopes), + Route( + path_pattern, + endpoint=wrapped_app, + methods=["GET", "POST"] + ) + ) + server_routes.append( + Route( + path_pattern + "/", + endpoint=wrapped_app, + methods=["GET", "POST"] ) ) else: - # No auth required + # No auth required - add routes for both with and without trailing slash server_routes.append( - Mount( - streamable_http_path, - app=handle_streamable_http, + Route( + path_pattern, + endpoint=normalized_handler, + methods=["GET", "POST"] + ) + ) + server_routes.append( + Route( + path_pattern + "/", + endpoint=normalized_handler, + methods=["GET", "POST"] ) ) @@ -355,6 +400,6 @@ def create_streamable_http_app( # Store the FastMCP server instance on the Starlette app state app.state.fastmcp_server = server - app.state.path = streamable_http_path + app.state.path = streamable_http_path.rstrip("/") return app From b37686cf153b984ad73f9c50267b91a8d31e37f4 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:52:42 -0400 Subject: [PATCH 07/41] Use trailing slashes --- src/fastmcp/server/http.py | 63 +++++++++----------------------------- src/fastmcp/settings.py | 4 +-- 2 files changed, 16 insertions(+), 51 deletions(-) diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index 696f861fd..a80b2235c 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -310,28 +310,12 @@ def create_streamable_http_app( # Re-raise other RuntimeErrors if they don't match the specific message raise - # Create an ASGI app wrapper that normalizes paths and handles both /mcp and /mcp/ - class PathNormalizingASGIApp: - def __init__(self, app): - self.app = app - - async def __call__(self, scope, receive, send): - # Normalize path to remove trailing slash for MCP SDK - if scope["type"] == "http": - path = scope["path"] - if path.endswith("/") and len(path) > 1: - scope = dict(scope) - scope["path"] = path.rstrip("/") - - await self.app(scope, receive, send) + # Ensure the streamable_http_path ends with a trailing slash to avoid automatic redirects + # when mounting the application + if not streamable_http_path.endswith("/"): + streamable_http_path = streamable_http_path + "/" - # Create the path-normalizing wrapper - normalized_handler = PathNormalizingASGIApp(handle_streamable_http) - - # Create path pattern without trailing slash - path_pattern = streamable_http_path.rstrip("/") - - # Add StreamableHTTP routes with or without auth - add both with and without trailing slash + # Add StreamableHTTP routes with or without auth if auth: auth_middleware, auth_routes, required_scopes = ( setup_auth_middleware_and_routes(auth) @@ -340,38 +324,19 @@ def create_streamable_http_app( server_routes.extend(auth_routes) server_middleware.extend(auth_middleware) - # Auth is enabled, wrap app with RequireAuthMiddleware - wrapped_app = RequireAuthMiddleware(normalized_handler, required_scopes) - - # Add routes for both with and without trailing slash + # Auth is enabled, wrap endpoint with RequireAuthMiddleware server_routes.append( - Route( - path_pattern, - endpoint=wrapped_app, - methods=["GET", "POST"] - ) - ) - server_routes.append( - Route( - path_pattern + "/", - endpoint=wrapped_app, - methods=["GET", "POST"] + Mount( + streamable_http_path, + app=RequireAuthMiddleware(handle_streamable_http, required_scopes), ) ) else: - # No auth required - add routes for both with and without trailing slash + # No auth required server_routes.append( - Route( - path_pattern, - endpoint=normalized_handler, - methods=["GET", "POST"] - ) - ) - server_routes.append( - Route( - path_pattern + "/", - endpoint=normalized_handler, - methods=["GET", "POST"] + Mount( + streamable_http_path, + app=handle_streamable_http, ) ) @@ -400,6 +365,6 @@ def create_streamable_http_app( # Store the FastMCP server instance on the Starlette app state app.state.fastmcp_server = server - app.state.path = streamable_http_path.rstrip("/") + app.state.path = streamable_http_path return app diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index 96fd657b3..cd082166b 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -192,9 +192,9 @@ class Settings(BaseSettings): # HTTP settings host: str = "127.0.0.1" port: int = 8000 - sse_path: str = "/sse" + sse_path: str = "/sse/" message_path: str = "/messages/" - streamable_http_path: str = "/mcp" + streamable_http_path: str = "/mcp/" debug: bool = False # error handling From ac95a3aaafafd34719bc01dd8f9e1efb8070d965 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:53:42 -0400 Subject: [PATCH 08/41] Update http.py --- src/fastmcp/server/http.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index a80b2235c..7041ba975 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -157,9 +157,8 @@ def create_sse_app( Returns: A Starlette application with RequestContextMiddleware """ - + # Ensure the message_path ends with a trailing slash to avoid automatic redirects - # when mounting the application. sse_path uses Route instead of Mount so no fix needed. if not message_path.endswith("/"): message_path = message_path + "/" @@ -311,11 +310,10 @@ def create_streamable_http_app( raise # Ensure the streamable_http_path ends with a trailing slash to avoid automatic redirects - # when mounting the application if not streamable_http_path.endswith("/"): streamable_http_path = streamable_http_path + "/" - # Add StreamableHTTP routes with or without auth + # Add StreamableHTTP routes with or without auth if auth: auth_middleware, auth_routes, required_scopes = ( setup_auth_middleware_and_routes(auth) From 18ae625fef44ee96adb73a365d7d16009327b0d4 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:06:48 -0400 Subject: [PATCH 09/41] Update docs and test --- docs/deployment/asgi.mdx | 8 ++++---- docs/deployment/running-server.mdx | 8 ++++---- docs/tutorials/rest-api.mdx | 2 +- src/fastmcp/client/auth/oauth.py | 2 +- src/fastmcp/utilities/mcp_config.py | 4 +++- tests/auth/providers/test_bearer.py | 6 +++--- tests/auth/test_oauth_client.py | 2 +- tests/client/test_client.py | 12 +++++++----- tests/client/test_openapi.py | 6 +++--- tests/client/test_sse.py | 6 +++--- tests/client/test_streamable_http.py | 8 ++++---- tests/deprecated/test_settings.py | 4 ++-- tests/server/http/test_custom_routes.py | 2 +- tests/server/http/test_http_dependencies.py | 4 ++-- tests/server/http/test_http_middleware.py | 2 +- tests/server/test_app_state.py | 4 ++-- tests/server/test_mount.py | 4 +++- tests/server/test_proxy.py | 4 ++-- tests/utilities/test_mcp_config.py | 8 ++++---- 19 files changed, 51 insertions(+), 45 deletions(-) diff --git a/docs/deployment/asgi.mdx b/docs/deployment/asgi.mdx index 56947cde9..e5e6b6c71 100644 --- a/docs/deployment/asgi.mdx +++ b/docs/deployment/asgi.mdx @@ -48,7 +48,7 @@ Both approaches return a Starlette application that can be integrated with other The returned app stores the `FastMCP` instance on `app.state.fastmcp_server`, so you can access it from custom middleware or routes via `request.app.state.fastmcp_server`. -The MCP server's endpoint is mounted at the root path `/mcp` for Streamable HTTP transport, and `/sse` for SSE transport, though you can change these paths by passing a `path` argument to the `http_app()` method: +The MCP server's endpoint is mounted at the root path `/mcp/` for Streamable HTTP transport, and `/sse/` for SSE transport, though you can change these paths by passing a `path` argument to the `http_app()` method: ```python # For Streamable HTTP transport @@ -137,7 +137,7 @@ app = Starlette( ) ``` -The MCP endpoint will be available at `/mcp-server/mcp` of the resulting Starlette app. +The MCP endpoint will be available at `/mcp-server/mcp/` of the resulting Starlette app. 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. @@ -167,7 +167,7 @@ app = Starlette( ) ``` -In this setup, the MCP server is accessible at the `/outer/inner/mcp` path of the resulting Starlette app. +In this setup, the MCP server is accessible at the `/outer/inner/mcp/` path of the resulting Starlette app. For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the *outer* Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized. @@ -194,7 +194,7 @@ app = FastAPI(lifespan=mcp_app.lifespan) app.mount("/mcp-server", mcp_app) ``` -The MCP endpoint will be available at `/mcp-server/mcp` of the resulting FastAPI app. +The MCP endpoint will be available at `/mcp-server/mcp/` of the resulting FastAPI app. For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting FastAPI app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized. diff --git a/docs/deployment/running-server.mdx b/docs/deployment/running-server.mdx index 5b6a7eadd..591cba32c 100644 --- a/docs/deployment/running-server.mdx +++ b/docs/deployment/running-server.mdx @@ -105,7 +105,7 @@ When using Stdio transport, you will typically *not* run the server yourself as Streamable HTTP is a modern, efficient transport for exposing your MCP server via HTTP. It is the recommended transport for web-based deployments. -To run a server using Streamable HTTP, you can use the `run()` method with the `transport` argument set to `"streamable-http"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and path (`/mcp`). +To run a server using Streamable HTTP, you can use the `run()` method with the `transport` argument set to `"streamable-http"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and path (`/mcp/`). ```python {6} server.py from fastmcp import FastMCP @@ -120,7 +120,7 @@ import asyncio from fastmcp import Client async def example(): - async with Client("http://127.0.0.1:8000/mcp") as client: + async with Client("http://127.0.0.1:8000/mcp/") as client: await client.ping() if __name__ == "__main__": @@ -168,7 +168,7 @@ New applications should use Streamable HTTP transport instead. Server-Sent Events (SSE) is an HTTP-based protocol for server-to-client streaming. While FastMCP still supports SSE, it is deprecated and Streamable HTTP is preferred for new projects. -To run a server using SSE, you can use the `run()` method with the `transport` argument set to `"sse"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and with default SSE path (`/sse`) and message path (`/messages/`). +To run a server using SSE, you can use the `run()` method with the `transport` argument set to `"sse"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and with default SSE path (`/sse/`) and message path (`/messages/`). ```python {6} server.py @@ -186,7 +186,7 @@ from fastmcp.client.transports import SSETransport async def example(): async with Client( - transport=SSETransport("http://127.0.0.1:8000/sse") + transport=SSETransport("http://127.0.0.1:8000/sse/") ) as client: await client.ping() diff --git a/docs/tutorials/rest-api.mdx b/docs/tutorials/rest-api.mdx index d0a51e80a..1b6ae1288 100644 --- a/docs/tutorials/rest-api.mdx +++ b/docs/tutorials/rest-api.mdx @@ -103,7 +103,7 @@ from fastmcp import Client async def main(): # Connect to the MCP server we just created - async with Client("http://127.0.0.1:8000/mcp") as client: + async with Client("http://127.0.0.1:8000/mcp/") as client: # List the tools that were automatically generated tools = await client.list_tools() diff --git a/src/fastmcp/client/auth/oauth.py b/src/fastmcp/client/auth/oauth.py index ca92a5cc3..ee0e29d77 100644 --- a/src/fastmcp/client/auth/oauth.py +++ b/src/fastmcp/client/auth/oauth.py @@ -307,7 +307,7 @@ def OAuth( Args: mcp_url: Full URL to the MCP endpoint (e.g., - "http://host/mcp/sse") + "http://host/mcp/sse/") scopes: OAuth scopes to request. Can be a space-separated string or a list of strings. client_name: Name for this client during registration diff --git a/src/fastmcp/utilities/mcp_config.py b/src/fastmcp/utilities/mcp_config.py index 176cb097d..e50c97be2 100644 --- a/src/fastmcp/utilities/mcp_config.py +++ b/src/fastmcp/utilities/mcp_config.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from typing import TYPE_CHECKING, Annotated, Any, Literal from urllib.parse import urlparse @@ -28,7 +29,8 @@ def infer_transport_type_from_url( parsed_url = urlparse(url) path = parsed_url.path - if "/sse/" in path or path.rstrip("/").endswith("/sse"): + # Match /sse followed by /, ?, &, or end of string + if re.search(r"/sse(/|\?|&|$)", path): return "sse" else: return "streamable-http" diff --git a/tests/auth/providers/test_bearer.py b/tests/auth/providers/test_bearer.py index efed070d4..ac7e529b1 100644 --- a/tests/auth/providers/test_bearer.py +++ b/tests/auth/providers/test_bearer.py @@ -67,7 +67,7 @@ def mcp_server_url(rsa_key_pair: RSAKeyPair) -> Generator[str]: public_key=rsa_key_pair.public_key, run_kwargs=dict(transport="streamable-http"), ) as url: - yield f"{url}/mcp" + yield f"{url}/mcp/" class TestRSAKeyPair: @@ -698,7 +698,7 @@ class TestFastMCPBearerAuth: auth_kwargs=dict(required_scopes=["read", "write"]), run_kwargs=dict(transport="streamable-http"), ) as url: - mcp_server_url = f"{url}/mcp" + mcp_server_url = f"{url}/mcp/" with pytest.raises(httpx.HTTPStatusError) as exc_info: async with Client(mcp_server_url, auth=BearerAuth(token)) as client: tools = await client.list_tools() # noqa: F841 @@ -721,7 +721,7 @@ class TestFastMCPBearerAuth: auth_kwargs=dict(required_scopes=["read", "write"]), run_kwargs=dict(transport="streamable-http"), ) as url: - mcp_server_url = f"{url}/mcp" + mcp_server_url = f"{url}/mcp/" async with Client(mcp_server_url, auth=BearerAuth(token)) as client: tools = await client.list_tools() assert tools diff --git a/tests/auth/test_oauth_client.py b/tests/auth/test_oauth_client.py index 292f6c4af..f36cf4c91 100644 --- a/tests/auth/test_oauth_client.py +++ b/tests/auth/test_oauth_client.py @@ -44,7 +44,7 @@ def run_server(host: str, port: int, **kwargs) -> None: @pytest.fixture(scope="module") def streamable_http_server() -> Generator[str, None, None]: with run_server_in_process(run_server, transport="streamable-http") as url: - yield f"{url}/mcp" + yield f"{url}/mcp/" @pytest.fixture() diff --git a/tests/client/test_client.py b/tests/client/test_client.py index d975ed77b..f792a15e0 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -735,7 +735,8 @@ class TestInferTransport: "http://example.com/api/sse/stream", "https://localhost:8080/mcp/sse/endpoint", "http://example.com/api/sse", - "https://localhost:8080/mcp/sse", + "http://example.com/api/sse/", + "https://localhost:8080/mcp/sse/", "http://example.com/api/sse?param=value", "https://localhost:8080/mcp/sse/?param=value", "https://localhost:8000/mcp/sse?x=1&y=2", @@ -744,6 +745,7 @@ class TestInferTransport: "path_with_sse_directory", "path_with_sse_subdirectory", "path_ending_with_sse", + "path_ending_with_sse_slash", "path_ending_with_sse_https", "path_with_sse_and_query_params", "path_with_sse_slash_and_query_params", @@ -758,7 +760,7 @@ class TestInferTransport: "url", [ "http://example.com/api", - "https://localhost:8080/mcp", + "https://localhost:8080/mcp/", "http://example.com/asset/image.jpg", "https://localhost:8080/sservice/endpoint", "https://example.com/assets/file", @@ -779,7 +781,7 @@ class TestInferTransport: config = { "mcpServers": { "test_server": { - "url": "http://localhost:8000/sse", + "url": "http://localhost:8000/sse/", "headers": {"Authorization": "Bearer 123"}, }, } @@ -787,7 +789,7 @@ class TestInferTransport: transport = infer_transport(config) assert isinstance(transport, MCPConfigTransport) assert isinstance(transport.transport, SSETransport) - assert transport.transport.url == "http://localhost:8000/sse" + assert transport.transport.url == "http://localhost:8000/sse/" assert transport.transport.headers == {"Authorization": "Bearer 123"} def test_infer_local_transport_from_config(self): @@ -825,7 +827,7 @@ class TestInferTransport: "args": ["hello"], }, "remote": { - "url": "http://localhost:8000/sse", + "url": "http://localhost:8000/sse/", "headers": {"Authorization": "Bearer 123"}, }, } diff --git a/tests/client/test_openapi.py b/tests/client/test_openapi.py index d97f89eb9..2ee4727a9 100644 --- a/tests/client/test_openapi.py +++ b/tests/client/test_openapi.py @@ -57,12 +57,12 @@ class TestClientHeaders: @pytest.fixture(scope="class") def shttp_server(self) -> Generator[str, None, None]: with run_server_in_process(run_server, transport="streamable-http") as url: - yield f"{url}/mcp" + yield f"{url}/mcp/" @pytest.fixture(scope="class") def sse_server(self) -> Generator[str, None, None]: with run_server_in_process(run_server, transport="sse") as url: - yield f"{url}/sse" + yield f"{url}/sse/" @pytest.fixture(scope="class") def proxy_server(self, shttp_server: str) -> Generator[str, None, None]: @@ -71,7 +71,7 @@ class TestClientHeaders: shttp_url=shttp_server, transport="streamable-http", ) as url: - yield f"{url}/mcp" + yield f"{url}/mcp/" async def test_client_headers_sse_resource(self, sse_server: str): async with Client( diff --git a/tests/client/test_sse.py b/tests/client/test_sse.py index df24d1d8e..f428c5c30 100644 --- a/tests/client/test_sse.py +++ b/tests/client/test_sse.py @@ -70,7 +70,7 @@ def run_server(host: str, port: int, **kwargs) -> None: @pytest.fixture(autouse=True, scope="module") def sse_server() -> Generator[str, None, None]: with run_server_in_process(run_server, transport="sse") as url: - yield f"{url}/sse" + yield f"{url}/sse/" async def test_ping(sse_server: str): @@ -92,7 +92,7 @@ async def test_http_headers(sse_server: str): def run_nested_server(host: str, port: int) -> None: - app = fastmcp_server().sse_app(path="/mcp/sse", message_path="/mcp/messages") + app = fastmcp_server().sse_app(path="/mcp/sse/", message_path="/mcp/messages") mount = Starlette(routes=[Mount("/nest-inner", app=app)]) mount2 = Starlette(routes=[Mount("/nest-outer", app=mount)]) server = uvicorn.Server( @@ -114,7 +114,7 @@ async def test_nested_sse_server_resolves_correctly(): with run_server_in_process(run_nested_server) as url: async with Client( - transport=SSETransport(f"{url}/nest-outer/nest-inner/mcp/sse") + transport=SSETransport(f"{url}/nest-outer/nest-inner/mcp/sse/") ) as client: result = await client.ping() assert result is True diff --git a/tests/client/test_streamable_http.py b/tests/client/test_streamable_http.py index 7e95e27b2..5b182c933 100644 --- a/tests/client/test_streamable_http.py +++ b/tests/client/test_streamable_http.py @@ -79,7 +79,7 @@ def run_server(host: str, port: int, stateless_http: bool = False, **kwargs) -> def run_nested_server(host: str, port: int) -> None: - mcp_app = fastmcp_server().http_app(path="/final/mcp") + mcp_app = fastmcp_server().http_app(path="/final/mcp/") mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)]) mount2 = Starlette( @@ -105,9 +105,9 @@ async def streamable_http_server( with run_server_in_process( run_server, stateless_http=stateless_http, transport="streamable-http" ) as url: - async with Client(transport=StreamableHttpTransport(f"{url}/mcp")) as client: + async with Client(transport=StreamableHttpTransport(f"{url}/mcp/")) as client: assert await client.ping() - yield f"{url}/mcp" + yield f"{url}/mcp/" async def test_ping(streamable_http_server: str): @@ -156,7 +156,7 @@ async def test_nested_streamable_http_server_resolves_correctly(): with run_server_in_process(run_nested_server) as url: async with Client( - transport=StreamableHttpTransport(f"{url}/nest-outer/nest-inner/final/mcp") + transport=StreamableHttpTransport(f"{url}/nest-outer/nest-inner/final/mcp/") ) as client: result = await client.ping() assert result is True diff --git a/tests/deprecated/test_settings.py b/tests/deprecated/test_settings.py index 6c8fc9862..e2bc66708 100644 --- a/tests/deprecated/test_settings.py +++ b/tests/deprecated/test_settings.py @@ -123,7 +123,7 @@ class TestDeprecatedServerInitKwargs: debug=False, host="127.0.0.1", port=9999, - sse_path="/sse", + sse_path="/sse/", message_path="/msg", streamable_http_path="/http", json_response=False, @@ -162,7 +162,7 @@ class TestDeprecatedServerInitKwargs: assert server._deprecated_settings.debug is False assert server._deprecated_settings.host == "127.0.0.1" assert server._deprecated_settings.port == 9999 - assert server._deprecated_settings.sse_path == "/sse" + assert server._deprecated_settings.sse_path == "/sse/" assert server._deprecated_settings.message_path == "/msg" assert server._deprecated_settings.streamable_http_path == "/http" assert server._deprecated_settings.json_response is False diff --git a/tests/server/http/test_custom_routes.py b/tests/server/http/test_custom_routes.py index 5c988d1d4..c43444756 100644 --- a/tests/server/http/test_custom_routes.py +++ b/tests/server/http/test_custom_routes.py @@ -55,7 +55,7 @@ class TestCustomRoutes: """Test that custom routes are included when using create_sse_app directly.""" # Create the app by calling the constructor function directly app = create_sse_app( - server=server_with_custom_route, message_path="/message", sse_path="/sse" + server=server_with_custom_route, message_path="/message", sse_path="/sse/" ) # Verify that the custom route is included diff --git a/tests/server/http/test_http_dependencies.py b/tests/server/http/test_http_dependencies.py index 81a52a90c..514f0a9d3 100644 --- a/tests/server/http/test_http_dependencies.py +++ b/tests/server/http/test_http_dependencies.py @@ -45,13 +45,13 @@ def run_server(host: str, port: int, **kwargs) -> None: @pytest.fixture(autouse=True, scope="module") def shttp_server() -> Generator[str, None, None]: with run_server_in_process(run_server, transport="streamable-http") as url: - yield f"{url}/mcp" + yield f"{url}/mcp/" @pytest.fixture(autouse=True, scope="module") def sse_server() -> Generator[str, None, None]: with run_server_in_process(run_server, transport="sse") as url: - yield f"{url}/sse" + yield f"{url}/sse/" async def test_http_headers_resource_shttp(shttp_server: str): diff --git a/tests/server/http/test_http_middleware.py b/tests/server/http/test_http_middleware.py index 7a1ab22f4..0c36d0522 100644 --- a/tests/server/http/test_http_middleware.py +++ b/tests/server/http/test_http_middleware.py @@ -126,7 +126,7 @@ async def test_create_sse_app_with_custom_middleware(): app = create_sse_app( server=server, message_path="/message", - sse_path="/sse", + sse_path="/sse/", middleware=custom_middleware, routes=additional_routes, ) diff --git a/tests/server/test_app_state.py b/tests/server/test_app_state.py index 609089400..eeccd6fee 100644 --- a/tests/server/test_app_state.py +++ b/tests/server/test_app_state.py @@ -16,11 +16,11 @@ def test_http_app_sse_sets_mcp_server_state(): def test_create_streamable_http_app_sets_state(): server = FastMCP(name="StateTest") - app = create_streamable_http_app(server, "/mcp") + app = create_streamable_http_app(server, "/mcp/") assert app.state.fastmcp_server is server def test_create_sse_app_sets_state(): server = FastMCP(name="StateTest") - app = create_sse_app(server, message_path="/message", sse_path="/sse") + app = create_sse_app(server, message_path="/message", sse_path="/sse/") assert app.state.fastmcp_server is server diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py index 46658395b..30304531a 100644 --- a/tests/server/test_mount.py +++ b/tests/server/test_mount.py @@ -273,7 +273,9 @@ class TestMultipleServerMount: main_app.mount(working_app, "working") # Use an unreachable port - unreachable_client = Client(transport=SSETransport("http://127.0.0.1:9999/sse")) + unreachable_client = Client( + transport=SSETransport("http://127.0.0.1:9999/sse/") + ) # Create a proxy server that will fail to connect unreachable_proxy = FastMCP.as_proxy(unreachable_client) diff --git a/tests/server/test_proxy.py b/tests/server/test_proxy.py index 68524ad2f..612f8bfc8 100644 --- a/tests/server/test_proxy.py +++ b/tests/server/test_proxy.py @@ -102,10 +102,10 @@ async def test_as_proxy_with_transport(fastmcp_server): def test_as_proxy_with_url(): """FastMCP.as_proxy should accept a URL without connecting.""" - proxy = FastMCP.as_proxy("http://example.com/mcp") + proxy = FastMCP.as_proxy("http://example.com/mcp/") assert isinstance(proxy, FastMCPProxy) assert isinstance(proxy.client.transport, StreamableHttpTransport) - assert proxy.client.transport.url == "http://example.com/mcp" + assert proxy.client.transport.url == "http://example.com/mcp/" class TestTools: diff --git a/tests/utilities/test_mcp_config.py b/tests/utilities/test_mcp_config.py index f5dee613d..ec6833b0a 100644 --- a/tests/utilities/test_mcp_config.py +++ b/tests/utilities/test_mcp_config.py @@ -61,21 +61,21 @@ def test_parse_remote_config_with_url_inference(): config = { "mcpServers": { "test_server": { - "url": "http://localhost:8000/sse", + "url": "http://localhost:8000/sse/", } } } mcp_config = MCPConfig.from_dict(config) transport = mcp_config.mcpServers["test_server"].to_transport() assert isinstance(transport, SSETransport) - assert transport.url == "http://localhost:8000/sse" + assert transport.url == "http://localhost:8000/sse/" def test_parse_multiple_servers(): config = { "mcpServers": { "test_server": { - "url": "http://localhost:8000/sse", + "url": "http://localhost:8000/sse/", }, "test_server_2": { "command": "echo", @@ -172,7 +172,7 @@ async def test_remote_config_sse_with_auth_token(): config = { "mcpServers": { "test_server": { - "url": "http://localhost:8000/sse", + "url": "http://localhost:8000/sse/", "auth": "test_token", } } From fbe2fc353338e2c8e852b1c064e884c2163352f4 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:12:23 -0400 Subject: [PATCH 10/41] Add transport handling for trailing slashes --- src/fastmcp/client/transports.py | 15 +++++++++++++++ tests/utilities/test_mcp_config.py | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index 778c58447..153491029 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -9,6 +9,7 @@ import warnings from collections.abc import AsyncIterator, Callable from pathlib import Path from typing import Any, Literal, TypedDict, TypeVar, cast, overload +from urllib.parse import urlparse, urlunparse import anyio import httpx @@ -159,6 +160,13 @@ class SSETransport(ClientTransport): url = str(url) if not isinstance(url, str) or not url.startswith("http"): raise ValueError("Invalid HTTP/S URL provided for SSE.") + + # Ensure the URL path ends with a trailing slash to avoid automatic redirects + parsed = urlparse(url) + if not parsed.path.endswith("/"): + parsed = parsed._replace(path=parsed.path + "/") + url = urlunparse(parsed) + self.url = url self.headers = headers or {} self._set_auth(auth) @@ -227,6 +235,13 @@ class StreamableHttpTransport(ClientTransport): url = str(url) if not isinstance(url, str) or not url.startswith("http"): raise ValueError("Invalid HTTP/S URL provided for Streamable HTTP.") + + # Ensure the URL path ends with a trailing slash to avoid automatic redirects + parsed = urlparse(url) + if not parsed.path.endswith("/"): + parsed = parsed._replace(path=parsed.path + "/") + url = urlunparse(parsed) + self.url = url self.headers = headers or {} self._set_auth(auth) diff --git a/tests/utilities/test_mcp_config.py b/tests/utilities/test_mcp_config.py index ec6833b0a..7775e12bc 100644 --- a/tests/utilities/test_mcp_config.py +++ b/tests/utilities/test_mcp_config.py @@ -39,7 +39,7 @@ def test_parse_single_remote_config(): mcp_config = MCPConfig.from_dict(config) transport = mcp_config.mcpServers["test_server"].to_transport() assert isinstance(transport, StreamableHttpTransport) - assert transport.url == "http://localhost:8000" + assert transport.url == "http://localhost:8000/" def test_parse_remote_config_with_transport(): @@ -54,7 +54,7 @@ def test_parse_remote_config_with_transport(): mcp_config = MCPConfig.from_dict(config) transport = mcp_config.mcpServers["test_server"].to_transport() assert isinstance(transport, SSETransport) - assert transport.url == "http://localhost:8000" + assert transport.url == "http://localhost:8000/" def test_parse_remote_config_with_url_inference(): From 5afe5b793e09a0384be00420235ab1bd1a6d236a Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Fri, 20 Jun 2025 12:59:11 -0500 Subject: [PATCH 11/41] update path and mdxify --- docs/docs.json | 441 ++++++++------ docs/python-sdk/fastmcp-cli-__init__.mdx | 9 + docs/python-sdk/fastmcp-cli-claude.mdx | 43 ++ docs/python-sdk/fastmcp-cli-cli.mdx | 65 +++ docs/python-sdk/fastmcp-cli-run.mdx | 106 ++++ docs/python-sdk/fastmcp-client-__init__.mdx | 8 + .../fastmcp-client-auth-__init__.mdx | 8 + .../python-sdk/fastmcp-client-auth-bearer.mdx | 18 + docs/python-sdk/fastmcp-client-auth-oauth.mdx | 103 ++++ docs/python-sdk/fastmcp-client-client.mdx | 94 +++ docs/python-sdk/fastmcp-client-logging.mdx | 14 + .../fastmcp-client-oauth_callback.mdx | 63 ++ docs/python-sdk/fastmcp-client-progress.mdx | 8 + docs/python-sdk/fastmcp-client-roots.mdx | 20 + docs/python-sdk/fastmcp-client-sampling.mdx | 14 + docs/python-sdk/fastmcp-client-transports.mdx | 191 ++++++ docs/python-sdk/fastmcp-exceptions.mdx | 65 +++ docs/python-sdk/fastmcp-prompts-__init__.mdx | 8 + docs/python-sdk/fastmcp-prompts-prompt.mdx | 84 +++ .../fastmcp-prompts-prompt_manager.mdx | 43 ++ .../python-sdk/fastmcp-resources-__init__.mdx | 8 + .../python-sdk/fastmcp-resources-resource.mdx | 90 +++ .../fastmcp-resources-resource_manager.mdx | 111 ++++ .../python-sdk/fastmcp-resources-template.mdx | 104 ++++ docs/python-sdk/fastmcp-resources-types.mdx | 83 +++ docs/python-sdk/fastmcp-server-__init__.mdx | 8 + .../fastmcp-server-auth-__init__.mdx | 8 + docs/python-sdk/fastmcp-server-auth-auth.mdx | 10 + ...fastmcp-server-auth-providers-__init__.mdx | 8 + .../fastmcp-server-auth-providers-bearer.mdx | 69 +++ ...stmcp-server-auth-providers-bearer_env.mdx | 22 + ...astmcp-server-auth-providers-in_memory.mdx | 15 + docs/python-sdk/fastmcp-server-context.mdx | 118 ++++ .../fastmcp-server-dependencies.mdx | 36 ++ docs/python-sdk/fastmcp-server-http.mdx | 113 ++++ docs/python-sdk/fastmcp-server-middleware.mdx | 56 ++ docs/python-sdk/fastmcp-server-openapi.mdx | 58 ++ docs/python-sdk/fastmcp-server-proxy.mdx | 101 ++++ docs/python-sdk/fastmcp-server-server.mdx | 542 ++++++++++++++++++ docs/python-sdk/fastmcp-settings.mdx | 59 ++ docs/python-sdk/fastmcp-tools-__init__.mdx | 8 + docs/python-sdk/fastmcp-tools-tool.mdx | 68 +++ .../python-sdk/fastmcp-tools-tool_manager.mdx | 58 ++ .../fastmcp-tools-tool_transform.mdx | 117 ++++ .../python-sdk/fastmcp-utilities-__init__.mdx | 9 + docs/python-sdk/fastmcp-utilities-cache.mdx | 30 + .../fastmcp-utilities-components.mdx | 52 ++ .../fastmcp-utilities-exceptions.mdx | 20 + docs/python-sdk/fastmcp-utilities-http.mdx | 18 + .../fastmcp-utilities-json_schema.mdx | 25 + docs/python-sdk/fastmcp-utilities-logging.mdx | 41 ++ .../fastmcp-utilities-mcp_config.mdx | 50 ++ docs/python-sdk/fastmcp-utilities-openapi.mdx | 118 ++++ docs/python-sdk/fastmcp-utilities-types.mdx | 112 ++++ justfile | 17 +- 55 files changed, 3596 insertions(+), 171 deletions(-) create mode 100644 docs/python-sdk/fastmcp-cli-__init__.mdx create mode 100644 docs/python-sdk/fastmcp-cli-claude.mdx create mode 100644 docs/python-sdk/fastmcp-cli-cli.mdx create mode 100644 docs/python-sdk/fastmcp-cli-run.mdx create mode 100644 docs/python-sdk/fastmcp-client-__init__.mdx create mode 100644 docs/python-sdk/fastmcp-client-auth-__init__.mdx create mode 100644 docs/python-sdk/fastmcp-client-auth-bearer.mdx create mode 100644 docs/python-sdk/fastmcp-client-auth-oauth.mdx create mode 100644 docs/python-sdk/fastmcp-client-client.mdx create mode 100644 docs/python-sdk/fastmcp-client-logging.mdx create mode 100644 docs/python-sdk/fastmcp-client-oauth_callback.mdx create mode 100644 docs/python-sdk/fastmcp-client-progress.mdx create mode 100644 docs/python-sdk/fastmcp-client-roots.mdx create mode 100644 docs/python-sdk/fastmcp-client-sampling.mdx create mode 100644 docs/python-sdk/fastmcp-client-transports.mdx create mode 100644 docs/python-sdk/fastmcp-exceptions.mdx create mode 100644 docs/python-sdk/fastmcp-prompts-__init__.mdx create mode 100644 docs/python-sdk/fastmcp-prompts-prompt.mdx create mode 100644 docs/python-sdk/fastmcp-prompts-prompt_manager.mdx create mode 100644 docs/python-sdk/fastmcp-resources-__init__.mdx create mode 100644 docs/python-sdk/fastmcp-resources-resource.mdx create mode 100644 docs/python-sdk/fastmcp-resources-resource_manager.mdx create mode 100644 docs/python-sdk/fastmcp-resources-template.mdx create mode 100644 docs/python-sdk/fastmcp-resources-types.mdx create mode 100644 docs/python-sdk/fastmcp-server-__init__.mdx create mode 100644 docs/python-sdk/fastmcp-server-auth-__init__.mdx create mode 100644 docs/python-sdk/fastmcp-server-auth-auth.mdx create mode 100644 docs/python-sdk/fastmcp-server-auth-providers-__init__.mdx create mode 100644 docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx create mode 100644 docs/python-sdk/fastmcp-server-auth-providers-bearer_env.mdx create mode 100644 docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx create mode 100644 docs/python-sdk/fastmcp-server-context.mdx create mode 100644 docs/python-sdk/fastmcp-server-dependencies.mdx create mode 100644 docs/python-sdk/fastmcp-server-http.mdx create mode 100644 docs/python-sdk/fastmcp-server-middleware.mdx create mode 100644 docs/python-sdk/fastmcp-server-openapi.mdx create mode 100644 docs/python-sdk/fastmcp-server-proxy.mdx create mode 100644 docs/python-sdk/fastmcp-server-server.mdx create mode 100644 docs/python-sdk/fastmcp-settings.mdx create mode 100644 docs/python-sdk/fastmcp-tools-__init__.mdx create mode 100644 docs/python-sdk/fastmcp-tools-tool.mdx create mode 100644 docs/python-sdk/fastmcp-tools-tool_manager.mdx create mode 100644 docs/python-sdk/fastmcp-tools-tool_transform.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-__init__.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-cache.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-components.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-exceptions.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-http.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-json_schema.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-logging.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-mcp_config.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-openapi.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-types.mdx diff --git a/docs/docs.json b/docs/docs.json index 0c0b83e53..0c0677a67 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1,178 +1,279 @@ { - "$schema": "https://mintlify.com/docs.json", - "appearance": { - "default": "system", - "strict": false + "$schema": "https://mintlify.com/docs.json", + "appearance": { + "default": "system", + "strict": false + }, + "background": { + "color": { + "dark": "#222831", + "light": "#EEEEEE" }, - "background": { - "color": { - "dark": "#222831", - "light": "#EEEEEE" - }, - "decoration": "windows" - }, - "banner": { - "content": "[FastMCP Cloud](https://fastmcp.link/x0Kyhy2) is coming!" - }, - "colors": { - "dark": "#f72585", - "light": "#4cc9f0", - "primary": "#2d00f7" - }, - "description": "The fast, Pythonic way to build MCP servers and clients.", - "favicon": { - "dark": "/assets/favicon.ico", - "light": "/assets/favicon.ico" - }, - "footer": { - "socials": { - "bluesky": "https://bsky.app/profile/jlowin.dev", - "github": "https://github.com/jlowin/fastmcp", - "x": "https://x.com/jlowin" - } - }, - "integrations": { - "ga4": { - "measurementId": "G-64R5W1TJXG" - } - }, - "name": "FastMCP", - "navbar": { - "primary": { - "href": "https://github.com/jlowin/fastmcp", - "type": "github" - } - }, - "navigation": { - "anchors": [ - { - "anchor": "Documentation", - "groups": [ - { - "group": "Get Started", - "pages": [ - "getting-started/welcome", - "getting-started/installation", - "getting-started/quickstart", - "updates" - ] - }, - { - "group": "Servers", - "pages": [ - "servers/fastmcp", - { - "group": "Core Components", - "icon": "toolbox", - "pages": [ - "servers/tools", - "servers/resources", - "servers/prompts", - "servers/context" - ] - }, - { - "group": "Authentication", - "icon": "shield-check", - "pages": [ - "servers/auth/bearer" - ] - }, - "servers/middleware", - "servers/openapi", - "servers/proxy", - "servers/composition", - { - "group": "Deployment", - "icon": "upload", - "pages": [ - "deployment/running-server", - "deployment/asgi" - ] - } - ] - }, - { - "group": "Clients", - "pages": [ - "clients/client", - "clients/transports", - { - "group": "Authentication", - "icon": "user-shield", - "pages": [ - "clients/auth/oauth", - "clients/auth/bearer" - ] - }, - "clients/advanced-features" - ] - }, - { - "group": "Integrations", - "pages": [ - "integrations/anthropic", - "integrations/claude-desktop", - "integrations/openai", - "integrations/gemini", - "integrations/contrib" - ] - }, - { - "group": "Patterns", - "pages": [ - "patterns/tool-transformation", - "patterns/decorating-methods", - "patterns/http-requests", - "patterns/testing", - "patterns/cli" - ] - } - ], - "icon": "book" - }, - { - "anchor": "Tutorials", - "groups": [ - { - "group": "MCP", - "pages": [ - "tutorials/mcp", - "tutorials/create-mcp-server", - "tutorials/rest-api" - ] - } - ], - "icon": "graduation-cap" - }, - { - "anchor": "Changelog", - "icon": "list-check", + "decoration": "windows" + }, + "banner": { + "content": "[FastMCP Cloud](https://fastmcp.link/x0Kyhy2) is coming!" + }, + "colors": { + "dark": "#f72585", + "light": "#4cc9f0", + "primary": "#2d00f7" + }, + "description": "The fast, Pythonic way to build MCP servers and clients.", + "favicon": { + "dark": "/assets/favicon.ico", + "light": "/assets/favicon.ico" + }, + "footer": { + "socials": { + "bluesky": "https://bsky.app/profile/jlowin.dev", + "github": "https://github.com/jlowin/fastmcp", + "x": "https://x.com/jlowin" + } + }, + "integrations": { + "ga4": { + "measurementId": "G-64R5W1TJXG" + } + }, + "name": "FastMCP", + "navbar": { + "primary": { + "href": "https://github.com/jlowin/fastmcp", + "type": "github" + } + }, + "navigation": { + "anchors": [ + { + "anchor": "Documentation", + "groups": [ + { + "group": "Get Started", + "pages": [ + "getting-started/welcome", + "getting-started/installation", + "getting-started/quickstart", + "updates" + ] + }, + { + "group": "Servers", + "pages": [ + "servers/fastmcp", + { + "group": "Core Components", + "icon": "toolbox", "pages": [ - "changelog" + "servers/tools", + "servers/resources", + "servers/prompts", + "servers/context" ] - }, - { - "anchor": "Community", - "icon": "users", + }, + { + "group": "Authentication", + "icon": "shield-check", "pages": [ - "community/showcase" + "servers/auth/bearer" ] - } + }, + "servers/middleware", + "servers/openapi", + "servers/proxy", + "servers/composition", + { + "group": "Deployment", + "icon": "upload", + "pages": [ + "deployment/running-server", + "deployment/asgi" + ] + } + ] + }, + { + "group": "Clients", + "pages": [ + "clients/client", + "clients/transports", + { + "group": "Authentication", + "icon": "user-shield", + "pages": [ + "clients/auth/oauth", + "clients/auth/bearer" + ] + }, + "clients/advanced-features" + ] + }, + { + "group": "Integrations", + "pages": [ + "integrations/anthropic", + "integrations/claude-desktop", + "integrations/openai", + "integrations/gemini", + "integrations/contrib" + ] + }, + { + "group": "Patterns", + "pages": [ + "patterns/tool-transformation", + "patterns/decorating-methods", + "patterns/http-requests", + "patterns/testing", + "patterns/cli" + ] + } + ], + "icon": "book" + }, + { + "anchor": "Tutorials", + "groups": [ + { + "group": "MCP", + "pages": [ + "tutorials/mcp", + "tutorials/create-mcp-server", + "tutorials/rest-api" + ] + } + ], + "icon": "graduation-cap" + }, + { + "anchor": "Changelog", + "icon": "list-check", + "pages": [ + "changelog" ] + }, + { + "anchor": "Community", + "icon": "users", + "pages": [ + "community/showcase" + ] + }, + { + "anchor": "SDK Reference", + "icon": "code", + "pages": [ + "python-sdk/fastmcp-exceptions", + "python-sdk/fastmcp-settings", + { + "group": "fastmcp.cli", + "pages": [ + "python-sdk/fastmcp-cli-__init__", + "python-sdk/fastmcp-cli-claude", + "python-sdk/fastmcp-cli-cli", + "python-sdk/fastmcp-cli-run" + ] + }, + { + "group": "fastmcp.client", + "pages": [ + { + "group": "auth", + "pages": [ + "python-sdk/fastmcp-client-auth-bearer", + "python-sdk/fastmcp-client-auth-oauth" + ] + }, + "python-sdk/fastmcp-client-client", + "python-sdk/fastmcp-client-logging", + "python-sdk/fastmcp-client-oauth_callback", + "python-sdk/fastmcp-client-progress", + "python-sdk/fastmcp-client-roots", + "python-sdk/fastmcp-client-sampling", + "python-sdk/fastmcp-client-transports" + ] + }, + { + "group": "fastmcp.prompts", + "pages": [ + "python-sdk/fastmcp-prompts-prompt", + "python-sdk/fastmcp-prompts-prompt_manager" + ] + }, + { + "group": "fastmcp.resources", + "pages": [ + "python-sdk/fastmcp-resources-resource", + "python-sdk/fastmcp-resources-resource_manager", + "python-sdk/fastmcp-resources-template", + "python-sdk/fastmcp-resources-types" + ] + }, + { + "group": "fastmcp.server", + "pages": [ + { + "group": "auth", + "pages": [ + "python-sdk/fastmcp-server-auth-auth", + { + "group": "providers", + "pages": [ + "python-sdk/fastmcp-server-auth-providers-bearer", + "python-sdk/fastmcp-server-auth-providers-bearer_env", + "python-sdk/fastmcp-server-auth-providers-in_memory" + ] + } + ] + }, + "python-sdk/fastmcp-server-context", + "python-sdk/fastmcp-server-dependencies", + "python-sdk/fastmcp-server-http", + "python-sdk/fastmcp-server-middleware", + "python-sdk/fastmcp-server-openapi", + "python-sdk/fastmcp-server-proxy", + "python-sdk/fastmcp-server-server" + ] + }, + { + "group": "fastmcp.tools", + "pages": [ + "python-sdk/fastmcp-tools-tool", + "python-sdk/fastmcp-tools-tool_manager", + "python-sdk/fastmcp-tools-tool_transform" + ] + }, + { + "group": "fastmcp.utilities", + "pages": [ + "python-sdk/fastmcp-utilities-__init__", + "python-sdk/fastmcp-utilities-cache", + "python-sdk/fastmcp-utilities-components", + "python-sdk/fastmcp-utilities-exceptions", + "python-sdk/fastmcp-utilities-http", + "python-sdk/fastmcp-utilities-json_schema", + "python-sdk/fastmcp-utilities-logging", + "python-sdk/fastmcp-utilities-mcp_config", + "python-sdk/fastmcp-utilities-openapi", + "python-sdk/fastmcp-utilities-types" + ] + } + ] + } + ] + }, + "redirects": [ + { + "destination": "/servers/proxy", + "source": "/patterns/proxy" }, - "redirects": [ - { - "destination": "/servers/proxy", - "source": "/patterns/proxy" - }, - { - "destination": "/servers/composition", - "source": "/patterns/composition" - } - ], - "search": { - "prompt": "Search the docs..." - }, - "theme": "mint" -} \ No newline at end of file + { + "destination": "/servers/composition", + "source": "/patterns/composition" + } + ], + "search": { + "prompt": "Search the docs..." + }, + "theme": "mint" +} diff --git a/docs/python-sdk/fastmcp-cli-__init__.mdx b/docs/python-sdk/fastmcp-cli-__init__.mdx new file mode 100644 index 000000000..d2873740a --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-__init__.mdx @@ -0,0 +1,9 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.cli` + + +FastMCP CLI package. diff --git a/docs/python-sdk/fastmcp-cli-claude.mdx b/docs/python-sdk/fastmcp-cli-claude.mdx new file mode 100644 index 000000000..13fe7d600 --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-claude.mdx @@ -0,0 +1,43 @@ +--- +title: claude +sidebarTitle: claude +--- + +# `fastmcp.cli.claude` + + +Claude app integration utilities. + +## Functions + +### `get_claude_config_path` + +```python +get_claude_config_path() -> Path | None +``` + + +Get the Claude config directory based on platform. + + +### `update_claude_config` + +```python +update_claude_config(file_spec: str, server_name: str) -> bool +``` + + +Add or update a FastMCP server in Claude's configuration. + +**Args:** +- `file_spec`: Path to the server file, optionally with :object suffix +- `server_name`: Name for the server in Claude's config +- `with_editable`: Optional directory to install in editable mode +- `with_packages`: Optional list of additional packages to install +- `env_vars`: Optional dictionary of environment variables. These are merged with +any existing variables, with new values taking precedence. + +**Raises:** +- `RuntimeError`: If Claude Desktop's config directory is not found, indicating +Claude Desktop may not be installed or properly set up. + diff --git a/docs/python-sdk/fastmcp-cli-cli.mdx b/docs/python-sdk/fastmcp-cli-cli.mdx new file mode 100644 index 000000000..1ebb968b2 --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-cli.mdx @@ -0,0 +1,65 @@ +--- +title: cli +sidebarTitle: cli +--- + +# `fastmcp.cli.cli` + + +FastMCP CLI tools. + +## Functions + +### `version` + +```python +version(ctx: Context) +``` + +### `dev` + +```python +dev(server_spec: str = typer.Argument(..., help='Python file to run, optionally with :object suffix'), with_editable: Annotated[Path | None, typer.Option('--with-editable', '-e', help='Directory containing pyproject.toml to install in editable mode', exists=True, file_okay=False, resolve_path=True)] = None, with_packages: Annotated[list[str], typer.Option('--with', help='Additional packages to install')] = [], inspector_version: Annotated[str | None, typer.Option('--inspector-version', help='Version of the MCP Inspector to use')] = None, ui_port: Annotated[int | None, typer.Option('--ui-port', help='Port for the MCP Inspector UI')] = None, server_port: Annotated[int | None, typer.Option('--server-port', help='Port for the MCP Inspector Proxy server')] = None) -> None +``` + + +Run a MCP server with the MCP Inspector. + + +### `run` + +```python +run(ctx: typer.Context, server_spec: str = typer.Argument(..., help='Python file, object specification (file:obj), or URL'), transport: Annotated[str | None, typer.Option('--transport', '-t', help='Transport protocol to use (stdio, streamable-http, or sse)')] = None, host: Annotated[str | None, typer.Option('--host', help='Host to bind to when using http transport (default: 127.0.0.1)')] = None, port: Annotated[int | None, typer.Option('--port', '-p', help='Port to bind to when using http transport (default: 8000)')] = None, log_level: Annotated[str | None, typer.Option('--log-level', '-l', help='Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)')] = None) -> None +``` + + +Run a MCP server or connect to a remote one. + +The server can be specified in three ways: +1. Module approach: server.py - runs the module directly, looking for an object named mcp/server/app. + +2. Import approach: server.py:app - imports and runs the specified server object. + +3. URL approach: http://server-url - connects to a remote server and creates a proxy. + + + +Note: This command runs the server directly. You are responsible for ensuring +all dependencies are available. + +Server arguments can be passed after -- : +fastmcp run server.py -- --config config.json --debug + + +### `install` + +```python +install(server_spec: str = typer.Argument(..., help='Python file to run, optionally with :object suffix'), server_name: Annotated[str | None, typer.Option('--name', '-n', help="Custom name for the server (defaults to server's name attribute or file name)")] = None, with_editable: Annotated[Path | None, typer.Option('--with-editable', '-e', help='Directory containing pyproject.toml to install in editable mode', exists=True, file_okay=False, resolve_path=True)] = None, with_packages: Annotated[list[str], typer.Option('--with', help='Additional packages to install')] = [], env_vars: Annotated[list[str], typer.Option('--env-var', '-v', help='Environment variables in KEY=VALUE format')] = [], env_file: Annotated[Path | None, typer.Option('--env-file', '-f', help='Load environment variables from a .env file', exists=True, file_okay=True, dir_okay=False, resolve_path=True)] = None) -> None +``` + + +Install a MCP server in the Claude desktop app. + +Environment variables are preserved once added and only updated if new values +are explicitly provided. + diff --git a/docs/python-sdk/fastmcp-cli-run.mdx b/docs/python-sdk/fastmcp-cli-run.mdx new file mode 100644 index 000000000..bdb07beac --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-run.mdx @@ -0,0 +1,106 @@ +--- +title: run +sidebarTitle: run +--- + +# `fastmcp.cli.run` + + +FastMCP run command implementation. + +## Functions + +### `is_url` + +```python +is_url(path: str) -> bool +``` + + +Check if a string is a URL. + + +### `parse_file_path` + +```python +parse_file_path(server_spec: str) -> tuple[Path, str | None] +``` + + +Parse a file path that may include a server object specification. + +**Args:** +- `server_spec`: Path to file, optionally with :object suffix + +**Returns:** +- Tuple of (file_path, server_object) + + +### `import_server` + +```python +import_server(file: Path, server_object: str | None = None) -> Any +``` + + +Import a MCP server from a file. + +**Args:** +- `file`: Path to the file +- `server_object`: Optional object name in format "module:object" or just "object" + +**Returns:** +- The server object + + +### `create_client_server` + +```python +create_client_server(url: str) -> Any +``` + + +Create a FastMCP server from a client URL. + +**Args:** +- `url`: The URL to connect to + +**Returns:** +- A FastMCP server instance + + +### `import_server_with_args` + +```python +import_server_with_args(file: Path, server_object: str | None = None, server_args: list[str] | None = None) -> Any +``` + + +Import a server with optional command line arguments. + +**Args:** +- `file`: Path to the server file +- `server_object`: Optional server object name +- `server_args`: Optional command line arguments to inject + +**Returns:** +- The imported server object + + +### `run_command` + +```python +run_command(server_spec: str, transport: str | None = None, host: str | None = None, port: int | None = None, log_level: str | None = None, server_args: list[str] | None = None) -> None +``` + + +Run a MCP server or connect to a remote one. + +**Args:** +- `server_spec`: Python file, object specification (file:obj), or URL +- `transport`: Transport protocol to use +- `host`: Host to bind to when using http transport +- `port`: Port to bind to when using http transport +- `log_level`: Log level +- `server_args`: Additional arguments to pass to the server + diff --git a/docs/python-sdk/fastmcp-client-__init__.mdx b/docs/python-sdk/fastmcp-client-__init__.mdx new file mode 100644 index 000000000..bc145d4b7 --- /dev/null +++ b/docs/python-sdk/fastmcp-client-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.client` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-client-auth-__init__.mdx b/docs/python-sdk/fastmcp-client-auth-__init__.mdx new file mode 100644 index 000000000..28242780d --- /dev/null +++ b/docs/python-sdk/fastmcp-client-auth-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.client.auth` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-client-auth-bearer.mdx b/docs/python-sdk/fastmcp-client-auth-bearer.mdx new file mode 100644 index 000000000..ab0c15240 --- /dev/null +++ b/docs/python-sdk/fastmcp-client-auth-bearer.mdx @@ -0,0 +1,18 @@ +--- +title: bearer +sidebarTitle: bearer +--- + +# `fastmcp.client.auth.bearer` + +## Classes + +### `BearerAuth` + +**Methods:** + +#### `auth_flow` + +```python +auth_flow(self, request) +``` diff --git a/docs/python-sdk/fastmcp-client-auth-oauth.mdx b/docs/python-sdk/fastmcp-client-auth-oauth.mdx new file mode 100644 index 000000000..d299c1a95 --- /dev/null +++ b/docs/python-sdk/fastmcp-client-auth-oauth.mdx @@ -0,0 +1,103 @@ +--- +title: oauth +sidebarTitle: oauth +--- + +# `fastmcp.client.auth.oauth` + +## Functions + +### `default_cache_dir` + +```python +default_cache_dir() -> Path +``` + +### `OAuth` + +```python +OAuth(mcp_url: str, scopes: str | list[str] | None = None, client_name: str = 'FastMCP Client', token_storage_cache_dir: Path | None = None, additional_client_metadata: dict[str, Any] | None = None) -> _MCPOAuthClientProvider +``` + + +Create an OAuthClientProvider for an MCP server. + +This is intended to be provided to the `auth` parameter of an +httpx.AsyncClient (or appropriate FastMCP client/transport instance) + +**Args:** +- `mcp_url`: Full URL to the MCP endpoint (e.g., +- `"http`: //host/mcp/sse") +- `scopes`: OAuth scopes to request. Can be a +- `client_name`: Name for this client during registration +- `token_storage_cache_dir`: Directory for FileTokenStorage +- `additional_client_metadata`: Extra fields for OAuthClientMetadata + +**Returns:** +- OAuthClientProvider + + +## Classes + +### `ServerOAuthMetadata` + + +More flexible OAuth metadata model that accepts broader ranges of values +than the restrictive MCP standard model. + +This handles real-world OAuth servers like PayPal that may support +additional methods not in the MCP specification. + + +### `OAuthClientProvider` + + +OAuth client provider with more flexible OAuth metadata discovery. + + +### `FileTokenStorage` + + +File-based token storage implementation for OAuth credentials and tokens. +Implements the mcp.client.auth.TokenStorage protocol. + +Each instance is tied to a specific server URL for proper token isolation. + + +**Methods:** + +#### `get_base_url` + +```python +get_base_url(url: str) -> str +``` + +Extract the base URL (scheme + host) from a URL. + + +#### `get_cache_key` + +```python +get_cache_key(self) -> str +``` + +Generate a safe filesystem key from the server's base URL. + + +#### `clear` + +```python +clear(self) -> None +``` + +Clear all cached data for this server. + + +#### `clear_all` + +```python +clear_all(cls, cache_dir: Path | None = None) -> None +``` + +Clear all cached data for all servers. + diff --git a/docs/python-sdk/fastmcp-client-client.mdx b/docs/python-sdk/fastmcp-client-client.mdx new file mode 100644 index 000000000..4c3f252bf --- /dev/null +++ b/docs/python-sdk/fastmcp-client-client.mdx @@ -0,0 +1,94 @@ +--- +title: client +sidebarTitle: client +--- + +# `fastmcp.client.client` + +## Classes + +### `Client` + + + + MCP client that delegates connection management to a Transport instance. + + The Client class is responsible for MCP protocol logic, while the Transport + handles connection establishment and management. Client provides methods for + working with resources, prompts, tools and other MCP capabilities. + + Args: + transport: Connection source specification, which can be: + - ClientTransport: Direct transport instance + - FastMCP: In-process FastMCP server + - AnyUrl | str: URL to connect to + - Path: File path for local socket + - MCPConfig: MCP server configuration + - dict: Transport configuration + roots: Optional RootsList or RootsHandler for filesystem access + sampling_handler: Optional handler for sampling requests + log_handler: Optional handler for log messages + message_handler: Optional handler for protocol messages + progress_handler: Optional handler for progress notifications + timeout: Optional timeout for requests (seconds or timedelta) + init_timeout: Optional timeout for initial connection (seconds or timedelta). + Set to 0 to disable. If None, uses the value in the FastMCP global settings. + + Examples: + ```python # Connect to FastMCP server client = + Client("http://localhost:8080") + + async with client: + # List available resources resources = await client.list_resources() + + # Call a tool result = await client.call_tool("my_tool", {"param": + "value"}) + ``` + + +**Methods:** + +#### `session` + +```python +session(self) -> ClientSession +``` + +Get the current active session. Raises RuntimeError if not connected. + + +#### `initialize_result` + +```python +initialize_result(self) -> mcp.types.InitializeResult +``` + +Get the result of the initialization request. + + +#### `set_roots` + +```python +set_roots(self, roots: RootsList | RootsHandler) -> None +``` + +Set the roots for the client. This does not automatically call `send_roots_list_changed`. + + +#### `set_sampling_callback` + +```python +set_sampling_callback(self, sampling_callback: SamplingHandler) -> None +``` + +Set the sampling callback for the client. + + +#### `is_connected` + +```python +is_connected(self) -> bool +``` + +Check if the client is currently connected. + diff --git a/docs/python-sdk/fastmcp-client-logging.mdx b/docs/python-sdk/fastmcp-client-logging.mdx new file mode 100644 index 000000000..84d201db7 --- /dev/null +++ b/docs/python-sdk/fastmcp-client-logging.mdx @@ -0,0 +1,14 @@ +--- +title: logging +sidebarTitle: logging +--- + +# `fastmcp.client.logging` + +## Functions + +### `create_log_callback` + +```python +create_log_callback(handler: LogHandler | None = None) -> LoggingFnT +``` diff --git a/docs/python-sdk/fastmcp-client-oauth_callback.mdx b/docs/python-sdk/fastmcp-client-oauth_callback.mdx new file mode 100644 index 000000000..6eab9de3a --- /dev/null +++ b/docs/python-sdk/fastmcp-client-oauth_callback.mdx @@ -0,0 +1,63 @@ +--- +title: oauth_callback +sidebarTitle: oauth_callback +--- + +# `fastmcp.client.oauth_callback` + + + +OAuth callback server for handling authorization code flows. + +This module provides a reusable callback server that can handle OAuth redirects +and display styled responses to users. + + +## Functions + +### `create_callback_html` + +```python +create_callback_html(message: str, is_success: bool = True, title: str = 'FastMCP OAuth', server_url: str | None = None) -> str +``` + + +Create a styled HTML response for OAuth callbacks. + + +### `create_oauth_callback_server` + +```python +create_oauth_callback_server(port: int, callback_path: str = '/callback', server_url: str | None = None, response_future: asyncio.Future | None = None) -> Server +``` + + +Create an OAuth callback server. + +**Args:** +- `port`: The port to run the server on +- `callback_path`: The path to listen for OAuth redirects on +- `server_url`: Optional server URL to display in success messages +- `response_future`: Optional future to resolve when OAuth callback is received + +**Returns:** +- Configured uvicorn Server instance (not yet running) + + +## Classes + +### `CallbackResponse` + +**Methods:** + +#### `from_dict` + +```python +from_dict(cls, data: dict[str, str]) -> CallbackResponse +``` + +#### `to_dict` + +```python +to_dict(self) -> dict[str, str] +``` diff --git a/docs/python-sdk/fastmcp-client-progress.mdx b/docs/python-sdk/fastmcp-client-progress.mdx new file mode 100644 index 000000000..aecd0f37b --- /dev/null +++ b/docs/python-sdk/fastmcp-client-progress.mdx @@ -0,0 +1,8 @@ +--- +title: progress +sidebarTitle: progress +--- + +# `fastmcp.client.progress` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-client-roots.mdx b/docs/python-sdk/fastmcp-client-roots.mdx new file mode 100644 index 000000000..820e1d0a7 --- /dev/null +++ b/docs/python-sdk/fastmcp-client-roots.mdx @@ -0,0 +1,20 @@ +--- +title: roots +sidebarTitle: roots +--- + +# `fastmcp.client.roots` + +## Functions + +### `convert_roots_list` + +```python +convert_roots_list(roots: RootsList) -> list[mcp.types.Root] +``` + +### `create_roots_callback` + +```python +create_roots_callback(handler: RootsList | RootsHandler) -> ListRootsFnT +``` diff --git a/docs/python-sdk/fastmcp-client-sampling.mdx b/docs/python-sdk/fastmcp-client-sampling.mdx new file mode 100644 index 000000000..be78badeb --- /dev/null +++ b/docs/python-sdk/fastmcp-client-sampling.mdx @@ -0,0 +1,14 @@ +--- +title: sampling +sidebarTitle: sampling +--- + +# `fastmcp.client.sampling` + +## Functions + +### `create_sampling_callback` + +```python +create_sampling_callback(sampling_handler: SamplingHandler) -> SamplingFnT +``` diff --git a/docs/python-sdk/fastmcp-client-transports.mdx b/docs/python-sdk/fastmcp-client-transports.mdx new file mode 100644 index 000000000..a4f9d22e6 --- /dev/null +++ b/docs/python-sdk/fastmcp-client-transports.mdx @@ -0,0 +1,191 @@ +--- +title: transports +sidebarTitle: transports +--- + +# `fastmcp.client.transports` + +## Functions + +### `infer_transport` + +```python +infer_transport(transport: ClientTransport | FastMCP | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str) -> ClientTransport +``` + + + + Infer the appropriate transport type from the given transport argument. + + This function attempts to infer the correct transport type from the provided + argument, handling various input types and converting them to the appropriate + ClientTransport subclass. + + The function supports these input types: + - ClientTransport: Used directly without modification + - FastMCP or FastMCP1Server: Creates an in-memory FastMCPTransport + - Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js) + - AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints) + - MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers + + For HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`. + + For MCPConfig with multiple servers, a composite client is created where each server + is mounted with its name as prefix. This allows accessing tools and resources from multiple + servers through a single unified client interface, using naming patterns like + `servername_toolname` for tools and `protocol://servername/path` for resources. + If the MCPConfig contains only one server, a direct connection is established without prefixing. + + Examples: + ```python + # Connect to a local Python script + transport = infer_transport("my_script.py") + + # Connect to a remote server via HTTP + transport = infer_transport("http://example.com/mcp") + + # Connect to multiple servers using MCPConfig + config = { + "mcpServers": { + "weather": {"url": "http://weather.example.com/mcp"}, + "calendar": {"url": "http://calendar.example.com/mcp"} + } + } + transport = infer_transport(config) + ``` + + +## Classes + +### `SessionKwargs` + + +Keyword arguments for the MCP ClientSession constructor. + + +### `ClientTransport` + + +Abstract base class for different MCP client transport mechanisms. + +A Transport is responsible for establishing and managing connections +to an MCP server, and providing a ClientSession within an async context. + + +### `WSTransport` + + +Transport implementation that connects to an MCP server via WebSockets. + + +### `SSETransport` + + +Transport implementation that connects to an MCP server via Server-Sent Events. + + +### `StreamableHttpTransport` + + +Transport implementation that connects to an MCP server via Streamable HTTP Requests. + + +### `StdioTransport` + + +Base transport for connecting to an MCP server via subprocess with stdio. + +This is a base class that can be subclassed for specific command-based +transports like Python, Node, Uvx, etc. + + +### `PythonStdioTransport` + + +Transport for running Python scripts. + + +### `FastMCPStdioTransport` + + +Transport for running FastMCP servers using the FastMCP CLI. + + +### `NodeStdioTransport` + + +Transport for running Node.js scripts. + + +### `UvxStdioTransport` + + +Transport for running commands via the uvx tool. + + +### `NpxStdioTransport` + + +Transport for running commands via the npx tool. + + +### `FastMCPTransport` + + +In-memory transport for FastMCP servers. + +This transport connects directly to a FastMCP server instance in the same +Python process. It works with both FastMCP 2.x servers and FastMCP 1.0 +servers from the low-level MCP SDK. This is particularly useful for unit +tests or scenarios where client and server run in the same runtime. + + +### `MCPConfigTransport` + + +Transport for connecting to one or more MCP servers defined in an MCPConfig. + + This transport provides a unified interface to multiple MCP servers defined in an MCPConfig + object or dictionary matching the MCPConfig schema. It supports two key scenarios: + + 1. If the MCPConfig contains exactly one server, it creates a direct transport to that server. + 2. If the MCPConfig contains multiple servers, it creates a composite client by mounting + all servers on a single FastMCP instance, with each server's name used as its mounting prefix. + + In the multi-server case, tools are accessible with the prefix pattern `{server_name}_{tool_name}` + and resources with the pattern `protocol://{server_name}/path/to/resource`. + + This is particularly useful for creating clients that need to interact with multiple specialized + MCP servers through a single interface, simplifying client code. + + Examples: + ```python + from fastmcp import Client + from fastmcp.utilities.mcp_config import MCPConfig + + # Create a config with multiple servers + config = { + "mcpServers": { + "weather": { + "url": "https://weather-api.example.com/mcp", + "transport": "streamable-http" + }, + "calendar": { + "url": "https://calendar-api.example.com/mcp", + "transport": "streamable-http" + } + } + } + + # Create a client with the config + client = Client(config) + + async with client: + # Access tools with prefixes + weather = await client.call_tool("weather_get_forecast", {"city": "London"}) + events = await client.call_tool("calendar_list_events", {"date": "2023-06-01"}) + + # Access resources with prefixed URIs + icons = await client.read_resource("weather://weather/icons/sunny") + ``` + diff --git a/docs/python-sdk/fastmcp-exceptions.mdx b/docs/python-sdk/fastmcp-exceptions.mdx new file mode 100644 index 000000000..9726d1cde --- /dev/null +++ b/docs/python-sdk/fastmcp-exceptions.mdx @@ -0,0 +1,65 @@ +--- +title: exceptions +sidebarTitle: exceptions +--- + +# `fastmcp.exceptions` + + +Custom exceptions for FastMCP. + +## Classes + +### `FastMCPError` + + +Base error for FastMCP. + + +### `ValidationError` + + +Error in validating parameters or return values. + + +### `ResourceError` + + +Error in resource operations. + + +### `ToolError` + + +Error in tool operations. + + +### `PromptError` + + +Error in prompt operations. + + +### `InvalidSignature` + + +Invalid signature for use with FastMCP. + + +### `ClientError` + + +Error in client operations. + + +### `NotFoundError` + + +Object not found. + + +### `DisabledError` + + +Object is disabled. + diff --git a/docs/python-sdk/fastmcp-prompts-__init__.mdx b/docs/python-sdk/fastmcp-prompts-__init__.mdx new file mode 100644 index 000000000..8ef80b59e --- /dev/null +++ b/docs/python-sdk/fastmcp-prompts-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.prompts` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-prompts-prompt.mdx b/docs/python-sdk/fastmcp-prompts-prompt.mdx new file mode 100644 index 000000000..60028f316 --- /dev/null +++ b/docs/python-sdk/fastmcp-prompts-prompt.mdx @@ -0,0 +1,84 @@ +--- +title: prompt +sidebarTitle: prompt +--- + +# `fastmcp.prompts.prompt` + + +Base classes for FastMCP prompts. + +## Functions + +### `Message` + +```python +Message(content: str | MCPContent, role: Role | None = None, **kwargs: Any) -> PromptMessage +``` + + +A user-friendly constructor for PromptMessage. + + +## Classes + +### `PromptArgument` + + +An argument that can be passed to a prompt. + + +### `Prompt` + + +A prompt template that can be rendered with parameters. + + +**Methods:** + +#### `to_mcp_prompt` + +```python +to_mcp_prompt(self, **overrides: Any) -> MCPPrompt +``` + +Convert the prompt to an MCP prompt. + + +#### `from_function` + +```python +from_function(fn: Callable[..., PromptResult | Awaitable[PromptResult]], name: str | None = None, description: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionPrompt +``` + +Create a Prompt from a function. + +The function can return: +- A string (converted to a message) +- A Message object +- A dict (converted to a message) +- A sequence of any of the above + + +### `FunctionPrompt` + + +A prompt that is a function. + + +**Methods:** + +#### `from_function` + +```python +from_function(cls, fn: Callable[..., PromptResult | Awaitable[PromptResult]], name: str | None = None, description: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionPrompt +``` + +Create a Prompt from a function. + +The function can return: +- A string (converted to a message) +- A Message object +- A dict (converted to a message) +- A sequence of any of the above + diff --git a/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx b/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx new file mode 100644 index 000000000..041337c28 --- /dev/null +++ b/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx @@ -0,0 +1,43 @@ +--- +title: prompt_manager +sidebarTitle: prompt_manager +--- + +# `fastmcp.prompts.prompt_manager` + +## Classes + +### `PromptManager` + + +Manages FastMCP prompts. + + +**Methods:** + +#### `mount` + +```python +mount(self, server: MountedServer) -> None +``` + +Adds a mounted server as a source for prompts. + + +#### `add_prompt_from_fn` + +```python +add_prompt_from_fn(self, fn: Callable[..., PromptResult | Awaitable[PromptResult]], name: str | None = None, description: str | None = None, tags: set[str] | None = None) -> FunctionPrompt +``` + +Create a prompt from a function. + + +#### `add_prompt` + +```python +add_prompt(self, prompt: Prompt) -> Prompt +``` + +Add a prompt to the manager. + diff --git a/docs/python-sdk/fastmcp-resources-__init__.mdx b/docs/python-sdk/fastmcp-resources-__init__.mdx new file mode 100644 index 000000000..cc5fd2786 --- /dev/null +++ b/docs/python-sdk/fastmcp-resources-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.resources` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-resources-resource.mdx b/docs/python-sdk/fastmcp-resources-resource.mdx new file mode 100644 index 000000000..dcfc51f00 --- /dev/null +++ b/docs/python-sdk/fastmcp-resources-resource.mdx @@ -0,0 +1,90 @@ +--- +title: resource +sidebarTitle: resource +--- + +# `fastmcp.resources.resource` + + +Base classes and interfaces for FastMCP resources. + +## Classes + +### `Resource` + + +Base class for all resources. + + +**Methods:** + +#### `from_function` + +```python +from_function(fn: Callable[[], Any], uri: str | AnyUrl, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionResource +``` + +#### `set_default_mime_type` + +```python +set_default_mime_type(cls, mime_type: str | None) -> str +``` + +Set default MIME type if not provided. + + +#### `set_default_name` + +```python +set_default_name(self) -> Self +``` + +Set default name from URI if not provided. + + +#### `to_mcp_resource` + +```python +to_mcp_resource(self, **overrides: Any) -> MCPResource +``` + +Convert the resource to an MCPResource. + + +#### `key` + +```python +key(self) -> str +``` + +The key of the component. This is used for internal bookkeeping +and may reflect e.g. prefixes or other identifiers. You should not depend on +keys having a certain value, as the same tool loaded from different +hierarchies of servers may have different keys. + + +### `FunctionResource` + + +A resource that defers data loading by wrapping a function. + +The function is only called when the resource is read, allowing for lazy loading +of potentially expensive data. This is particularly useful when listing resources, +as the function won't be called until the resource is actually accessed. + +The function can return: +- str for text content (default) +- bytes for binary content +- other types will be converted to JSON + + +**Methods:** + +#### `from_function` + +```python +from_function(cls, fn: Callable[[], Any], uri: str | AnyUrl, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionResource +``` + +Create a FunctionResource from a function. + diff --git a/docs/python-sdk/fastmcp-resources-resource_manager.mdx b/docs/python-sdk/fastmcp-resources-resource_manager.mdx new file mode 100644 index 000000000..9adb43e83 --- /dev/null +++ b/docs/python-sdk/fastmcp-resources-resource_manager.mdx @@ -0,0 +1,111 @@ +--- +title: resource_manager +sidebarTitle: resource_manager +--- + +# `fastmcp.resources.resource_manager` + + +Resource manager functionality. + +## Classes + +### `ResourceManager` + + +Manages FastMCP resources. + + +**Methods:** + +#### `mount` + +```python +mount(self, server: MountedServer) -> None +``` + +Adds a mounted server as a source for resources and templates. + + +#### `add_resource_or_template_from_fn` + +```python +add_resource_or_template_from_fn(self, fn: Callable[..., Any], uri: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> Resource | ResourceTemplate +``` + +Add a resource or template to the manager from a function. + +**Args:** +- `fn`: The function to register as a resource or template +- `uri`: The URI for the resource or template +- `name`: Optional name for the resource or template +- `description`: Optional description of the resource or template +- `mime_type`: Optional MIME type for the resource or template +- `tags`: Optional set of tags for categorizing the resource or template + +**Returns:** +- The added resource or template. If a resource or template with the same URI already exists, +- returns the existing resource or template. + + +#### `add_resource_from_fn` + +```python +add_resource_from_fn(self, fn: Callable[..., Any], uri: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> Resource +``` + +Add a resource to the manager from a function. + +**Args:** +- `fn`: The function to register as a resource +- `uri`: The URI for the resource +- `name`: Optional name for the resource +- `description`: Optional description of the resource +- `mime_type`: Optional MIME type for the resource +- `tags`: Optional set of tags for categorizing the resource + +**Returns:** +- The added resource. If a resource with the same URI already exists, +- returns the existing resource. + + +#### `add_resource` + +```python +add_resource(self, resource: Resource) -> Resource +``` + +Add a resource to the manager. + +**Args:** +- `resource`: A Resource instance to add. The resource's .key attribute +will be used as the storage key. To overwrite it, call +Resource.with_key() before calling this method. + + +#### `add_template_from_fn` + +```python +add_template_from_fn(self, fn: Callable[..., Any], uri_template: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> ResourceTemplate +``` + +Create a template from a function. + + +#### `add_template` + +```python +add_template(self, template: ResourceTemplate) -> ResourceTemplate +``` + +Add a template to the manager. + +**Args:** +- `template`: A ResourceTemplate instance to add. The template's .key attribute +will be used as the storage key. To overwrite it, call +ResourceTemplate.with_key() before calling this method. + +**Returns:** +- The added template. If a template with the same URI already exists, +- returns the existing template. + diff --git a/docs/python-sdk/fastmcp-resources-template.mdx b/docs/python-sdk/fastmcp-resources-template.mdx new file mode 100644 index 000000000..c1810f097 --- /dev/null +++ b/docs/python-sdk/fastmcp-resources-template.mdx @@ -0,0 +1,104 @@ +--- +title: template +sidebarTitle: template +--- + +# `fastmcp.resources.template` + + +Resource template functionality. + +## Functions + +### `build_regex` + +```python +build_regex(template: str) -> re.Pattern +``` + +### `match_uri_template` + +```python +match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None +``` + +## Classes + +### `ResourceTemplate` + + +A template for dynamically creating resources. + + +**Methods:** + +#### `from_function` + +```python +from_function(fn: Callable[..., Any], uri_template: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionResourceTemplate +``` + +#### `set_default_mime_type` + +```python +set_default_mime_type(cls, mime_type: str | None) -> str +``` + +Set default MIME type if not provided. + + +#### `matches` + +```python +matches(self, uri: str) -> dict[str, Any] | None +``` + +Check if URI matches template and extract parameters. + + +#### `to_mcp_template` + +```python +to_mcp_template(self, **overrides: Any) -> MCPResourceTemplate +``` + +Convert the resource template to an MCPResourceTemplate. + + +#### `from_mcp_template` + +```python +from_mcp_template(cls, mcp_template: MCPResourceTemplate) -> ResourceTemplate +``` + +Creates a FastMCP ResourceTemplate from a raw MCP ResourceTemplate object. + + +#### `key` + +```python +key(self) -> str +``` + +The key of the component. This is used for internal bookkeeping +and may reflect e.g. prefixes or other identifiers. You should not depend on +keys having a certain value, as the same tool loaded from different +hierarchies of servers may have different keys. + + +### `FunctionResourceTemplate` + + +A template for dynamically creating resources. + + +**Methods:** + +#### `from_function` + +```python +from_function(cls, fn: Callable[..., Any], uri_template: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionResourceTemplate +``` + +Create a template from a function. + diff --git a/docs/python-sdk/fastmcp-resources-types.mdx b/docs/python-sdk/fastmcp-resources-types.mdx new file mode 100644 index 000000000..675b44cc1 --- /dev/null +++ b/docs/python-sdk/fastmcp-resources-types.mdx @@ -0,0 +1,83 @@ +--- +title: types +sidebarTitle: types +--- + +# `fastmcp.resources.types` + + +Concrete resource implementations. + +## Classes + +### `TextResource` + + +A resource that reads from a string. + + +### `BinaryResource` + + +A resource that reads from bytes. + + +### `FileResource` + + +A resource that reads from a file. + +Set is_binary=True to read file as binary data instead of text. + + +**Methods:** + +#### `validate_absolute_path` + +```python +validate_absolute_path(cls, path: Path) -> Path +``` + +Ensure path is absolute. + + +#### `set_binary_from_mime_type` + +```python +set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool +``` + +Set is_binary based on mime_type if not explicitly set. + + +### `HttpResource` + + +A resource that reads from an HTTP endpoint. + + +### `DirectoryResource` + + +A resource that lists files in a directory. + + +**Methods:** + +#### `validate_absolute_path` + +```python +validate_absolute_path(cls, path: Path) -> Path +``` + +Ensure path is absolute. + + +#### `list_files` + +```python +list_files(self) -> list[Path] +``` + +List files in the directory. + diff --git a/docs/python-sdk/fastmcp-server-__init__.mdx b/docs/python-sdk/fastmcp-server-__init__.mdx new file mode 100644 index 000000000..157a018ce --- /dev/null +++ b/docs/python-sdk/fastmcp-server-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.server` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-server-auth-__init__.mdx b/docs/python-sdk/fastmcp-server-auth-__init__.mdx new file mode 100644 index 000000000..c86f07005 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-auth-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.server.auth` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-server-auth-auth.mdx b/docs/python-sdk/fastmcp-server-auth-auth.mdx new file mode 100644 index 000000000..8a20aa716 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-auth-auth.mdx @@ -0,0 +1,10 @@ +--- +title: auth +sidebarTitle: auth +--- + +# `fastmcp.server.auth.auth` + +## Classes + +### `OAuthProvider` diff --git a/docs/python-sdk/fastmcp-server-auth-providers-__init__.mdx b/docs/python-sdk/fastmcp-server-auth-providers-__init__.mdx new file mode 100644 index 000000000..9de7cce8a --- /dev/null +++ b/docs/python-sdk/fastmcp-server-auth-providers-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.server.auth.providers` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx b/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx new file mode 100644 index 000000000..d619de127 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx @@ -0,0 +1,69 @@ +--- +title: bearer +sidebarTitle: bearer +--- + +# `fastmcp.server.auth.providers.bearer` + +## Classes + +### `JWKData` + + +JSON Web Key data structure. + + +### `JWKSData` + + +JSON Web Key Set data structure. + + +### `RSAKeyPair` + +**Methods:** + +#### `generate` + +```python +generate(cls) -> 'RSAKeyPair' +``` + +Generate an RSA key pair for testing. + +**Returns:** +- (private_key_pem, public_key_pem) + + +#### `create_token` + +```python +create_token(self, subject: str = 'fastmcp-user', issuer: str = 'https://fastmcp.example.com', audience: str | None = None, scopes: list[str] | None = None, expires_in_seconds: int = 3600, additional_claims: dict[str, Any] | None = None, kid: str | None = None) -> str +``` + +Generate a test JWT token for testing purposes. + +**Args:** +- `private_key_pem`: RSA private key in PEM format +- `subject`: Subject claim (usually user ID) +- `issuer`: Issuer claim +- `audience`: Audience claim (optional) +- `scopes`: List of scopes to include +- `expires_in_seconds`: Token expiration time in seconds +- `additional_claims`: Any additional claims to include +- `kid`: Key ID for JWKS lookup (optional) + +**Returns:** +- Signed JWT token string + + +### `BearerAuthProvider` + + +Simple JWT Bearer Token validator for hosted MCP servers. +Uses RS256 asymmetric encryption. Supports either static public key +or JWKS URI for key rotation. + +Note that this provider DOES NOT permit client registration or revocation, or any OAuth flows. +It is intended to be used with a control plane that manages clients and tokens. + diff --git a/docs/python-sdk/fastmcp-server-auth-providers-bearer_env.mdx b/docs/python-sdk/fastmcp-server-auth-providers-bearer_env.mdx new file mode 100644 index 000000000..f64c65c84 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-auth-providers-bearer_env.mdx @@ -0,0 +1,22 @@ +--- +title: bearer_env +sidebarTitle: bearer_env +--- + +# `fastmcp.server.auth.providers.bearer_env` + +## Classes + +### `EnvBearerAuthProviderSettings` + + +Settings for the BearerAuthProvider. + + +### `EnvBearerAuthProvider` + + +A BearerAuthProvider that loads settings from environment variables. Any +providing setting will always take precedence over the environment +variables. + diff --git a/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx b/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx new file mode 100644 index 000000000..ef34ce2fb --- /dev/null +++ b/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx @@ -0,0 +1,15 @@ +--- +title: in_memory +sidebarTitle: in_memory +--- + +# `fastmcp.server.auth.providers.in_memory` + +## Classes + +### `InMemoryOAuthProvider` + + +An in-memory OAuth provider for testing purposes. +It simulates the OAuth 2.1 flow locally without external calls. + diff --git a/docs/python-sdk/fastmcp-server-context.mdx b/docs/python-sdk/fastmcp-server-context.mdx new file mode 100644 index 000000000..ea1d92643 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-context.mdx @@ -0,0 +1,118 @@ +--- +title: context +sidebarTitle: context +--- + +# `fastmcp.server.context` + +## Functions + +### `set_context` + +```python +set_context(context: Context) -> Generator[Context, None, None] +``` + +## Classes + +### `Context` + + +Context object providing access to MCP capabilities. + +This provides a cleaner interface to MCP's RequestContext functionality. +It gets injected into tool and resource functions that request it via type hints. + +To use context in a tool function, add a parameter with the Context type annotation: + +```python +@server.tool +def my_tool(x: int, ctx: Context) -> str: + # Log messages to the client + ctx.info(f"Processing {x}") + ctx.debug("Debug info") + ctx.warning("Warning message") + ctx.error("Error message") + + # Report progress + ctx.report_progress(50, 100, "Processing") + + # Access resources + data = ctx.read_resource("resource://data") + + # Get request info + request_id = ctx.request_id + client_id = ctx.client_id + + return str(x) +``` + +The context parameter name can be anything as long as it's annotated with Context. +The context is optional - tools that don't need it can omit the parameter. + + +**Methods:** + +#### `request_context` + +```python +request_context(self) -> RequestContext +``` + +Access to the underlying request context. + +If called outside of a request context, this will raise a ValueError. + + +#### `client_id` + +```python +client_id(self) -> str | None +``` + +Get the client ID if available. + + +#### `request_id` + +```python +request_id(self) -> str +``` + +Get the unique ID for this request. + + +#### `session_id` + +```python +session_id(self) -> str | None +``` + +Get the MCP session ID for HTTP transports. + +Returns the session ID that can be used as a key for session-based +data storage (e.g., Redis) to share data between tool calls within +the same client session. + +**Returns:** +- The session ID for HTTP transports (SSE, StreamableHTTP), or None +- for stdio and in-memory transports which don't use session IDs. + + +#### `session` + +```python +session(self) +``` + +Access to the underlying session for advanced usage. + + +#### `get_http_request` + +```python +get_http_request(self) -> Request +``` + +Get the active starlette request. + diff --git a/docs/python-sdk/fastmcp-server-dependencies.mdx b/docs/python-sdk/fastmcp-server-dependencies.mdx new file mode 100644 index 000000000..0d6c37074 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-dependencies.mdx @@ -0,0 +1,36 @@ +--- +title: dependencies +sidebarTitle: dependencies +--- + +# `fastmcp.server.dependencies` + +## Functions + +### `get_context` + +```python +get_context() -> Context +``` + +### `get_http_request` + +```python +get_http_request() -> Request +``` + +### `get_http_headers` + +```python +get_http_headers(include_all: bool = False) -> dict[str, str] +``` + + +Extract headers from the current HTTP request if available. + +Never raises an exception, even if there is no active HTTP request (in which case +an empty dict is returned). + +By default, strips problematic headers like `content-length` that cause issues if forwarded to downstream clients. +If `include_all` is True, all headers are returned. + diff --git a/docs/python-sdk/fastmcp-server-http.mdx b/docs/python-sdk/fastmcp-server-http.mdx new file mode 100644 index 000000000..63f2768cb --- /dev/null +++ b/docs/python-sdk/fastmcp-server-http.mdx @@ -0,0 +1,113 @@ +--- +title: http +sidebarTitle: http +--- + +# `fastmcp.server.http` + +## Functions + +### `set_http_request` + +```python +set_http_request(request: Request) -> Generator[Request, None, None] +``` + +### `setup_auth_middleware_and_routes` + +```python +setup_auth_middleware_and_routes(auth: OAuthProvider) -> tuple[list[Middleware], list[BaseRoute], list[str]] +``` + + +Set up authentication middleware and routes if auth is enabled. + +**Args:** +- `auth`: The OAuthProvider authorization server provider + +**Returns:** +- Tuple of (middleware, auth_routes, required_scopes) + + +### `create_base_app` + +```python +create_base_app(routes: list[BaseRoute], middleware: list[Middleware], debug: bool = False, lifespan: Callable | None = None) -> StarletteWithLifespan +``` + + +Create a base Starlette app with common middleware and routes. + +**Args:** +- `routes`: List of routes to include in the app +- `middleware`: List of middleware to include in the app +- `debug`: Whether to enable debug mode +- `lifespan`: Optional lifespan manager for the app + +**Returns:** +- A Starlette application + + +### `create_sse_app` + +```python +create_sse_app(server: FastMCP[LifespanResultT], message_path: str, sse_path: str, auth: OAuthProvider | None = None, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None) -> StarletteWithLifespan +``` + + +Return an instance of the SSE server app. + +**Args:** +- `server`: The FastMCP server instance +- `message_path`: Path for SSE messages +- `sse_path`: Path for SSE connections +- `auth`: Optional auth provider +- `debug`: Whether to enable debug mode +- `routes`: Optional list of custom routes +- `middleware`: Optional list of middleware + +Returns: + A Starlette application with RequestContextMiddleware + + +### `create_streamable_http_app` + +```python +create_streamable_http_app(server: FastMCP[LifespanResultT], streamable_http_path: str, event_store: EventStore | None = None, auth: OAuthProvider | None = None, json_response: bool = False, stateless_http: bool = False, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None) -> StarletteWithLifespan +``` + + +Return an instance of the StreamableHTTP server app. + +**Args:** +- `server`: The FastMCP server instance +- `streamable_http_path`: Path for StreamableHTTP connections +- `event_store`: Optional event store for session management +- `auth`: Optional auth provider +- `json_response`: Whether to use JSON response format +- `stateless_http`: Whether to use stateless mode (new transport per request) +- `debug`: Whether to enable debug mode +- `routes`: Optional list of custom routes +- `middleware`: Optional list of middleware + +**Returns:** +- A Starlette application with StreamableHTTP support + + +## Classes + +### `StarletteWithLifespan` + +**Methods:** + +#### `lifespan` + +```python +lifespan(self) -> Lifespan +``` + +### `RequestContextMiddleware` + + +Middleware that stores each request in a ContextVar + diff --git a/docs/python-sdk/fastmcp-server-middleware.mdx b/docs/python-sdk/fastmcp-server-middleware.mdx new file mode 100644 index 000000000..ec8eab242 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-middleware.mdx @@ -0,0 +1,56 @@ +--- +title: middleware +sidebarTitle: middleware +--- + +# `fastmcp.server.middleware` + +## Functions + +### `make_middleware_wrapper` + +```python +make_middleware_wrapper(middleware: Middleware, call_next: CallNext[T, R]) -> CallNext[T, R] +``` + + +Create a wrapper that applies a single middleware to a context. The +closure bakes in the middleware and call_next function, so it can be +passed to other functions that expect a call_next function. + + +## Classes + +### `CallNext` + +### `CallToolResult` + +### `ListToolsResult` + +### `ListResourcesResult` + +### `ListResourceTemplatesResult` + +### `ListPromptsResult` + +### `ServerResultProtocol` + +### `MiddlewareContext` + + +Unified context for all middleware operations. + + +**Methods:** + +#### `copy` + +```python +copy(self, **kwargs: Any) -> MiddlewareContext[T] +``` + +### `Middleware` + + +Base class for FastMCP middleware with dispatching hooks. + diff --git a/docs/python-sdk/fastmcp-server-openapi.mdx b/docs/python-sdk/fastmcp-server-openapi.mdx new file mode 100644 index 000000000..d2490cea7 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-openapi.mdx @@ -0,0 +1,58 @@ +--- +title: openapi +sidebarTitle: openapi +--- + +# `fastmcp.server.openapi` + + +FastMCP server implementation for OpenAPI integration. + +## Classes + +### `MCPType` + + +Type of FastMCP component to create from a route. + + +### `RouteType` + + +Deprecated: Use MCPType instead. + +This enum is kept for backward compatibility and will be removed in a future version. + + +### `RouteMap` + + +Mapping configuration for HTTP routes to FastMCP component types. + + +### `OpenAPITool` + + +Tool implementation for OpenAPI endpoints. + + +### `OpenAPIResource` + + +Resource implementation for OpenAPI endpoints. + + +### `OpenAPIResourceTemplate` + + +Resource template implementation for OpenAPI endpoints. + + +### `FastMCPOpenAPI` + + +FastMCP server implementation that creates components from an OpenAPI schema. + +This class parses an OpenAPI specification and creates appropriate FastMCP components +(Tools, Resources, ResourceTemplates) based on route mappings. + diff --git a/docs/python-sdk/fastmcp-server-proxy.mdx b/docs/python-sdk/fastmcp-server-proxy.mdx new file mode 100644 index 000000000..bad549605 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-proxy.mdx @@ -0,0 +1,101 @@ +--- +title: proxy +sidebarTitle: proxy +--- + +# `fastmcp.server.proxy` + +## Classes + +### `ProxyToolManager` + + +A ToolManager that sources its tools from a remote client in addition to local and mounted tools. + + +### `ProxyResourceManager` + + +A ResourceManager that sources its resources from a remote client in addition to local and mounted resources. + + +### `ProxyPromptManager` + + +A PromptManager that sources its prompts from a remote client in addition to local and mounted prompts. + + +### `ProxyTool` + + +A Tool that represents and executes a tool on a remote server. + + +**Methods:** + +#### `from_mcp_tool` + +```python +from_mcp_tool(cls, client: Client, mcp_tool: mcp.types.Tool) -> ProxyTool +``` + +Factory method to create a ProxyTool from a raw MCP tool schema. + + +### `ProxyResource` + + +A Resource that represents and reads a resource from a remote server. + + +**Methods:** + +#### `from_mcp_resource` + +```python +from_mcp_resource(cls, client: Client, mcp_resource: mcp.types.Resource) -> ProxyResource +``` + +Factory method to create a ProxyResource from a raw MCP resource schema. + + +### `ProxyTemplate` + + +A ResourceTemplate that represents and creates resources from a remote server template. + + +**Methods:** + +#### `from_mcp_template` + +```python +from_mcp_template(cls, client: Client, mcp_template: mcp.types.ResourceTemplate) -> ProxyTemplate +``` + +Factory method to create a ProxyTemplate from a raw MCP template schema. + + +### `ProxyPrompt` + + +A Prompt that represents and renders a prompt from a remote server. + + +**Methods:** + +#### `from_mcp_prompt` + +```python +from_mcp_prompt(cls, client: Client, mcp_prompt: mcp.types.Prompt) -> ProxyPrompt +``` + +Factory method to create a ProxyPrompt from a raw MCP prompt schema. + + +### `FastMCPProxy` + + +A FastMCP server that acts as a proxy to a remote MCP-compliant server. +It uses specialized managers that fulfill requests via an HTTP client. + diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx new file mode 100644 index 000000000..8f631c002 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-server.mdx @@ -0,0 +1,542 @@ +--- +title: server +sidebarTitle: server +--- + +# `fastmcp.server.server` + + +FastMCP - A more ergonomic interface for MCP servers. + +## Functions + +### `add_resource_prefix` + +```python +add_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> str +``` + + +Add a prefix to a resource URI. + + Args: + uri: The original resource URI + prefix: The prefix to add + + Returns: + The resource URI with the prefix added + + Examples: + >>> add_resource_prefix("resource://path/to/resource", "prefix") + "resource://prefix/path/to/resource" # with new style + >>> add_resource_prefix("resource://path/to/resource", "prefix") + "prefix+resource://path/to/resource" # with legacy style + >>> add_resource_prefix("resource:///absolute/path", "prefix") + "resource://prefix//absolute/path" # with new style + + Raises: + ValueError: If the URI doesn't match the expected protocol://path format + + +### `remove_resource_prefix` + +```python +remove_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> str +``` + + +Remove a prefix from a resource URI. + + Args: + uri: The resource URI with a prefix + prefix: The prefix to remove + prefix_format: The format of the prefix to remove + Returns: + The resource URI with the prefix removed + + Examples: + >>> remove_resource_prefix("resource://prefix/path/to/resource", "prefix") + "resource://path/to/resource" # with new style + >>> remove_resource_prefix("prefix+resource://path/to/resource", "prefix") + "resource://path/to/resource" # with legacy style + >>> remove_resource_prefix("resource://prefix//absolute/path", "prefix") + "resource:///absolute/path" # with new style + + Raises: + ValueError: If the URI doesn't match the expected protocol://path format + + +### `has_resource_prefix` + +```python +has_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> bool +``` + + +Check if a resource URI has a specific prefix. + + Args: + uri: The resource URI to check + prefix: The prefix to look for + + Returns: + True if the URI has the specified prefix, False otherwise + + Examples: + >>> has_resource_prefix("resource://prefix/path/to/resource", "prefix") + True # with new style + >>> has_resource_prefix("prefix+resource://path/to/resource", "prefix") + True # with legacy style + >>> has_resource_prefix("resource://other/path/to/resource", "prefix") + False + + Raises: + ValueError: If the URI doesn't match the expected protocol://path format + + +## Classes + +### `FastMCP` + +**Methods:** + +#### `settings` + +```python +settings(self) -> Settings +``` + +#### `name` + +```python +name(self) -> str +``` + +#### `instructions` + +```python +instructions(self) -> str | None +``` + +#### `run` + +```python +run(self, transport: Literal['stdio', 'streamable-http', 'sse'] | None = None, **transport_kwargs: Any) -> None +``` + +Run the FastMCP server. Note this is a synchronous function. + +**Args:** +- `transport`: Transport protocol to use ("stdio", "sse", or "streamable-http") + + +#### `add_middleware` + +```python +add_middleware(self, middleware: Middleware) -> None +``` + +#### `custom_route` + +```python +custom_route(self, path: str, methods: list[str], name: str | None = None, include_in_schema: bool = True) +``` + +Decorator to register a custom HTTP route on the FastMCP server. + +Allows adding arbitrary HTTP endpoints outside the standard MCP protocol, +which can be useful for OAuth callbacks, health checks, or admin APIs. +The handler function must be an async function that accepts a Starlette +Request and returns a Response. + +**Args:** +- `path`: URL path for the route (e.g., "/oauth/callback") +- `methods`: List of HTTP methods to support (e.g., ["GET", "POST"]) +- `name`: Optional name for the route (to reference this route with +Starlette's reverse URL lookup feature) +- `include_in_schema`: Whether to include in OpenAPI schema, defaults to True + + +#### `add_tool` + +```python +add_tool(self, tool: Tool) -> None +``` + +Add a tool to the server. + +The tool function can optionally request a Context object by adding a parameter +with the Context type annotation. See the @tool decorator for examples. + +**Args:** +- `tool`: The Tool instance to register + + +#### `remove_tool` + +```python +remove_tool(self, name: str) -> None +``` + +Remove a tool from the server. + +**Args:** +- `name`: The name of the tool to remove + +**Raises:** +- `NotFoundError`: If the tool is not found + + +#### `tool` + +```python +tool(self, name_or_fn: AnyFunction) -> FunctionTool +``` + +#### `tool` + +```python +tool(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool] +``` + +#### `tool` + +```python +tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool +``` + +Decorator to register a tool. + +Tools can optionally request a Context object by adding a parameter with the +Context type annotation. The context provides access to MCP capabilities like +logging, progress reporting, and resource access. + +This decorator supports multiple calling patterns: +- @server.tool (without parentheses) +- @server.tool (with empty parentheses) +- @server.tool("custom_name") (with name as first argument) +- @server.tool(name="custom_name") (with name as keyword argument) +- server.tool(function, name="custom_name") (direct function call) + +**Args:** +- `name_or_fn`: Either a function (when used as @tool), a string name, or None +- `name`: Optional name for the tool (keyword-only, alternative to name_or_fn) +- `description`: Optional description of what the tool does +- `tags`: Optional set of tags for categorizing the tool +- `annotations`: Optional annotations about the tool's behavior (e.g. {"is_async": True}) +- `exclude_args`: Optional list of argument names to exclude from the tool schema +- `enabled`: Optional boolean to enable or disable the tool + + +#### `add_resource` + +```python +add_resource(self, resource: Resource) -> None +``` + +Add a resource to the server. + +**Args:** +- `resource`: A Resource instance to add + + +#### `add_template` + +```python +add_template(self, template: ResourceTemplate) -> None +``` + +Add a resource template to the server. + +**Args:** +- `template`: A ResourceTemplate instance to add + + +#### `add_resource_fn` + +```python +add_resource_fn(self, fn: AnyFunction, uri: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> None +``` + +Add a resource or template to the server from a function. + +If the URI contains parameters (e.g. "resource://{param}") or the function +has parameters, it will be registered as a template resource. + +**Args:** +- `fn`: The function to register as a resource +- `uri`: The URI for the resource +- `name`: Optional name for the resource +- `description`: Optional description of the resource +- `mime_type`: Optional MIME type for the resource +- `tags`: Optional set of tags for categorizing the resource + + +#### `resource` + +```python +resource(self, uri: str) -> Callable[[AnyFunction], Resource | ResourceTemplate] +``` + +Decorator to register a function as a resource. + +The function will be called when the resource is read to generate its content. +The function can return: +- str for text content +- bytes for binary content +- other types will be converted to JSON + +Resources can optionally request a Context object by adding a parameter with the +Context type annotation. The context provides access to MCP capabilities like +logging, progress reporting, and session information. + +If the URI contains parameters (e.g. "resource://{param}") or the function +has parameters, it will be registered as a template resource. + +**Args:** +- `uri`: URI for the resource (e.g. "resource://my-resource" or "resource://{param}") +- `name`: Optional name for the resource +- `description`: Optional description of the resource +- `mime_type`: Optional MIME type for the resource +- `tags`: Optional set of tags for categorizing the resource +- `enabled`: Optional boolean to enable or disable the resource + + +#### `add_prompt` + +```python +add_prompt(self, prompt: Prompt) -> None +``` + +Add a prompt to the server. + +**Args:** +- `prompt`: A Prompt instance to add + + +#### `prompt` + +```python +prompt(self, name_or_fn: AnyFunction) -> FunctionPrompt +``` + +#### `prompt` + +```python +prompt(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionPrompt] +``` + +#### `prompt` + +```python +prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt +``` + +Decorator to register a prompt. + + Prompts can optionally request a Context object by adding a parameter with the + Context type annotation. The context provides access to MCP capabilities like + logging, progress reporting, and session information. + + This decorator supports multiple calling patterns: + - @server.prompt (without parentheses) + - @server.prompt() (with empty parentheses) + - @server.prompt("custom_name") (with name as first argument) + - @server.prompt(name="custom_name") (with name as keyword argument) + - server.prompt(function, name="custom_name") (direct function call) + + Args: + name_or_fn: Either a function (when used as @prompt), a string name, or None + name: Optional name for the prompt (keyword-only, alternative to name_or_fn) + description: Optional description of what the prompt does + tags: Optional set of tags for categorizing the prompt + enabled: Optional boolean to enable or disable the prompt + + Example: + @server.prompt + def analyze_table(table_name: str) -> list\[Message]: + schema = read_table_schema(table_name) + return [ + { + "role": "user", + "content": f"Analyze this schema: +{schema}" + } + ] + + @server.prompt() + def analyze_with_context(table_name: str, ctx: Context) -> list\[Message]: + ctx.info(f"Analyzing table {table_name}") + schema = read_table_schema(table_name) + return [ + { + "role": "user", + "content": f"Analyze this schema: +{schema}" + } + ] + + @server.prompt("custom_name") + def analyze_file(path: str) -> list\[Message]: + content = await read_file(path) + return [ + { + "role": "user", + "content": { + "type": "resource", + "resource": { + "uri": f"file://{path}", + "text": content + } + } + } + ] + + @server.prompt(name="custom_name") + def another_prompt(data: str) -> list\[Message]: + return [{"role": "user", "content": data}] + + # Direct function call + server.prompt(my_function, name="custom_name") + + +#### `sse_app` + +```python +sse_app(self, path: str | None = None, message_path: str | None = None, middleware: list[ASGIMiddleware] | None = None) -> StarletteWithLifespan +``` + +Create a Starlette app for the SSE server. + +**Args:** +- `path`: The path to the SSE endpoint +- `message_path`: The path to the message endpoint +- `middleware`: A list of middleware to apply to the app + + +#### `streamable_http_app` + +```python +streamable_http_app(self, path: str | None = None, middleware: list[ASGIMiddleware] | None = None) -> StarletteWithLifespan +``` + +Create a Starlette app for the StreamableHTTP server. + +**Args:** +- `path`: The path to the StreamableHTTP endpoint +- `middleware`: A list of middleware to apply to the app + + +#### `http_app` + +```python +http_app(self, path: str | None = None, middleware: list[ASGIMiddleware] | None = None, json_response: bool | None = None, stateless_http: bool | None = None, transport: Literal['streamable-http', 'sse'] = 'streamable-http') -> StarletteWithLifespan +``` + +Create a Starlette app using the specified HTTP transport. + +**Args:** +- `path`: The path for the HTTP endpoint +- `middleware`: A list of middleware to apply to the app +- `transport`: Transport protocol to use - either "streamable-http" (default) or "sse" + +**Returns:** +- A Starlette application configured with the specified transport + + +#### `mount` + +```python +mount(self, server: FastMCP[LifespanResultT], prefix: str | None = None, as_proxy: bool | None = None) -> None +``` + +Mount another FastMCP server on this server with an optional prefix. + +Unlike importing (with import_server), mounting establishes a dynamic connection +between servers. When a client interacts with a mounted server's objects through +the parent server, requests are forwarded to the mounted server in real-time. +This means changes to the mounted server are immediately reflected when accessed +through the parent. + +When a server is mounted with a prefix: +- Tools from the mounted server are accessible with prefixed names. + Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather". +- Resources are accessible with prefixed URIs. + Example: If server has a resource with URI "weather://forecast", it will be available as + "weather://prefix/forecast". +- Templates are accessible with prefixed URI templates. + Example: If server has a template with URI "weather://location/{id}", it will be available + as "weather://prefix/location/{id}". +- Prompts are accessible with prefixed names. + Example: If server has a prompt named "weather_prompt", it will be available as + "prefix_weather_prompt". + +When a server is mounted without a prefix (prefix=None), its tools, resources, templates, +and prompts are accessible with their original names. Multiple servers can be mounted +without prefixes, and they will be tried in order until a match is found. + +There are two modes for mounting servers: +1. Direct mounting (default when server has no custom lifespan): The parent server + directly accesses the mounted server's objects in-memory for better performance. + In this mode, no client lifecycle events occur on the mounted server, including + lifespan execution. + +2. Proxy mounting (default when server has a custom lifespan): The parent server + treats the mounted server as a separate entity and communicates with it via a + Client transport. This preserves all client-facing behaviors, including lifespan + execution, but with slightly higher overhead. + +**Args:** +- `server`: The FastMCP server to mount. +- `prefix`: Optional prefix to use for the mounted server's objects. If None, +the server's objects are accessible with their original names. +- `as_proxy`: Whether to treat the mounted server as a proxy. If None (default), +automatically determined based on whether the server has a custom lifespan +(True if it has a custom lifespan, False otherwise). +- `tool_separator`: Deprecated. Separator character for tool names. +- `resource_separator`: Deprecated. Separator character for resource URIs. +- `prompt_separator`: Deprecated. Separator character for prompt names. + + +#### `from_openapi` + +```python +from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, **settings: Any) -> FastMCPOpenAPI +``` + +Create a FastMCP server from an OpenAPI specification. + + +#### `from_fastapi` + +```python +from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> FastMCPOpenAPI +``` + +Create a FastMCP server from a FastAPI application. + + +#### `as_proxy` + +```python +as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy +``` + +Create a FastMCP proxy server for the given backend. + +The ``backend`` argument can be either an existing :class:`~fastmcp.client.Client` +instance or any value accepted as the ``transport`` argument of +:class:`~fastmcp.client.Client`. This mirrors the convenience of the +``Client`` constructor. + + +#### `from_client` + +```python +from_client(cls, client: Client[ClientTransportT], **settings: Any) -> FastMCPProxy +``` + +Create a FastMCP proxy server from a FastMCP client. + + +### `MountedServer` diff --git a/docs/python-sdk/fastmcp-settings.mdx b/docs/python-sdk/fastmcp-settings.mdx new file mode 100644 index 000000000..fd3e3d791 --- /dev/null +++ b/docs/python-sdk/fastmcp-settings.mdx @@ -0,0 +1,59 @@ +--- +title: settings +sidebarTitle: settings +--- + +# `fastmcp.settings` + +## Classes + +### `ExtendedEnvSettingsSource` + + +A special EnvSettingsSource that allows for multiple env var prefixes to be used. + +Raises a deprecation warning if the old `FASTMCP_SERVER_` prefix is used. + + +**Methods:** + +#### `get_field_value` + +```python +get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool] +``` + +### `ExtendedSettingsConfigDict` + +### `Settings` + + +FastMCP settings. + + +**Methods:** + +#### `settings_customise_sources` + +```python +settings_customise_sources(cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource) -> tuple[PydanticBaseSettingsSource, ...] +``` + +#### `settings` + +```python +settings(self) -> Self +``` + +This property is for backwards compatibility with FastMCP < 2.8.0, +which accessed fastmcp.settings.settings + + +#### `setup_logging` + +```python +setup_logging(self) -> Self +``` + +Finalize the settings. + diff --git a/docs/python-sdk/fastmcp-tools-__init__.mdx b/docs/python-sdk/fastmcp-tools-__init__.mdx new file mode 100644 index 000000000..5b7c8b04d --- /dev/null +++ b/docs/python-sdk/fastmcp-tools-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.tools` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-tools-tool.mdx b/docs/python-sdk/fastmcp-tools-tool.mdx new file mode 100644 index 000000000..7cae406aa --- /dev/null +++ b/docs/python-sdk/fastmcp-tools-tool.mdx @@ -0,0 +1,68 @@ +--- +title: tool +sidebarTitle: tool +--- + +# `fastmcp.tools.tool` + +## Functions + +### `default_serializer` + +```python +default_serializer(data: Any) -> str +``` + +## Classes + +### `Tool` + + +Internal tool registration info. + + +**Methods:** + +#### `to_mcp_tool` + +```python +to_mcp_tool(self, **overrides: Any) -> MCPTool +``` + +#### `from_function` + +```python +from_function(fn: Callable[..., Any], name: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, serializer: Callable[[Any], str] | None = None, enabled: bool | None = None) -> FunctionTool +``` + +Create a Tool from a function. + + +#### `from_tool` + +```python +from_tool(cls, tool: Tool, transform_fn: Callable[..., Any] | None = None, name: str | None = None, transform_args: dict[str, ArgTransform] | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, serializer: Callable[[Any], str] | None = None, enabled: bool | None = None) -> TransformedTool +``` + +### `FunctionTool` + +**Methods:** + +#### `from_function` + +```python +from_function(cls, fn: Callable[..., Any], name: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, serializer: Callable[[Any], str] | None = None, enabled: bool | None = None) -> FunctionTool +``` + +Create a Tool from a function. + + +### `ParsedFunction` + +**Methods:** + +#### `from_function` + +```python +from_function(cls, fn: Callable[..., Any], exclude_args: list[str] | None = None, validate: bool = True) -> ParsedFunction +``` diff --git a/docs/python-sdk/fastmcp-tools-tool_manager.mdx b/docs/python-sdk/fastmcp-tools-tool_manager.mdx new file mode 100644 index 000000000..fad031d72 --- /dev/null +++ b/docs/python-sdk/fastmcp-tools-tool_manager.mdx @@ -0,0 +1,58 @@ +--- +title: tool_manager +sidebarTitle: tool_manager +--- + +# `fastmcp.tools.tool_manager` + +## Classes + +### `ToolManager` + + +Manages FastMCP tools. + + +**Methods:** + +#### `mount` + +```python +mount(self, server: MountedServer) -> None +``` + +Adds a mounted server as a source for tools. + + +#### `add_tool_from_fn` + +```python +add_tool_from_fn(self, fn: Callable[..., Any], name: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, serializer: Callable[[Any], str] | None = None, exclude_args: list[str] | None = None) -> Tool +``` + +Add a tool to the server. + + +#### `add_tool` + +```python +add_tool(self, tool: Tool) -> Tool +``` + +Register a tool with the server. + + +#### `remove_tool` + +```python +remove_tool(self, key: str) -> None +``` + +Remove a tool from the server. + +**Args:** +- `key`: The key of the tool to remove + +**Raises:** +- `NotFoundError`: If the tool is not found + diff --git a/docs/python-sdk/fastmcp-tools-tool_transform.mdx b/docs/python-sdk/fastmcp-tools-tool_transform.mdx new file mode 100644 index 000000000..c0d10e34d --- /dev/null +++ b/docs/python-sdk/fastmcp-tools-tool_transform.mdx @@ -0,0 +1,117 @@ +--- +title: tool_transform +sidebarTitle: tool_transform +--- + +# `fastmcp.tools.tool_transform` + +## Classes + +### `ArgTransform` + + +Configuration for transforming a parent tool's argument. + + This class allows fine-grained control over how individual arguments are transformed + when creating a new tool from an existing one. You can rename arguments, change their + descriptions, add default values, or hide them from clients while passing constants. + + Attributes: + name: New name for the argument. Use None to keep original name, or ... for no change. + description: New description for the argument. Use None to remove description, or ... for no change. + default: New default value for the argument. Use ... for no change. + default_factory: Callable that returns a default value. Cannot be used with default. + type: New type for the argument. Use ... for no change. + hide: If True, hide this argument from clients but pass a constant value to parent. + required: If True, make argument required (remove default). Use ... for no change. + examples: Examples for the argument. Use ... for no change. + + Examples: + # Rename argument 'old_name' to 'new_name' + ArgTransform(name="new_name") + + # Change description only + ArgTransform(description="Updated description") + + # Add a default value (makes argument optional) + ArgTransform(default=42) + + # Add a default factory (makes argument optional) + ArgTransform(default_factory=lambda: time.time()) + + # Change the type + ArgTransform(type=str) + + # Hide the argument entirely from clients + ArgTransform(hide=True) + + # Hide argument but pass a constant value to parent + ArgTransform(hide=True, default="constant_value") + + # Hide argument but pass a factory-generated value to parent + ArgTransform(hide=True, default_factory=lambda: uuid.uuid4().hex) + + # Make an optional parameter required (removes any default) + ArgTransform(required=True) + + # Combine multiple transformations + ArgTransform(name="new_name", description="New desc", default=None, type=int) + + +### `TransformedTool` + + +A tool that is transformed from another tool. + +This class represents a tool that has been created by transforming another tool. +It supports argument renaming, schema modification, custom function injection, +and provides context for the forward() and forward_raw() functions. + +The transformation can be purely schema-based (argument renaming, dropping, etc.) +or can include a custom function that uses forward() to call the parent tool +with transformed arguments. + + +**Methods:** + +#### `from_tool` + +```python +from_tool(cls, tool: Tool, name: str | None = None, description: str | None = None, tags: set[str] | None = None, transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | None = None, serializer: Callable[[Any], str] | None = None, enabled: bool | None = None) -> TransformedTool +``` + +Create a transformed tool from a parent tool. + +**Args:** +- `tool`: The parent tool to transform. +- `transform_fn`: Optional custom function. Can use forward() and forward_raw() +to call the parent tool. Functions with **kwargs receive transformed +argument names. +- `name`: New name for the tool. Defaults to parent tool's name. +- `transform_args`: Optional transformations for parent tool arguments. +Only specified arguments are transformed, others pass through unchanged: +- str: Simple rename +- ArgTransform: Complex transformation (rename/description/default/drop) +- None: Drop the argument +- `description`: New description. Defaults to parent's description. +- `tags`: New tags. Defaults to parent's tags. +- `annotations`: New annotations. Defaults to parent's annotations. +- `serializer`: New serializer. Defaults to parent's serializer. + +**Returns:** +- TransformedTool with the specified transformations. + +Examples: +- # Transform specific arguments only +- Tool.from_tool(parent, transform_args={"old": "new"}) # Others unchanged +- # Custom function with partial transforms +- async def custom(x: int, y: int) -> str: +result = await forward(x=x, y=y) +return f"Custom: {result}" +- Tool.from_tool(parent, transform_fn=custom, transform_args={"a": "x", "b": "y"}) +- # Using **kwargs (gets all args, transformed and untransformed) +- async def flexible(**kwargs) -> str: +result = await forward(**kwargs) +return f"Got: {kwargs}" +- Tool.from_tool(parent, transform_fn=flexible, transform_args={"a": "x"}) + diff --git a/docs/python-sdk/fastmcp-utilities-__init__.mdx b/docs/python-sdk/fastmcp-utilities-__init__.mdx new file mode 100644 index 000000000..12e12b4ed --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-__init__.mdx @@ -0,0 +1,9 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.utilities` + + +FastMCP utility modules. diff --git a/docs/python-sdk/fastmcp-utilities-cache.mdx b/docs/python-sdk/fastmcp-utilities-cache.mdx new file mode 100644 index 000000000..ab41395d9 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-cache.mdx @@ -0,0 +1,30 @@ +--- +title: cache +sidebarTitle: cache +--- + +# `fastmcp.utilities.cache` + +## Classes + +### `TimedCache` + +**Methods:** + +#### `set` + +```python +set(self, key: Any, value: Any) -> None +``` + +#### `get` + +```python +get(self, key: Any) -> Any +``` + +#### `clear` + +```python +clear(self) -> None +``` diff --git a/docs/python-sdk/fastmcp-utilities-components.mdx b/docs/python-sdk/fastmcp-utilities-components.mdx new file mode 100644 index 000000000..61434c7d5 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-components.mdx @@ -0,0 +1,52 @@ +--- +title: components +sidebarTitle: components +--- + +# `fastmcp.utilities.components` + +## Classes + +### `FastMCPComponent` + + +Base class for FastMCP tools, prompts, resources, and resource templates. + + +**Methods:** + +#### `key` + +```python +key(self) -> str +``` + +The key of the component. This is used for internal bookkeeping +and may reflect e.g. prefixes or other identifiers. You should not depend on +keys having a certain value, as the same tool loaded from different +hierarchies of servers may have different keys. + + +#### `with_key` + +```python +with_key(self, key: str) -> Self +``` + +#### `enable` + +```python +enable(self) -> None +``` + +Enable the component. + + +#### `disable` + +```python +disable(self) -> None +``` + +Disable the component. + diff --git a/docs/python-sdk/fastmcp-utilities-exceptions.mdx b/docs/python-sdk/fastmcp-utilities-exceptions.mdx new file mode 100644 index 000000000..2d480a146 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-exceptions.mdx @@ -0,0 +1,20 @@ +--- +title: exceptions +sidebarTitle: exceptions +--- + +# `fastmcp.utilities.exceptions` + +## Functions + +### `iter_exc` + +```python +iter_exc(group: BaseExceptionGroup) +``` + +### `get_catch_handlers` + +```python +get_catch_handlers() -> Mapping[type[BaseException] | Iterable[type[BaseException]], Callable[[BaseExceptionGroup[Any]], Any]] +``` diff --git a/docs/python-sdk/fastmcp-utilities-http.mdx b/docs/python-sdk/fastmcp-utilities-http.mdx new file mode 100644 index 000000000..6e5e4b75f --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-http.mdx @@ -0,0 +1,18 @@ +--- +title: http +sidebarTitle: http +--- + +# `fastmcp.utilities.http` + +## Functions + +### `find_available_port` + +```python +find_available_port() -> int +``` + + +Find an available port by letting the OS assign one. + diff --git a/docs/python-sdk/fastmcp-utilities-json_schema.mdx b/docs/python-sdk/fastmcp-utilities-json_schema.mdx new file mode 100644 index 000000000..bedf79119 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-json_schema.mdx @@ -0,0 +1,25 @@ +--- +title: json_schema +sidebarTitle: json_schema +--- + +# `fastmcp.utilities.json_schema` + +## Functions + +### `compress_schema` + +```python +compress_schema(schema: dict, prune_params: list[str] | None = None, prune_defs: bool = True, prune_additional_properties: bool = True, prune_titles: bool = False) -> dict +``` + + +Remove the given parameters from the schema. + +**Args:** +- `schema`: The schema to compress +- `prune_params`: List of parameter names to remove from properties +- `prune_defs`: Whether to remove unused definitions +- `prune_additional_properties`: Whether to remove additionalProperties: false +- `prune_titles`: Whether to remove title fields from the schema + diff --git a/docs/python-sdk/fastmcp-utilities-logging.mdx b/docs/python-sdk/fastmcp-utilities-logging.mdx new file mode 100644 index 000000000..90e294f6a --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-logging.mdx @@ -0,0 +1,41 @@ +--- +title: logging +sidebarTitle: logging +--- + +# `fastmcp.utilities.logging` + + +Logging utilities for FastMCP. + +## Functions + +### `get_logger` + +```python +get_logger(name: str) -> logging.Logger +``` + + +Get a logger nested under FastMCP namespace. + +**Args:** +- `name`: the name of the logger, which will be prefixed with 'FastMCP.' + +**Returns:** +- a configured logger instance + + +### `configure_logging` + +```python +configure_logging(level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | int = 'INFO', logger: logging.Logger | None = None, enable_rich_tracebacks: bool = True) -> None +``` + + +Configure logging for FastMCP. + +**Args:** +- `logger`: the logger to configure +- `level`: the log level to use + diff --git a/docs/python-sdk/fastmcp-utilities-mcp_config.mdx b/docs/python-sdk/fastmcp-utilities-mcp_config.mdx new file mode 100644 index 000000000..b74dfcfa0 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-mcp_config.mdx @@ -0,0 +1,50 @@ +--- +title: mcp_config +sidebarTitle: mcp_config +--- + +# `fastmcp.utilities.mcp_config` + +## Functions + +### `infer_transport_type_from_url` + +```python +infer_transport_type_from_url(url: str | AnyUrl) -> Literal['streamable-http', 'sse'] +``` + + +Infer the appropriate transport type from the given URL. + + +## Classes + +### `StdioMCPServer` + +**Methods:** + +#### `to_transport` + +```python +to_transport(self) -> StdioTransport +``` + +### `RemoteMCPServer` + +**Methods:** + +#### `to_transport` + +```python +to_transport(self) -> StreamableHttpTransport | SSETransport +``` + +### `MCPConfig` + +**Methods:** + +#### `from_dict` + +```python +from_dict(cls, config: dict[str, Any]) -> MCPConfig +``` diff --git a/docs/python-sdk/fastmcp-utilities-openapi.mdx b/docs/python-sdk/fastmcp-utilities-openapi.mdx new file mode 100644 index 000000000..7b7d0aa62 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-openapi.mdx @@ -0,0 +1,118 @@ +--- +title: openapi +sidebarTitle: openapi +--- + +# `fastmcp.utilities.openapi` + +## Functions + +### `parse_openapi_to_http_routes` + +```python +parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute] +``` + + +Parses an OpenAPI schema dictionary into a list of HTTPRoute objects +using the openapi-pydantic library. + +Supports both OpenAPI 3.0.x and 3.1.x versions. + + +### `clean_schema_for_display` + +```python +clean_schema_for_display(schema: JsonSchema | None) -> JsonSchema | None +``` + + +Clean up a schema dictionary for display by removing internal/complex fields. + + +### `generate_example_from_schema` + +```python +generate_example_from_schema(schema: JsonSchema | None) -> Any +``` + + +Generate a simple example value from a JSON schema dictionary. +Very basic implementation focusing on types. + + +### `format_json_for_description` + +```python +format_json_for_description(data: Any, indent: int = 2) -> str +``` + + +Formats Python data as a JSON string block for markdown. + + +### `format_description_with_responses` + +```python +format_description_with_responses(base_description: str, responses: dict[str, Any], parameters: list[ParameterInfo] | None = None, request_body: RequestBodyInfo | None = None) -> str +``` + + +Formats the base description string with response, parameter, and request body information. + +**Args:** +- `base_description`: The initial description to be formatted. +- `responses`: A dictionary of response information, keyed by status code. +- `parameters`: A list of parameter information, +including path and query parameters. Each parameter includes details such as name, +location, whether it is required, and a description. +- `request_body`: Information about the request body, +including its description, whether it is required, and its content schema. + +**Returns:** +- The formatted description string with additional details about responses, parameters, +- and the request body. + + +## Classes + +### `ParameterInfo` + + +Represents a single parameter for an HTTP operation in our IR. + + +### `RequestBodyInfo` + + +Represents the request body for an HTTP operation in our IR. + + +### `ResponseInfo` + + +Represents response information in our IR. + + +### `HTTPRoute` + + +Intermediate Representation for a single OpenAPI operation. + + +### `OpenAPIParser` + + +Unified parser for OpenAPI schemas with generic type parameters to handle both 3.0 and 3.1. + + +**Methods:** + +#### `parse` + +```python +parse(self) -> list[HTTPRoute] +``` + +Parse the OpenAPI schema into HTTP routes. + diff --git a/docs/python-sdk/fastmcp-utilities-types.mdx b/docs/python-sdk/fastmcp-utilities-types.mdx new file mode 100644 index 000000000..3810bb878 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-types.mdx @@ -0,0 +1,112 @@ +--- +title: types +sidebarTitle: types +--- + +# `fastmcp.utilities.types` + + +Common types used across FastMCP. + +## Functions + +### `get_cached_typeadapter` + +```python +get_cached_typeadapter(cls: T) -> TypeAdapter[T] +``` + + +TypeAdapters are heavy objects, and in an application context we'd typically +create them once in a global scope and reuse them as often as possible. +However, this isn't feasible for user-generated functions. Instead, we use a +cache to minimize the cost of creating them as much as possible. + + +### `issubclass_safe` + +```python +issubclass_safe(cls: type, base: type) -> bool +``` + + +Check if cls is a subclass of base, even if cls is a type variable. + + +### `is_class_member_of_type` + +```python +is_class_member_of_type(cls: type, base: type) -> bool +``` + + +Check if cls is a member of base, even if cls is a type variable. + +Base can be a type, a UnionType, or an Annotated type. Generic types are not +considered members (e.g. T is not a member of list\[T]). + + +### `find_kwarg_by_type` + +```python +find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None +``` + + +Find the name of the kwarg that is of type kwarg_type. + +Includes union types that contain the kwarg_type, as well as Annotated types. + + +## Classes + +### `FastMCPBaseModel` + + +Base model for FastMCP models. + + +### `Image` + + +Helper class for returning images from tools. + + +**Methods:** + +#### `to_image_content` + +```python +to_image_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> ImageContent +``` + +Convert to MCP ImageContent. + + +### `Audio` + + +Helper class for returning audio from tools. + + +**Methods:** + +#### `to_audio_content` + +```python +to_audio_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> AudioContent +``` + +### `File` + + +Helper class for returning audio from tools. + + +**Methods:** + +#### `to_resource_content` + +```python +to_resource_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> EmbeddedResource +``` diff --git a/justfile b/justfile index 6d897b2e9..a27c2877f 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,8 @@ +# Build the project build: uv sync +# Run tests test: build uv run --frozen pytest -xvs tests @@ -8,5 +10,18 @@ test: build typecheck: uv run --frozen pyright +# Serve documentation locally docs: - cd docs && npx mintlify dev + cd docs && npx mint@latest dev + +# Generate API reference documentation for all modules +api-ref-all: + uvx --with-editable . --refresh-package mdxify mdxify@latest --all --root-module fastmcp + +# Generate API reference for specific modules (e.g., just api-ref prefect.flows prefect.tasks) +api-ref *MODULES: + uvx --with-editable . --refresh-package mdxify mdxify@latest {{MODULES}} --root-module fastmcp + +# Clean up API reference documentation +api-ref-clean: + rm -rf docs/python-sdk \ No newline at end of file From bbf8165636bcc4d8815bcc53df10ba967c5b40fb Mon Sep 17 00:00:00 2001 From: "Owen W. Taylor" Date: Fri, 20 Jun 2025 14:29:06 -0400 Subject: [PATCH 12/41] openapi: Improve pruning of unused defs Defs that are used only by other unused defs were counted as used. Fix this by tracing what defs use other defs recursively. --- src/fastmcp/utilities/json_schema.py | 90 +++++++++++++++++++++------- tests/utilities/test_json_schema.py | 22 +++++-- 2 files changed, 84 insertions(+), 28 deletions(-) diff --git a/src/fastmcp/utilities/json_schema.py b/src/fastmcp/utilities/json_schema.py index 87ea4a789..ca5fe37c9 100644 --- a/src/fastmcp/utilities/json_schema.py +++ b/src/fastmcp/utilities/json_schema.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +from collections import defaultdict def _prune_param(schema: dict, param: str) -> dict: @@ -24,25 +25,77 @@ def _prune_param(schema: dict, param: str) -> dict: return schema +def _prune_unused_defs(schema: dict) -> dict: + """Walk the schema and prune unused defs.""" + + root_defs: set[str] = set() + referenced_by: defaultdict[str, list] = defaultdict(list) + + defs = schema.get("$defs") + if defs is None: + return schema + + def walk( + node: object, current_def: str | None = None, skip_defs: bool = False + ) -> None: + if isinstance(node, dict): + # Process $ref for definition tracking + ref = node.get("$ref") + if isinstance(ref, str) and ref.startswith("#/$defs/"): + def_name = ref.split("/")[-1] + if current_def: + referenced_by[def_name].append(current_def) + else: + root_defs.add(def_name) + + # Walk children + for k, v in node.items(): + if skip_defs and k == "$defs": + continue + + walk(v, current_def=current_def) + + elif isinstance(node, list): + for v in node: + walk(v) + + # Traverse the schema once, skipping the $defs + walk(schema, skip_defs=True) + + # Now figure out what defs reference other defs + for def_name, value in defs.items(): + walk(value, current_def=def_name) + + # Figure out what defs were referenced directly or recursively + def def_is_referenced(def_name): + if def_name in root_defs: + return True + references = referenced_by.get(def_name) + if references: + for reference in references: + if def_is_referenced(reference): + return True + return False + + # Remove orphaned definitions if requested + for def_name in list(defs): + if not def_is_referenced(def_name): + defs.pop(def_name) + if not defs: + schema.pop("$defs", None) + + return schema + + def _walk_and_prune( schema: dict, - prune_defs: bool = False, prune_titles: bool = False, prune_additional_properties: bool = False, ) -> dict: - """Walk the schema and optionally prune titles, unused definitions, and additionalProperties: false.""" - - # Will only be used if prune_defs is True - used_defs: set[str] = set() + """Walk the schema and optionally prune titles and additionalProperties: false.""" def walk(node: object) -> None: if isinstance(node, dict): - # Process $ref for definition tracking - if prune_defs: - ref = node.get("$ref") - if isinstance(ref, str) and ref.startswith("#/$defs/"): - used_defs.add(ref.split("/")[-1]) - # Remove title if requested if prune_titles and "title" in node: node.pop("title") @@ -62,18 +115,8 @@ def _walk_and_prune( for v in node: walk(v) - # Traverse the schema once walk(schema) - # Remove orphaned definitions if requested - if prune_defs: - defs = schema.get("$defs", {}) - for def_name in list(defs): - if def_name not in used_defs: - defs.pop(def_name) - if not defs: - schema.pop("$defs", None) - return schema @@ -109,12 +152,13 @@ def compress_schema( schema = _prune_param(schema, param=param) # Do a single walk to handle pruning operations - if prune_defs or prune_titles or prune_additional_properties: + if prune_titles or prune_additional_properties: schema = _walk_and_prune( schema, - prune_defs=prune_defs, prune_titles=prune_titles, prune_additional_properties=prune_additional_properties, ) + if prune_defs: + schema = _prune_unused_defs(schema) return schema diff --git a/tests/utilities/test_json_schema.py b/tests/utilities/test_json_schema.py index a1b5f1584..55c970224 100644 --- a/tests/utilities/test_json_schema.py +++ b/tests/utilities/test_json_schema.py @@ -1,14 +1,11 @@ from fastmcp.utilities.json_schema import ( _prune_param, + _prune_unused_defs, _walk_and_prune, compress_schema, ) - -# Create wrappers for backward compatibility with tests -def _prune_unused_defs(schema): - """Wrapper for _walk_and_prune that only prunes definitions.""" - return _walk_and_prune(schema, prune_defs=True) +# Wrapper for backward compatibility with tests def _prune_additional_properties(schema): @@ -95,6 +92,21 @@ class TestPruneUnusedDefs: assert "nested_def" in result["$defs"] assert "unused_def" not in result["$defs"] + def test_nested_references_removed(self): + """Test that definitions referenced via nesting in unused defs are removed.""" + schema = { + "properties": {}, + "$defs": { + "foo_def": { + "type": "object", + "properties": {"nested": {"$ref": "#/$defs/nested_def"}}, + }, + "nested_def": {"type": "string"}, + }, + } + result = _prune_unused_defs(schema) + assert "$defs" not in result + def test_array_references_kept(self): """Test that definitions referenced in array items are kept.""" schema = { From 14d1b8a94d8d56ff66c80e07cfddf41b28c35f35 Mon Sep 17 00:00:00 2001 From: "Owen W. Taylor" Date: Fri, 20 Jun 2025 14:30:55 -0400 Subject: [PATCH 13/41] openapi: Rewrite recursive #/components/schemas/ references When a schema referenced another schema as #/components/schemas/... that wasn't properly rewritten into #/$defs/.. --- src/fastmcp/utilities/openapi.py | 8 ++++--- .../openapi/test_openapi_advanced.py | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/fastmcp/utilities/openapi.py b/src/fastmcp/utilities/openapi.py index 44c0216f6..0cae85cb2 100644 --- a/src/fastmcp/utilities/openapi.py +++ b/src/fastmcp/utilities/openapi.py @@ -262,16 +262,18 @@ class OpenAPIParser( if isinstance(resolved_schema, (self.schema_cls)): # Convert schema to dictionary - return resolved_schema.model_dump( + result = resolved_schema.model_dump( mode="json", by_alias=True, exclude_none=True ) elif isinstance(resolved_schema, dict): - return resolved_schema + result = resolved_schema else: logger.warning( f"Expected Schema after resolving, got {type(resolved_schema)}. Returning empty dict." ) - return {} + result = {} + + return _replace_ref_with_defs(result) except Exception as e: logger.error(f"Failed to extract schema as dict: {e}", exc_info=False) return {} diff --git a/tests/utilities/openapi/test_openapi_advanced.py b/tests/utilities/openapi/test_openapi_advanced.py index 6b7ec8af3..979ca9b28 100644 --- a/tests/utilities/openapi/test_openapi_advanced.py +++ b/tests/utilities/openapi/test_openapi_advanced.py @@ -294,6 +294,28 @@ def test_complex_schema_route_count(parsed_complex_routes): assert len(parsed_complex_routes) == 3 +def test_complex_schema_ref_rewriting(parsed_complex_routes): + """Test that all #/components references have been rewritten.""" + + def no_components(value): + if isinstance(value, dict): + for k, v in value.items(): + if k == "$ref": + assert not v.startswith("#/components/"), ( + f"reference '{v}' was not rewritten" + ) + else: + no_components(v) + elif isinstance(value, list): + for v in value: + no_components(v) + + for route in parsed_complex_routes: + no_components(route.schema_definitions) + for param in route.parameters: + no_components(param.schema_) + + def test_complex_schema_list_users_query_param_limit(complex_route_map): """Test that a reference to a limit query parameter is correctly resolved.""" list_users = complex_route_map["listUsers"] From a7e3ae47bfdd88be0de4836cc5aa3b1575c47f7b Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Fri, 20 Jun 2025 17:07:19 -0500 Subject: [PATCH 14/41] fix rendering for some pages --- docs/docs.json | 8 ++++++++ docs/python-sdk/fastmcp-client-auth-oauth.mdx | 2 +- docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx | 4 ++-- justfile | 4 ++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/docs.json b/docs/docs.json index 0c0677a67..2f5933cc1 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -177,9 +177,11 @@ { "group": "fastmcp.client", "pages": [ + "python-sdk/fastmcp-client-__init__", { "group": "auth", "pages": [ + "python-sdk/fastmcp-client-auth-__init__", "python-sdk/fastmcp-client-auth-bearer", "python-sdk/fastmcp-client-auth-oauth" ] @@ -196,6 +198,7 @@ { "group": "fastmcp.prompts", "pages": [ + "python-sdk/fastmcp-prompts-__init__", "python-sdk/fastmcp-prompts-prompt", "python-sdk/fastmcp-prompts-prompt_manager" ] @@ -203,6 +206,7 @@ { "group": "fastmcp.resources", "pages": [ + "python-sdk/fastmcp-resources-__init__", "python-sdk/fastmcp-resources-resource", "python-sdk/fastmcp-resources-resource_manager", "python-sdk/fastmcp-resources-template", @@ -212,13 +216,16 @@ { "group": "fastmcp.server", "pages": [ + "python-sdk/fastmcp-server-__init__", { "group": "auth", "pages": [ + "python-sdk/fastmcp-server-auth-__init__", "python-sdk/fastmcp-server-auth-auth", { "group": "providers", "pages": [ + "python-sdk/fastmcp-server-auth-providers-__init__", "python-sdk/fastmcp-server-auth-providers-bearer", "python-sdk/fastmcp-server-auth-providers-bearer_env", "python-sdk/fastmcp-server-auth-providers-in_memory" @@ -238,6 +245,7 @@ { "group": "fastmcp.tools", "pages": [ + "python-sdk/fastmcp-tools-__init__", "python-sdk/fastmcp-tools-tool", "python-sdk/fastmcp-tools-tool_manager", "python-sdk/fastmcp-tools-tool_transform" diff --git a/docs/python-sdk/fastmcp-client-auth-oauth.mdx b/docs/python-sdk/fastmcp-client-auth-oauth.mdx index d299c1a95..b8b3e64ad 100644 --- a/docs/python-sdk/fastmcp-client-auth-oauth.mdx +++ b/docs/python-sdk/fastmcp-client-auth-oauth.mdx @@ -27,7 +27,7 @@ httpx.AsyncClient (or appropriate FastMCP client/transport instance) **Args:** - `mcp_url`: Full URL to the MCP endpoint (e.g., -- `"http`: //host/mcp/sse") +- `"http`: //host/mcp/sse/") - `scopes`: OAuth scopes to request. Can be a - `client_name`: Name for this client during registration - `token_storage_cache_dir`: Directory for FileTokenStorage diff --git a/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx b/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx index d619de127..5e85ee1e9 100644 --- a/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx +++ b/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx @@ -38,7 +38,7 @@ Generate an RSA key pair for testing. #### `create_token` ```python -create_token(self, subject: str = 'fastmcp-user', issuer: str = 'https://fastmcp.example.com', audience: str | None = None, scopes: list[str] | None = None, expires_in_seconds: int = 3600, additional_claims: dict[str, Any] | None = None, kid: str | None = None) -> str +create_token(self, subject: str = 'fastmcp-user', issuer: str = 'https://fastmcp.example.com', audience: str | list[str] | None = None, scopes: list[str] | None = None, expires_in_seconds: int = 3600, additional_claims: dict[str, Any] | None = None, kid: str | None = None) -> str ``` Generate a test JWT token for testing purposes. @@ -47,7 +47,7 @@ Generate a test JWT token for testing purposes. - `private_key_pem`: RSA private key in PEM format - `subject`: Subject claim (usually user ID) - `issuer`: Issuer claim -- `audience`: Audience claim (optional) +- `audience`: Audience claim - can be a string or list of strings (optional) - `scopes`: List of scopes to include - `expires_in_seconds`: Token expiration time in seconds - `additional_claims`: Any additional claims to include diff --git a/justfile b/justfile index a27c2877f..94d7a37b0 100644 --- a/justfile +++ b/justfile @@ -16,11 +16,11 @@ docs: # Generate API reference documentation for all modules api-ref-all: - uvx --with-editable . --refresh-package mdxify mdxify@latest --all --root-module fastmcp + uv run --with-editable . --with git+https://github.com/zzstoatzz/mdxify.git@fix-navigation-structure mdxify --all --root-module fastmcp --anchor-name "SDK Reference" # Generate API reference for specific modules (e.g., just api-ref prefect.flows prefect.tasks) api-ref *MODULES: - uvx --with-editable . --refresh-package mdxify mdxify@latest {{MODULES}} --root-module fastmcp + uv run --with-editable . --with git+https://github.com/zzstoatzz/mdxify.git@fix-navigation-structure mdxify {{MODULES}} --root-module fastmcp --anchor-name "SDK Reference" # Clean up API reference documentation api-ref-clean: From 006341871eb78b0f223b2f5122391246d3945345 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Fri, 20 Jun 2025 17:08:24 -0500 Subject: [PATCH 15/41] ope --- justfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 94d7a37b0..9d918cb9d 100644 --- a/justfile +++ b/justfile @@ -16,11 +16,11 @@ docs: # Generate API reference documentation for all modules api-ref-all: - uv run --with-editable . --with git+https://github.com/zzstoatzz/mdxify.git@fix-navigation-structure mdxify --all --root-module fastmcp --anchor-name "SDK Reference" + uv run --with-editable . --with mdxify@latest mdxify --all --root-module fastmcp --anchor-name "SDK Reference" # Generate API reference for specific modules (e.g., just api-ref prefect.flows prefect.tasks) api-ref *MODULES: - uv run --with-editable . --with git+https://github.com/zzstoatzz/mdxify.git@fix-navigation-structure mdxify {{MODULES}} --root-module fastmcp --anchor-name "SDK Reference" + uv run --with-editable . --with mdxify@latest mdxify {{MODULES}} --root-module fastmcp --anchor-name "SDK Reference" # Clean up API reference documentation api-ref-clean: From 094d2593894426d7d24ae7445938c6ab88f1b125 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Fri, 20 Jun 2025 17:09:30 -0500 Subject: [PATCH 16/41] remove xtra newline --- src/fastmcp/client/auth/oauth.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fastmcp/client/auth/oauth.py b/src/fastmcp/client/auth/oauth.py index ee0e29d77..b858cc17d 100644 --- a/src/fastmcp/client/auth/oauth.py +++ b/src/fastmcp/client/auth/oauth.py @@ -306,8 +306,7 @@ def OAuth( httpx.AsyncClient (or appropriate FastMCP client/transport instance) Args: - mcp_url: Full URL to the MCP endpoint (e.g., - "http://host/mcp/sse/") + mcp_url: Full URL to the MCP endpoint (e.g. "http://host/mcp/sse/") scopes: OAuth scopes to request. Can be a space-separated string or a list of strings. client_name: Name for this client during registration From 98a91cbd9e0238b2e1be9e472813113975af3aa5 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Fri, 20 Jun 2025 17:15:33 -0500 Subject: [PATCH 17/41] fix escaping problem --- docs/python-sdk/fastmcp-cli-claude.mdx | 2 +- docs/python-sdk/fastmcp-cli-run.mdx | 6 +++--- docs/python-sdk/fastmcp-client-auth-oauth.mdx | 3 +-- docs/python-sdk/fastmcp-server-server.mdx | 4 ++-- docs/python-sdk/fastmcp-tools-tool_transform.mdx | 8 ++++---- docs/python-sdk/fastmcp-utilities-json_schema.mdx | 2 +- justfile | 4 ++-- 7 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/python-sdk/fastmcp-cli-claude.mdx b/docs/python-sdk/fastmcp-cli-claude.mdx index 13fe7d600..6ea44b33e 100644 --- a/docs/python-sdk/fastmcp-cli-claude.mdx +++ b/docs/python-sdk/fastmcp-cli-claude.mdx @@ -30,7 +30,7 @@ update_claude_config(file_spec: str, server_name: str) -> bool Add or update a FastMCP server in Claude's configuration. **Args:** -- `file_spec`: Path to the server file, optionally with :object suffix +- `file_spec`: Path to the server file, optionally with \:object suffix - `server_name`: Name for the server in Claude's config - `with_editable`: Optional directory to install in editable mode - `with_packages`: Optional list of additional packages to install diff --git a/docs/python-sdk/fastmcp-cli-run.mdx b/docs/python-sdk/fastmcp-cli-run.mdx index bdb07beac..7505c7fb4 100644 --- a/docs/python-sdk/fastmcp-cli-run.mdx +++ b/docs/python-sdk/fastmcp-cli-run.mdx @@ -30,7 +30,7 @@ parse_file_path(server_spec: str) -> tuple[Path, str | None] Parse a file path that may include a server object specification. **Args:** -- `server_spec`: Path to file, optionally with :object suffix +- `server_spec`: Path to file, optionally with \:object suffix **Returns:** - Tuple of (file_path, server_object) @@ -47,7 +47,7 @@ Import a MCP server from a file. **Args:** - `file`: Path to the file -- `server_object`: Optional object name in format "module:object" or just "object" +- `server_object`: Optional object name in format "module\:object" or just "object" **Returns:** - The server object @@ -97,7 +97,7 @@ run_command(server_spec: str, transport: str | None = None, host: str | None = N Run a MCP server or connect to a remote one. **Args:** -- `server_spec`: Python file, object specification (file:obj), or URL +- `server_spec`: Python file, object specification (file\:obj), or URL - `transport`: Transport protocol to use - `host`: Host to bind to when using http transport - `port`: Port to bind to when using http transport diff --git a/docs/python-sdk/fastmcp-client-auth-oauth.mdx b/docs/python-sdk/fastmcp-client-auth-oauth.mdx index b8b3e64ad..f10afba36 100644 --- a/docs/python-sdk/fastmcp-client-auth-oauth.mdx +++ b/docs/python-sdk/fastmcp-client-auth-oauth.mdx @@ -26,8 +26,7 @@ This is intended to be provided to the `auth` parameter of an httpx.AsyncClient (or appropriate FastMCP client/transport instance) **Args:** -- `mcp_url`: Full URL to the MCP endpoint (e.g., -- `"http`: //host/mcp/sse/") +- `mcp_url`: Full URL to the MCP endpoint (e.g. "http\://host/mcp/sse/") - `scopes`: OAuth scopes to request. Can be a - `client_name`: Name for this client during registration - `token_storage_cache_dir`: Directory for FileTokenStorage diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx index 8f631c002..2b3c1ed83 100644 --- a/docs/python-sdk/fastmcp-server-server.mdx +++ b/docs/python-sdk/fastmcp-server-server.mdx @@ -223,7 +223,7 @@ This decorator supports multiple calling patterns: - `name`: Optional name for the tool (keyword-only, alternative to name_or_fn) - `description`: Optional description of what the tool does - `tags`: Optional set of tags for categorizing the tool -- `annotations`: Optional annotations about the tool's behavior (e.g. {"is_async": True}) +- `annotations`: Optional annotations about the tool's behavior (e.g. {"is_async"\: True}) - `exclude_args`: Optional list of argument names to exclude from the tool schema - `enabled`: Optional boolean to enable or disable the tool @@ -294,7 +294,7 @@ If the URI contains parameters (e.g. "resource://{param}") or the function has parameters, it will be registered as a template resource. **Args:** -- `uri`: URI for the resource (e.g. "resource://my-resource" or "resource://{param}") +- `uri`: URI for the resource (e.g. "resource\://my-resource" or "resource\://{param}") - `name`: Optional name for the resource - `description`: Optional description of the resource - `mime_type`: Optional MIME type for the resource diff --git a/docs/python-sdk/fastmcp-tools-tool_transform.mdx b/docs/python-sdk/fastmcp-tools-tool_transform.mdx index c0d10e34d..abee7d5eb 100644 --- a/docs/python-sdk/fastmcp-tools-tool_transform.mdx +++ b/docs/python-sdk/fastmcp-tools-tool_transform.mdx @@ -89,10 +89,10 @@ to call the parent tool. Functions with **kwargs receive transformed argument names. - `name`: New name for the tool. Defaults to parent tool's name. - `transform_args`: Optional transformations for parent tool arguments. -Only specified arguments are transformed, others pass through unchanged: -- str: Simple rename -- ArgTransform: Complex transformation (rename/description/default/drop) -- None: Drop the argument +Only specified arguments are transformed, others pass through unchanged\: +- str\: Simple rename +- ArgTransform\: Complex transformation (rename/description/default/drop) +- None\: Drop the argument - `description`: New description. Defaults to parent's description. - `tags`: New tags. Defaults to parent's tags. - `annotations`: New annotations. Defaults to parent's annotations. diff --git a/docs/python-sdk/fastmcp-utilities-json_schema.mdx b/docs/python-sdk/fastmcp-utilities-json_schema.mdx index bedf79119..ad68473a0 100644 --- a/docs/python-sdk/fastmcp-utilities-json_schema.mdx +++ b/docs/python-sdk/fastmcp-utilities-json_schema.mdx @@ -20,6 +20,6 @@ Remove the given parameters from the schema. - `schema`: The schema to compress - `prune_params`: List of parameter names to remove from properties - `prune_defs`: Whether to remove unused definitions -- `prune_additional_properties`: Whether to remove additionalProperties: false +- `prune_additional_properties`: Whether to remove additionalProperties\: false - `prune_titles`: Whether to remove title fields from the schema diff --git a/justfile b/justfile index 9d918cb9d..a18451447 100644 --- a/justfile +++ b/justfile @@ -16,11 +16,11 @@ docs: # Generate API reference documentation for all modules api-ref-all: - uv run --with-editable . --with mdxify@latest mdxify --all --root-module fastmcp --anchor-name "SDK Reference" + uvx --with-editable . --refresh-package mdxify mdxify@latest --all --root-module fastmcp --anchor-name "SDK Reference" # Generate API reference for specific modules (e.g., just api-ref prefect.flows prefect.tasks) api-ref *MODULES: - uv run --with-editable . --with mdxify@latest mdxify {{MODULES}} --root-module fastmcp --anchor-name "SDK Reference" + uvx --with-editable . --refresh-package mdxify mdxify@latest {{MODULES}} --root-module fastmcp --anchor-name "SDK Reference" # Clean up API reference documentation api-ref-clean: From e245155bc5e22311fad12f5217d2e9f7aea95093 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 20:48:42 -0400 Subject: [PATCH 18/41] Simplify docs nav --- docs/changelog.mdx | 2 +- docs/clients/auth/bearer.mdx | 2 +- docs/clients/auth/oauth.mdx | 2 +- docs/docs.json | 44 +++++++++------------------------ docs/integrations/anthropic.mdx | 2 +- docs/integrations/gemini.mdx | 2 +- docs/integrations/openai.mdx | 2 +- docs/servers/auth/bearer.mdx | 2 +- docs/updates.mdx | 2 +- 9 files changed, 20 insertions(+), 40 deletions(-) diff --git a/docs/changelog.mdx b/docs/changelog.mdx index d99e49e83..284b86344 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -1,5 +1,5 @@ --- -mode: center +icon: "list-check" --- diff --git a/docs/clients/auth/bearer.mdx b/docs/clients/auth/bearer.mdx index edc41e32d..478e1a957 100644 --- a/docs/clients/auth/bearer.mdx +++ b/docs/clients/auth/bearer.mdx @@ -3,7 +3,7 @@ title: Bearer Token Authentication sidebarTitle: Bearer Auth description: Authenticate your FastMCP client with a Bearer token. icon: key -tag: "New!" +tag: NEW --- import { VersionBadge } from "/snippets/version-badge.mdx" diff --git a/docs/clients/auth/oauth.mdx b/docs/clients/auth/oauth.mdx index e9d2e5a29..31b7fbdf4 100644 --- a/docs/clients/auth/oauth.mdx +++ b/docs/clients/auth/oauth.mdx @@ -3,7 +3,7 @@ title: OAuth Authentication sidebarTitle: OAuth description: Authenticate your FastMCP client via OAuth 2.1. icon: window -tag: "New!" +tag: NEW --- import { VersionBadge } from "/snippets/version-badge.mdx" diff --git a/docs/docs.json b/docs/docs.json index 2f5933cc1..3f17a0342 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -53,10 +53,13 @@ "pages": [ "getting-started/welcome", "getting-started/installation", - "getting-started/quickstart", - "updates" + "getting-started/quickstart" ] }, + { + "group": "What's New", + "pages": ["updates", "changelog"] + }, { "group": "Servers", "pages": [ @@ -74,9 +77,7 @@ { "group": "Authentication", "icon": "shield-check", - "pages": [ - "servers/auth/bearer" - ] + "pages": ["servers/auth/bearer"] }, "servers/middleware", "servers/openapi", @@ -85,10 +86,7 @@ { "group": "Deployment", "icon": "upload", - "pages": [ - "deployment/running-server", - "deployment/asgi" - ] + "pages": ["deployment/running-server", "deployment/asgi"] } ] }, @@ -100,10 +98,7 @@ { "group": "Authentication", "icon": "user-shield", - "pages": [ - "clients/auth/oauth", - "clients/auth/bearer" - ] + "pages": ["clients/auth/oauth", "clients/auth/bearer"] }, "clients/advanced-features" ] @@ -127,15 +122,9 @@ "patterns/testing", "patterns/cli" ] - } - ], - "icon": "book" - }, - { - "anchor": "Tutorials", - "groups": [ + }, { - "group": "MCP", + "group": "Tutorials", "pages": [ "tutorials/mcp", "tutorials/create-mcp-server", @@ -143,21 +132,12 @@ ] } ], - "icon": "graduation-cap" - }, - { - "anchor": "Changelog", - "icon": "list-check", - "pages": [ - "changelog" - ] + "icon": "book" }, { "anchor": "Community", "icon": "users", - "pages": [ - "community/showcase" - ] + "pages": ["community/showcase"] }, { "anchor": "SDK Reference", diff --git a/docs/integrations/anthropic.mdx b/docs/integrations/anthropic.mdx index c5cdaea69..e9ebe8601 100644 --- a/docs/integrations/anthropic.mdx +++ b/docs/integrations/anthropic.mdx @@ -3,7 +3,7 @@ title: Anthropic API + FastMCP sidebarTitle: Anthropic API description: Call FastMCP servers from the Anthropic API icon: message-smile -tag: "New!" +tag: NEW --- import { VersionBadge } from "/snippets/version-badge.mdx" diff --git a/docs/integrations/gemini.mdx b/docs/integrations/gemini.mdx index 1959e8fa3..ab9e68ce0 100644 --- a/docs/integrations/gemini.mdx +++ b/docs/integrations/gemini.mdx @@ -3,7 +3,7 @@ title: Gemini SDK + FastMCP sidebarTitle: Gemini SDK description: Call FastMCP servers from the Google Gemini SDK icon: message-smile -tag: "New!" +tag: NEW --- import { VersionBadge } from "/snippets/version-badge.mdx" diff --git a/docs/integrations/openai.mdx b/docs/integrations/openai.mdx index b68ebcc18..ba0d00941 100644 --- a/docs/integrations/openai.mdx +++ b/docs/integrations/openai.mdx @@ -3,7 +3,7 @@ title: OpenAI API + FastMCP sidebarTitle: OpenAI API description: Call FastMCP servers from the OpenAI API icon: message-smile -tag: "New!" +tag: NEW --- import { VersionBadge } from "/snippets/version-badge.mdx" diff --git a/docs/servers/auth/bearer.mdx b/docs/servers/auth/bearer.mdx index 257238bc1..900bebe5a 100644 --- a/docs/servers/auth/bearer.mdx +++ b/docs/servers/auth/bearer.mdx @@ -3,7 +3,7 @@ title: Bearer Token Authentication sidebarTitle: Bearer Auth description: Secure your FastMCP server's HTTP endpoints by validating JWT Bearer tokens. icon: key -tag: "New!" +tag: NEW --- import { VersionBadge } from "/snippets/version-badge.mdx" diff --git a/docs/updates.mdx b/docs/updates.mdx index 28d25ccb7..27f7afb39 100644 --- a/docs/updates.mdx +++ b/docs/updates.mdx @@ -2,7 +2,7 @@ title: "FastMCP Updates" sidebarTitle: "Updates" icon: "sparkles" -tag: "New!" +tag: NEW --- From 6a3a3077b7d3894148116a9a51155a79cc36e0bd Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 21 Jun 2025 16:40:54 -0400 Subject: [PATCH 19/41] Add fastmcp inspect command with detailed server analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive server inspection utility supporting both FastMCP 1.x and 2.x - Create detailed info dataclasses for tools, prompts, resources, and templates - Implement CLI command with path:object notation and JSON output - Add version reporting (fastmcp_version, mcp_version, server_version) - Include comprehensive unit tests for utilities and CLI 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/cli/cli.py | 107 +++++++++ src/fastmcp/utilities/inspect.py | 326 ++++++++++++++++++++++++++ tests/cli/test_inspect.py | 354 ++++++++++++++++++++++++++++ tests/utilities/test_inspect.py | 388 +++++++++++++++++++++++++++++++ 4 files changed, 1175 insertions(+) create mode 100644 src/fastmcp/utilities/inspect.py create mode 100644 tests/cli/test_inspect.py create mode 100644 tests/utilities/test_inspect.py diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index f3e326270..57c77da73 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -1,7 +1,9 @@ """FastMCP CLI tools.""" +import asyncio import importlib.metadata import importlib.util +import json import os import platform import subprocess @@ -19,6 +21,7 @@ import fastmcp from fastmcp.cli import claude from fastmcp.cli import run as run_module from fastmcp.server.server import FastMCP +from fastmcp.utilities.inspect import get_fastmcp_info from fastmcp.utilities.logging import get_logger logger = get_logger("cli") @@ -435,3 +438,107 @@ def install( else: logger.error(f"Failed to install {name} in Claude app") sys.exit(1) + + +@app.command() +def inspect( + server_spec: str = typer.Argument( + ..., + help="Python file to inspect, optionally with :object suffix", + ), + output: Annotated[ + Path, + typer.Option( + "--output", + "-o", + help="Output file path for the JSON report (default: server-info.json)", + ), + ] = Path("server-info.json"), +) -> None: + """Inspect a FastMCP server and generate a JSON report. + + This command analyzes a FastMCP server (v1.x or v2.x) and generates + a comprehensive JSON report containing information about the server's + name, instructions, version, tools, prompts, resources, templates, + and capabilities. + + Examples: + fastmcp inspect server.py + fastmcp inspect server.py -o report.json + fastmcp inspect server.py:mcp -o analysis.json + fastmcp inspect path/to/server.py:app -o /tmp/server-info.json + """ + + # Parse the server specification + file, server_object = run_module.parse_file_path(server_spec) + + logger.debug( + "Inspecting server", + extra={ + "file": str(file), + "server_object": server_object, + "output": str(output), + }, + ) + + try: + # Import the server + server = run_module.import_server(file, server_object) + + # Get server information + async def get_info(): + return await get_fastmcp_info(server) + + info = asyncio.run(get_info()) + + # Convert to dict for JSON serialization + def convert_dataclass_to_dict(obj): + """Convert dataclass instances to dicts for JSON serialization.""" + if hasattr(obj, "__dataclass_fields__"): + return { + k: convert_dataclass_to_dict(v) for k, v in obj.__dict__.items() + } + elif isinstance(obj, list): + return [convert_dataclass_to_dict(item) for item in obj] + elif isinstance(obj, set): + return list(obj) + elif hasattr(obj, "model_dump"): # Pydantic models + return obj.model_dump() + elif hasattr(obj, "__dict__"): # Other objects with __dict__ + return { + k: convert_dataclass_to_dict(v) for k, v in obj.__dict__.items() + } + else: + return obj + + info_dict = convert_dataclass_to_dict(info) + + # Ensure output directory exists + output.parent.mkdir(parents=True, exist_ok=True) + + # Write JSON report (always pretty-printed) + with output.open("w", encoding="utf-8") as f: + json.dump(info_dict, f, indent=2, ensure_ascii=False) + + logger.info(f"Server inspection complete. Report saved to {output}") + + # Print summary to console + console.print( + f"[bold green]✓[/bold green] Inspected server: [bold]{info.name}[/bold]" + ) + console.print(f" Tools: {len(info.tools)}") + console.print(f" Prompts: {len(info.prompts)}") + console.print(f" Resources: {len(info.resources)}") + console.print(f" Templates: {len(info.templates)}") + console.print(f" Report saved to: [cyan]{output}[/cyan]") + + except Exception as e: + logger.error( + f"Failed to inspect server: {e}", + extra={ + "server_spec": server_spec, + "error": str(e), + }, + ) + console.print(f"[bold red]✗[/bold red] Failed to inspect server: {e}") + sys.exit(1) diff --git a/src/fastmcp/utilities/inspect.py b/src/fastmcp/utilities/inspect.py new file mode 100644 index 000000000..9ddd2c61f --- /dev/null +++ b/src/fastmcp/utilities/inspect.py @@ -0,0 +1,326 @@ +"""Utilities for inspecting FastMCP instances.""" + +from __future__ import annotations + +import importlib.metadata +from dataclasses import dataclass +from typing import Any + +from mcp.server.fastmcp import FastMCP as FastMCP1x + +import fastmcp +from fastmcp.server.server import FastMCP + + +@dataclass +class ToolInfo: + """Information about a tool.""" + + key: str + name: str + description: str | None + input_schema: dict[str, Any] + annotations: dict[str, Any] | None = None + tags: list[str] | None = None + enabled: bool | None = None + + +@dataclass +class PromptInfo: + """Information about a prompt.""" + + key: str + name: str + description: str | None + arguments: list[dict[str, Any]] | None = None + tags: list[str] | None = None + enabled: bool | None = None + + +@dataclass +class ResourceInfo: + """Information about a resource.""" + + key: str + uri: str + name: str | None + description: str | None + mime_type: str | None = None + tags: list[str] | None = None + enabled: bool | None = None + + +@dataclass +class TemplateInfo: + """Information about a resource template.""" + + key: str + uri_template: str + name: str | None + description: str | None + mime_type: str | None = None + tags: list[str] | None = None + enabled: bool | None = None + + +@dataclass +class FastMCPInfo: + """Information extracted from a FastMCP instance.""" + + name: str + instructions: str | None + fastmcp_version: str + mcp_version: str + server_version: str + tools: list[ToolInfo] + prompts: list[PromptInfo] + resources: list[ResourceInfo] + templates: list[TemplateInfo] + capabilities: dict[str, Any] + + +async def get_fastmcp_info_v2(mcp: FastMCP[Any]) -> FastMCPInfo: + """Extract information from a FastMCP v2.x instance. + + Args: + mcp: The FastMCP v2.x instance to inspect + + Returns: + FastMCPInfo dataclass containing the extracted information + """ + # Get all the components using FastMCP2's direct methods + tools_dict = await mcp.get_tools() + prompts_dict = await mcp.get_prompts() + resources_dict = await mcp.get_resources() + templates_dict = await mcp.get_resource_templates() + + # Extract detailed tool information + tool_infos = [] + for key, tool in tools_dict.items(): + # Convert to MCP tool to get input schema + mcp_tool = tool.to_mcp_tool(name=key) + tool_infos.append( + ToolInfo( + key=key, + name=tool.name or key, + description=tool.description, + input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {}, + annotations=tool.annotations.model_dump() if tool.annotations else None, + tags=list(tool.tags) if tool.tags else None, + enabled=tool.enabled, + ) + ) + + # Extract detailed prompt information + prompt_infos = [] + for key, prompt in prompts_dict.items(): + prompt_infos.append( + PromptInfo( + key=key, + name=prompt.name or key, + description=prompt.description, + arguments=[arg.model_dump() for arg in prompt.arguments] + if prompt.arguments + else None, + tags=list(prompt.tags) if prompt.tags else None, + enabled=prompt.enabled, + ) + ) + + # Extract detailed resource information + resource_infos = [] + for key, resource in resources_dict.items(): + resource_infos.append( + ResourceInfo( + key=key, + uri=key, # For v2, key is the URI + name=resource.name, + description=resource.description, + mime_type=resource.mime_type, + tags=list(resource.tags) if resource.tags else None, + enabled=resource.enabled, + ) + ) + + # Extract detailed template information + template_infos = [] + for key, template in templates_dict.items(): + template_infos.append( + TemplateInfo( + key=key, + uri_template=key, # For v2, key is the URI template + name=template.name, + description=template.description, + mime_type=template.mime_type, + tags=list(template.tags) if template.tags else None, + enabled=template.enabled, + ) + ) + + # Basic MCP capabilities that FastMCP supports + capabilities = { + "tools": {"listChanged": True}, + "resources": {"subscribe": False, "listChanged": False}, + "prompts": {"listChanged": False}, + "logging": {}, + } + + return FastMCPInfo( + name=mcp.name, + instructions=mcp.instructions, + fastmcp_version=fastmcp.__version__, + mcp_version=importlib.metadata.version("mcp"), + server_version=fastmcp.__version__, # v2.x uses FastMCP version + tools=tool_infos, + prompts=prompt_infos, + resources=resource_infos, + templates=template_infos, + capabilities=capabilities, + ) + + +async def get_fastmcp_info_v1(mcp: Any) -> FastMCPInfo: + """Extract information from a FastMCP v1.x instance using a Client. + + Args: + mcp: The FastMCP v1.x instance to inspect + + Returns: + FastMCPInfo dataclass containing the extracted information + """ + from fastmcp import Client + + # Use a client to interact with the FastMCP1x server + async with Client(mcp) as client: + # Get components via client calls (these return MCP objects) + mcp_tools = await client.list_tools() + mcp_prompts = await client.list_prompts() + mcp_resources = await client.list_resources() + + # Try to get resource templates (FastMCP 1.x does have templates) + try: + mcp_templates = await client.list_resource_templates() + except Exception: + mcp_templates = [] + + # Extract detailed tool information from MCP Tool objects + tool_infos = [] + for mcp_tool in mcp_tools: + # Extract annotations if they exist + annotations = None + if hasattr(mcp_tool, "annotations") and mcp_tool.annotations: + if hasattr(mcp_tool.annotations, "model_dump"): + annotations = mcp_tool.annotations.model_dump() + elif isinstance(mcp_tool.annotations, dict): + annotations = mcp_tool.annotations + else: + annotations = None + + tool_infos.append( + ToolInfo( + key=mcp_tool.name, # For 1.x, key and name are the same + name=mcp_tool.name, + description=mcp_tool.description, + input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {}, + annotations=annotations, + tags=None, # 1.x doesn't have tags + enabled=None, # 1.x doesn't have enabled field + ) + ) + + # Extract detailed prompt information from MCP Prompt objects + prompt_infos = [] + for mcp_prompt in mcp_prompts: + # Convert arguments if they exist + arguments = None + if hasattr(mcp_prompt, "arguments") and mcp_prompt.arguments: + arguments = [arg.model_dump() for arg in mcp_prompt.arguments] + + prompt_infos.append( + PromptInfo( + key=mcp_prompt.name, # For 1.x, key and name are the same + name=mcp_prompt.name, + description=mcp_prompt.description, + arguments=arguments, + tags=None, # 1.x doesn't have tags + enabled=None, # 1.x doesn't have enabled field + ) + ) + + # Extract detailed resource information from MCP Resource objects + resource_infos = [] + for mcp_resource in mcp_resources: + resource_infos.append( + ResourceInfo( + key=str(mcp_resource.uri), # For 1.x, key and uri are the same + uri=str(mcp_resource.uri), + name=mcp_resource.name, + description=mcp_resource.description, + mime_type=mcp_resource.mimeType, + tags=None, # 1.x doesn't have tags + enabled=None, # 1.x doesn't have enabled field + ) + ) + + # Extract detailed template information from MCP ResourceTemplate objects + template_infos = [] + for mcp_template in mcp_templates: + template_infos.append( + TemplateInfo( + key=str( + mcp_template.uriTemplate + ), # For 1.x, key and uriTemplate are the same + uri_template=str(mcp_template.uriTemplate), + name=mcp_template.name, + description=mcp_template.description, + mime_type=mcp_template.mimeType, + tags=None, # 1.x doesn't have tags + enabled=None, # 1.x doesn't have enabled field + ) + ) + + # Basic MCP capabilities + capabilities = { + "tools": {"listChanged": True}, + "resources": {"subscribe": False, "listChanged": False}, + "prompts": {"listChanged": False}, + "logging": {}, + } + + return FastMCPInfo( + name=mcp.name, + instructions=getattr(mcp, "instructions", None), + fastmcp_version=fastmcp.__version__, # Report current fastmcp version + mcp_version=importlib.metadata.version("mcp"), + server_version="1.0", # FastMCP 1.x version + tools=tool_infos, + prompts=prompt_infos, + resources=resource_infos, + templates=template_infos, # FastMCP1x does have templates + capabilities=capabilities, + ) + + +def _is_fastmcp_v1(mcp: Any) -> bool: + """Check if the given instance is a FastMCP v1.x instance.""" + + # Check if it's an instance of FastMCP1x and not FastMCP2 + return isinstance(mcp, FastMCP1x) and not isinstance(mcp, FastMCP) + + +async def get_fastmcp_info(mcp: FastMCP[Any] | Any) -> FastMCPInfo: + """Extract information from a FastMCP instance into a dataclass. + + This function automatically detects whether the instance is FastMCP v1.x or v2.x + and uses the appropriate extraction method. + + Args: + mcp: The FastMCP instance to inspect (v1.x or v2.x) + + Returns: + FastMCPInfo dataclass containing the extracted information + """ + if _is_fastmcp_v1(mcp): + return await get_fastmcp_info_v1(mcp) + else: + return await get_fastmcp_info_v2(mcp) diff --git a/tests/cli/test_inspect.py b/tests/cli/test_inspect.py new file mode 100644 index 000000000..fa2f992cb --- /dev/null +++ b/tests/cli/test_inspect.py @@ -0,0 +1,354 @@ +"""Tests for the CLI inspect command.""" + +import json +import tempfile +from pathlib import Path + +from typer.testing import CliRunner + +from fastmcp.cli.cli import app + + +class TestInspectCommand: + """Tests for the fastmcp inspect CLI command.""" + + def setup_method(self): + """Set up test fixtures.""" + self.runner = CliRunner() + + def test_inspect_basic_server(self): + """Test inspecting a basic FastMCP 2.x server.""" + # Create a temporary server file + server_content = ''' +from fastmcp import FastMCP + +mcp = FastMCP("TestServer", instructions="A test server") + +@mcp.tool +def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + +@mcp.resource("resource://data") +def get_data() -> str: + """Get test data.""" + return "test data" + +@mcp.prompt +def test_prompt(message: str) -> list: + """Test prompt.""" + return [{"role": "user", "content": message}] +''' + + with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: + f.write(server_content) + server_file = f.name + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + output_file = f.name + + try: + # Run the inspect command + result = self.runner.invoke( + app, ["inspect", server_file, "-o", output_file] + ) + + assert result.exit_code == 0 + assert "✓ Inspected server: TestServer" in result.stdout + assert "Tools: 1" in result.stdout + assert "Prompts: 1" in result.stdout + assert "Resources: 1" in result.stdout + + # Check the JSON output + with open(output_file) as f: + data = json.load(f) + + assert data["name"] == "TestServer" + assert data["instructions"] == "A test server" + assert "fastmcp_version" in data + assert "mcp_version" in data + assert "server_version" in data + + # Check tools + assert len(data["tools"]) == 1 + tool = data["tools"][0] + assert tool["key"] == "add" + assert tool["name"] == "add" + assert tool["description"] == "Add two numbers." + assert "input_schema" in tool + assert tool["enabled"] is True + + # Check resources + assert len(data["resources"]) == 1 + resource = data["resources"][0] + assert resource["key"] == "resource://data" + assert resource["uri"] == "resource://data" + assert resource["name"] == "get_data" + + # Check prompts + assert len(data["prompts"]) == 1 + prompt = data["prompts"][0] + assert prompt["key"] == "test_prompt" + assert prompt["name"] == "test_prompt" + assert prompt["description"] == "Test prompt." + + # Check capabilities + assert "capabilities" in data + assert "tools" in data["capabilities"] + + finally: + # Clean up + Path(server_file).unlink(missing_ok=True) + Path(output_file).unlink(missing_ok=True) + + def test_inspect_with_object_spec(self): + """Test inspecting a server with object specification.""" + server_content = ''' +from fastmcp import FastMCP + +server = FastMCP("ObjectSpecServer") + +@server.tool +def multiply(a: int, b: int) -> int: + """Multiply two numbers.""" + return a * b +''' + + with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: + f.write(server_content) + server_file = f.name + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + output_file = f.name + + try: + # Run the inspect command with object specification + result = self.runner.invoke( + app, ["inspect", f"{server_file}:server", "-o", output_file] + ) + + assert result.exit_code == 0 + assert "✓ Inspected server: ObjectSpecServer" in result.stdout + + # Check the JSON output + with open(output_file) as f: + data = json.load(f) + + assert data["name"] == "ObjectSpecServer" + assert len(data["tools"]) == 1 + assert data["tools"][0]["name"] == "multiply" + + finally: + # Clean up + Path(server_file).unlink(missing_ok=True) + Path(output_file).unlink(missing_ok=True) + + def test_inspect_default_output(self): + """Test inspecting with default output filename.""" + server_content = ''' +from fastmcp import FastMCP + +mcp = FastMCP("DefaultOutputServer") + +@mcp.tool +def test_tool() -> str: + """Test tool.""" + return "test" +''' + + with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: + f.write(server_content) + server_file = f.name + + try: + # Run the inspect command without specifying output file + result = self.runner.invoke(app, ["inspect", server_file]) + + assert result.exit_code == 0 + assert "✓ Inspected server: DefaultOutputServer" in result.stdout + assert "Report saved to: server-info.json" in result.stdout + + # Check the default output file exists + default_output = Path("server-info.json") + assert default_output.exists() + + # Check the JSON content + with open(default_output) as f: + data = json.load(f) + + assert data["name"] == "DefaultOutputServer" + + finally: + # Clean up + Path(server_file).unlink(missing_ok=True) + Path("server-info.json").unlink(missing_ok=True) + + def test_inspect_invalid_server_file(self): + """Test inspecting a non-existent server file.""" + result = self.runner.invoke( + app, ["inspect", "nonexistent.py", "-o", "output.json"] + ) + + assert result.exit_code == 1 + # The error happens at the file parsing level, so no stdout output + + def test_inspect_server_with_error(self): + """Test inspecting a server file with syntax errors.""" + server_content = """ +from fastmcp import FastMCP + +mcp = FastMCP("ErrorServer") +# Syntax error below +@mcp.tool +def broken_tool( +""" + + with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: + f.write(server_content) + server_file = f.name + + try: + result = self.runner.invoke( + app, ["inspect", server_file, "-o", "output.json"] + ) + + assert result.exit_code == 1 + assert "✗ Failed to inspect server:" in result.stdout + + finally: + # Clean up + Path(server_file).unlink(missing_ok=True) + Path("output.json").unlink(missing_ok=True) + + def test_inspect_comprehensive_json_structure(self): + """Test that the JSON output has the correct structure.""" + server_content = ''' +from fastmcp import FastMCP + +mcp = FastMCP("ComprehensiveServer", instructions="Full test server") + +@mcp.tool +def calculate(x: int, y: int) -> int: + """Calculate something.""" + return x + y + +@mcp.resource("resource://static") +def static_resource() -> str: + """Static resource.""" + return "static" + +@mcp.resource("resource://template/{id}") +def template_resource(id: str) -> str: + """Template resource.""" + return f"data-{id}" + +@mcp.prompt +def analysis_prompt(data: str) -> list: + """Analysis prompt.""" + return [{"role": "user", "content": f"Analyze: {data}"}] +''' + + with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: + f.write(server_content) + server_file = f.name + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + output_file = f.name + + try: + result = self.runner.invoke( + app, ["inspect", server_file, "-o", output_file] + ) + + assert result.exit_code == 0 + + # Load and validate JSON structure + with open(output_file) as f: + data = json.load(f) + + # Check top-level structure + required_fields = [ + "name", + "instructions", + "fastmcp_version", + "mcp_version", + "server_version", + "tools", + "prompts", + "resources", + "templates", + "capabilities", + ] + for field in required_fields: + assert field in data, f"Missing field: {field}" + + # Check version fields are strings + assert isinstance(data["fastmcp_version"], str) + assert isinstance(data["mcp_version"], str) + assert isinstance(data["server_version"], str) + + # Check that we have the expected components + assert len(data["tools"]) == 1 + assert len(data["resources"]) == 1 + assert len(data["templates"]) == 1 + assert len(data["prompts"]) == 1 + + # Check tool structure + tool = data["tools"][0] + tool_fields = [ + "key", + "name", + "description", + "input_schema", + "annotations", + "tags", + "enabled", + ] + for field in tool_fields: + assert field in tool, f"Missing tool field: {field}" + + # Check resource structure + resource = data["resources"][0] + resource_fields = [ + "key", + "uri", + "name", + "description", + "mime_type", + "tags", + "enabled", + ] + for field in resource_fields: + assert field in resource, f"Missing resource field: {field}" + + # Check template structure + template = data["templates"][0] + template_fields = [ + "key", + "uri_template", + "name", + "description", + "mime_type", + "tags", + "enabled", + ] + for field in template_fields: + assert field in template, f"Missing template field: {field}" + + # Check prompt structure + prompt = data["prompts"][0] + prompt_fields = [ + "key", + "name", + "description", + "arguments", + "tags", + "enabled", + ] + for field in prompt_fields: + assert field in prompt, f"Missing prompt field: {field}" + + finally: + # Clean up + Path(server_file).unlink(missing_ok=True) + Path(output_file).unlink(missing_ok=True) diff --git a/tests/utilities/test_inspect.py b/tests/utilities/test_inspect.py new file mode 100644 index 000000000..0ca03b6c2 --- /dev/null +++ b/tests/utilities/test_inspect.py @@ -0,0 +1,388 @@ +"""Tests for the inspect.py module.""" + +# Import FastMCP1x for testing (always available since mcp is a dependency) +from mcp.server.fastmcp import FastMCP as FastMCP1x + +import fastmcp +from fastmcp import Client, FastMCP +from fastmcp.utilities.inspect import ( + FastMCPInfo, + ToolInfo, + _is_fastmcp_v1, + get_fastmcp_info, + get_fastmcp_info_v1, +) + + +class TestFastMCPInfo: + """Tests for the FastMCPInfo dataclass.""" + + def test_fastmcp_info_creation(self): + """Test that FastMCPInfo can be created with all required fields.""" + tool = ToolInfo( + key="tool1", name="tool1", description="Test tool", input_schema={} + ) + info = FastMCPInfo( + name="TestServer", + instructions="Test instructions", + fastmcp_version="1.0.0", + mcp_version="1.0.0", + server_version="1.0.0", + tools=[tool], + prompts=[], + resources=[], + templates=[], + capabilities={"tools": {"listChanged": True}}, + ) + + assert info.name == "TestServer" + assert info.instructions == "Test instructions" + assert info.fastmcp_version == "1.0.0" + assert info.mcp_version == "1.0.0" + assert info.server_version == "1.0.0" + assert len(info.tools) == 1 + assert info.tools[0].name == "tool1" + assert info.capabilities == {"tools": {"listChanged": True}} + + def test_fastmcp_info_with_none_instructions(self): + """Test that FastMCPInfo works with None instructions.""" + info = FastMCPInfo( + name="TestServer", + instructions=None, + fastmcp_version="1.0.0", + mcp_version="1.0.0", + server_version="1.0.0", + tools=[], + prompts=[], + resources=[], + templates=[], + capabilities={}, + ) + + assert info.instructions is None + + +class TestGetFastMCPInfo: + """Tests for the get_fastmcp_info function.""" + + async def test_empty_server(self): + """Test get_fastmcp_info with an empty server.""" + mcp = FastMCP("EmptyServer", instructions="Empty server for testing") + + info = await get_fastmcp_info(mcp) + + assert info.name == "EmptyServer" + assert info.instructions == "Empty server for testing" + assert info.fastmcp_version == fastmcp.__version__ + assert info.mcp_version is not None + assert info.server_version == fastmcp.__version__ # v2.x uses FastMCP version + assert info.tools == [] + assert info.prompts == [] + assert info.resources == [] + assert info.templates == [] + assert "tools" in info.capabilities + assert "resources" in info.capabilities + assert "prompts" in info.capabilities + assert "logging" in info.capabilities + + async def test_server_with_tools(self): + """Test get_fastmcp_info with a server that has tools.""" + mcp = FastMCP("ToolServer") + + @mcp.tool + def add_numbers(a: int, b: int) -> int: + return a + b + + @mcp.tool + def greet(name: str) -> str: + return f"Hello, {name}!" + + info = await get_fastmcp_info(mcp) + + assert info.name == "ToolServer" + assert len(info.tools) == 2 + tool_names = [tool.name for tool in info.tools] + assert "add_numbers" in tool_names + assert "greet" in tool_names + + async def test_server_with_resources(self): + """Test get_fastmcp_info with a server that has resources.""" + mcp = FastMCP("ResourceServer") + + @mcp.resource("resource://static") + def get_static_data() -> str: + return "Static data" + + @mcp.resource("resource://dynamic/{param}") + def get_dynamic_data(param: str) -> str: + return f"Dynamic data: {param}" + + info = await get_fastmcp_info(mcp) + + assert info.name == "ResourceServer" + assert len(info.resources) == 1 # Static resource + assert len(info.templates) == 1 # Dynamic resource becomes template + resource_uris = [res.uri for res in info.resources] + template_uris = [tmpl.uri_template for tmpl in info.templates] + assert "resource://static" in resource_uris + assert "resource://dynamic/{param}" in template_uris + + async def test_server_with_prompts(self): + """Test get_fastmcp_info with a server that has prompts.""" + mcp = FastMCP("PromptServer") + + @mcp.prompt + def analyze_data(data: str) -> list: + return [{"role": "user", "content": f"Analyze: {data}"}] + + @mcp.prompt("custom_prompt") + def custom_analysis(text: str) -> list: + return [{"role": "user", "content": f"Custom: {text}"}] + + info = await get_fastmcp_info(mcp) + + assert info.name == "PromptServer" + assert len(info.prompts) == 2 + prompt_names = [prompt.name for prompt in info.prompts] + assert "analyze_data" in prompt_names + assert "custom_prompt" in prompt_names + + async def test_comprehensive_server(self): + """Test get_fastmcp_info with a server that has all component types.""" + mcp = FastMCP("ComprehensiveServer", instructions="A server with everything") + + # Add a tool + @mcp.tool + def calculate(x: int, y: int) -> int: + return x * y + + # Add a resource + @mcp.resource("resource://data") + def get_data() -> str: + return "Some data" + + # Add a template + @mcp.resource("resource://item/{id}") + def get_item(id: str) -> str: + return f"Item {id}" + + # Add a prompt + @mcp.prompt + def analyze(content: str) -> list: + return [{"role": "user", "content": content}] + + info = await get_fastmcp_info(mcp) + + assert info.name == "ComprehensiveServer" + assert info.instructions == "A server with everything" + assert info.fastmcp_version == fastmcp.__version__ + + # Check all components are present + assert len(info.tools) == 1 + tool_names = [tool.name for tool in info.tools] + assert "calculate" in tool_names + + assert len(info.resources) == 1 + resource_uris = [res.uri for res in info.resources] + assert "resource://data" in resource_uris + + assert len(info.templates) == 1 + template_uris = [tmpl.uri_template for tmpl in info.templates] + assert "resource://item/{id}" in template_uris + + assert len(info.prompts) == 1 + prompt_names = [prompt.name for prompt in info.prompts] + assert "analyze" in prompt_names + + # Check capabilities + assert "tools" in info.capabilities + assert "resources" in info.capabilities + assert "prompts" in info.capabilities + assert "logging" in info.capabilities + + async def test_server_no_instructions(self): + """Test get_fastmcp_info with a server that has no instructions.""" + mcp = FastMCP("NoInstructionsServer") + + info = await get_fastmcp_info(mcp) + + assert info.name == "NoInstructionsServer" + assert info.instructions is None + + async def test_server_with_client_integration(self): + """Test that the extracted info matches what a client would see.""" + mcp = FastMCP("IntegrationServer") + + @mcp.tool + def test_tool() -> str: + return "test" + + @mcp.resource("resource://test") + def test_resource() -> str: + return "test resource" + + @mcp.prompt + def test_prompt() -> list: + return [{"role": "user", "content": "test"}] + + # Get info using our function + info = await get_fastmcp_info(mcp) + + # Verify using client + async with Client(mcp) as client: + tools = await client.list_tools() + resources = await client.list_resources() + prompts = await client.list_prompts() + + assert len(info.tools) == len(tools) + assert len(info.resources) == len(resources) + assert len(info.prompts) == len(prompts) + + assert info.tools[0].name == tools[0].name + assert info.resources[0].uri == str(resources[0].uri) + assert info.prompts[0].name == prompts[0].name + + +class TestFastMCP1xCompatibility: + """Tests for FastMCP 1.x compatibility.""" + + async def test_fastmcp1x_detection(self): + """Test that FastMCP1x instances are correctly detected.""" + mcp1x = FastMCP1x("Test1x") + mcp2x = FastMCP("Test2x") + + assert _is_fastmcp_v1(mcp1x) is True + assert _is_fastmcp_v1(mcp2x) is False + + async def test_fastmcp1x_empty_server(self): + """Test get_fastmcp_info_v1 with an empty FastMCP1x server.""" + mcp = FastMCP1x("Test1x") + + info = await get_fastmcp_info_v1(mcp) + + assert info.name == "Test1x" + assert info.instructions is None + assert info.fastmcp_version == fastmcp.__version__ + assert info.mcp_version is not None + assert info.server_version == "1.0" # v1.x servers use "1.0" + assert info.tools == [] + assert info.prompts == [] + assert info.resources == [] + assert info.templates == [] # No templates added in this test + assert "tools" in info.capabilities + + async def test_fastmcp1x_with_tools(self): + """Test get_fastmcp_info_v1 with a FastMCP1x server that has tools.""" + mcp = FastMCP1x("Test1x") + + @mcp.tool() + def add_numbers(a: int, b: int) -> int: + return a + b + + @mcp.tool() + def greet(name: str) -> str: + return f"Hello, {name}!" + + info = await get_fastmcp_info_v1(mcp) + + assert info.name == "Test1x" + assert len(info.tools) == 2 + tool_names = [tool.name for tool in info.tools] + assert "add_numbers" in tool_names + assert "greet" in tool_names + + async def test_fastmcp1x_with_resources(self): + """Test get_fastmcp_info_v1 with a FastMCP1x server that has resources.""" + mcp = FastMCP1x("Test1x") + + @mcp.resource("resource://data") + def get_data() -> str: + return "Some data" + + info = await get_fastmcp_info_v1(mcp) + + assert info.name == "Test1x" + assert len(info.resources) == 1 + resource_uris = [res.uri for res in info.resources] + assert "resource://data" in resource_uris + assert len(info.templates) == 0 # No templates added in this test + + async def test_fastmcp1x_with_prompts(self): + """Test get_fastmcp_info_v1 with a FastMCP1x server that has prompts.""" + mcp = FastMCP1x("Test1x") + + @mcp.prompt("analyze") + def analyze_data(data: str) -> list: + return [{"role": "user", "content": f"Analyze: {data}"}] + + info = await get_fastmcp_info_v1(mcp) + + assert info.name == "Test1x" + assert len(info.prompts) == 1 + prompt_names = [prompt.name for prompt in info.prompts] + assert "analyze" in prompt_names + + async def test_dispatcher_with_fastmcp1x(self): + """Test that the main get_fastmcp_info function correctly dispatches to v1.""" + mcp = FastMCP1x("Test1x") + + @mcp.tool() + def test_tool() -> str: + return "test" + + info = await get_fastmcp_info(mcp) + + assert info.name == "Test1x" + assert len(info.tools) == 1 + tool_names = [tool.name for tool in info.tools] + assert "test_tool" in tool_names + assert len(info.templates) == 0 # No templates added in this test + + async def test_dispatcher_with_fastmcp2x(self): + """Test that the main get_fastmcp_info function correctly dispatches to v2.""" + mcp = FastMCP("Test2x") + + @mcp.tool + def test_tool() -> str: + return "test" + + info = await get_fastmcp_info(mcp) + + assert info.name == "Test2x" + assert len(info.tools) == 1 + tool_names = [tool.name for tool in info.tools] + assert "test_tool" in tool_names + + async def test_fastmcp1x_vs_fastmcp2x_comparison(self): + """Test that both versions can be inspected and compared.""" + mcp1x = FastMCP1x("Test1x") + mcp2x = FastMCP("Test2x") + + @mcp1x.tool() + def tool1x() -> str: + return "1x" + + @mcp2x.tool + def tool2x() -> str: + return "2x" + + info1x = await get_fastmcp_info(mcp1x) + info2x = await get_fastmcp_info(mcp2x) + + assert info1x.name == "Test1x" + assert info2x.name == "Test2x" + assert len(info1x.tools) == 1 + assert len(info2x.tools) == 1 + + tool1x_names = [tool.name for tool in info1x.tools] + tool2x_names = [tool.name for tool in info2x.tools] + assert "tool1x" in tool1x_names + assert "tool2x" in tool2x_names + + # Check server versions + assert info1x.server_version == "1.0" + assert info2x.server_version == fastmcp.__version__ + + # No templates added in these tests + assert len(info1x.templates) == 0 + assert len(info2x.templates) == 0 From bc1e185141d9e76fef4a99037c367ca3c974fc49 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 21 Jun 2025 16:43:40 -0400 Subject: [PATCH 20/41] Use dataclasses.asdict() for better maintainability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/cli/cli.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index 57c77da73..4b31502dc 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -492,26 +492,23 @@ def inspect( info = asyncio.run(get_info()) # Convert to dict for JSON serialization - def convert_dataclass_to_dict(obj): - """Convert dataclass instances to dicts for JSON serialization.""" - if hasattr(obj, "__dataclass_fields__"): - return { - k: convert_dataclass_to_dict(v) for k, v in obj.__dict__.items() - } - elif isinstance(obj, list): - return [convert_dataclass_to_dict(item) for item in obj] + from dataclasses import asdict + + def convert_for_json(obj): + """Convert objects for JSON serialization.""" + if isinstance(obj, list): + return [convert_for_json(item) for item in obj] elif isinstance(obj, set): return list(obj) elif hasattr(obj, "model_dump"): # Pydantic models return obj.model_dump() - elif hasattr(obj, "__dict__"): # Other objects with __dict__ - return { - k: convert_dataclass_to_dict(v) for k, v in obj.__dict__.items() - } else: return obj - info_dict = convert_dataclass_to_dict(info) + info_dict = asdict( + info, + dict_factory=lambda fields: {k: convert_for_json(v) for k, v in fields}, + ) # Ensure output directory exists output.parent.mkdir(parents=True, exist_ok=True) From 8506f784538fa304c241a50e17c1f3e9ea41b8b6 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 21 Jun 2025 20:04:39 -0400 Subject: [PATCH 21/41] Fix event loop conflict in inspect CLI command Co-Authored-By: Claude --- src/fastmcp/cli/cli.py | 38 ++-- src/fastmcp/utilities/inspect.py | 10 +- tests/cli/test_inspect.py | 354 ------------------------------- tests/utilities/test_inspect.py | 34 +-- 4 files changed, 38 insertions(+), 398 deletions(-) delete mode 100644 tests/cli/test_inspect.py diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index 4b31502dc..d3e34524e 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -3,7 +3,6 @@ import asyncio import importlib.metadata import importlib.util -import json import os import platform import subprocess @@ -13,6 +12,7 @@ from typing import Annotated import dotenv import typer +from pydantic import TypeAdapter from rich.console import Console from rich.table import Table from typer import Context, Exit @@ -21,7 +21,7 @@ import fastmcp from fastmcp.cli import claude from fastmcp.cli import run as run_module from fastmcp.server.server import FastMCP -from fastmcp.utilities.inspect import get_fastmcp_info +from fastmcp.utilities.inspect import FastMCPInfo, inspect_fastmcp from fastmcp.utilities.logging import get_logger logger = get_logger("cli") @@ -487,35 +487,29 @@ def inspect( # Get server information async def get_info(): - return await get_fastmcp_info(server) + return await inspect_fastmcp(server) - info = asyncio.run(get_info()) + try: + # Try to use existing event loop if available + asyncio.get_running_loop() + # If there's already a loop running, we need to run in a thread + import concurrent.futures - # Convert to dict for JSON serialization - from dataclasses import asdict + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(asyncio.run, get_info()) + info = future.result() + except RuntimeError: + # No running loop, safe to use asyncio.run + info = asyncio.run(get_info()) - def convert_for_json(obj): - """Convert objects for JSON serialization.""" - if isinstance(obj, list): - return [convert_for_json(item) for item in obj] - elif isinstance(obj, set): - return list(obj) - elif hasattr(obj, "model_dump"): # Pydantic models - return obj.model_dump() - else: - return obj - - info_dict = asdict( - info, - dict_factory=lambda fields: {k: convert_for_json(v) for k, v in fields}, - ) + info_json = TypeAdapter(FastMCPInfo).dump_json(info, indent=2) # Ensure output directory exists output.parent.mkdir(parents=True, exist_ok=True) # Write JSON report (always pretty-printed) with output.open("w", encoding="utf-8") as f: - json.dump(info_dict, f, indent=2, ensure_ascii=False) + f.write(info_json.decode("utf-8")) logger.info(f"Server inspection complete. Report saved to {output}") diff --git a/src/fastmcp/utilities/inspect.py b/src/fastmcp/utilities/inspect.py index 9ddd2c61f..da73acf72 100644 --- a/src/fastmcp/utilities/inspect.py +++ b/src/fastmcp/utilities/inspect.py @@ -79,7 +79,7 @@ class FastMCPInfo: capabilities: dict[str, Any] -async def get_fastmcp_info_v2(mcp: FastMCP[Any]) -> FastMCPInfo: +async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo: """Extract information from a FastMCP v2.x instance. Args: @@ -179,7 +179,7 @@ async def get_fastmcp_info_v2(mcp: FastMCP[Any]) -> FastMCPInfo: ) -async def get_fastmcp_info_v1(mcp: Any) -> FastMCPInfo: +async def inspect_fastmcp_v1(mcp: Any) -> FastMCPInfo: """Extract information from a FastMCP v1.x instance using a Client. Args: @@ -308,7 +308,7 @@ def _is_fastmcp_v1(mcp: Any) -> bool: return isinstance(mcp, FastMCP1x) and not isinstance(mcp, FastMCP) -async def get_fastmcp_info(mcp: FastMCP[Any] | Any) -> FastMCPInfo: +async def inspect_fastmcp(mcp: FastMCP[Any] | Any) -> FastMCPInfo: """Extract information from a FastMCP instance into a dataclass. This function automatically detects whether the instance is FastMCP v1.x or v2.x @@ -321,6 +321,6 @@ async def get_fastmcp_info(mcp: FastMCP[Any] | Any) -> FastMCPInfo: FastMCPInfo dataclass containing the extracted information """ if _is_fastmcp_v1(mcp): - return await get_fastmcp_info_v1(mcp) + return await inspect_fastmcp_v1(mcp) else: - return await get_fastmcp_info_v2(mcp) + return await inspect_fastmcp_v2(mcp) diff --git a/tests/cli/test_inspect.py b/tests/cli/test_inspect.py deleted file mode 100644 index fa2f992cb..000000000 --- a/tests/cli/test_inspect.py +++ /dev/null @@ -1,354 +0,0 @@ -"""Tests for the CLI inspect command.""" - -import json -import tempfile -from pathlib import Path - -from typer.testing import CliRunner - -from fastmcp.cli.cli import app - - -class TestInspectCommand: - """Tests for the fastmcp inspect CLI command.""" - - def setup_method(self): - """Set up test fixtures.""" - self.runner = CliRunner() - - def test_inspect_basic_server(self): - """Test inspecting a basic FastMCP 2.x server.""" - # Create a temporary server file - server_content = ''' -from fastmcp import FastMCP - -mcp = FastMCP("TestServer", instructions="A test server") - -@mcp.tool -def add(a: int, b: int) -> int: - """Add two numbers.""" - return a + b - -@mcp.resource("resource://data") -def get_data() -> str: - """Get test data.""" - return "test data" - -@mcp.prompt -def test_prompt(message: str) -> list: - """Test prompt.""" - return [{"role": "user", "content": message}] -''' - - with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: - f.write(server_content) - server_file = f.name - - with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: - output_file = f.name - - try: - # Run the inspect command - result = self.runner.invoke( - app, ["inspect", server_file, "-o", output_file] - ) - - assert result.exit_code == 0 - assert "✓ Inspected server: TestServer" in result.stdout - assert "Tools: 1" in result.stdout - assert "Prompts: 1" in result.stdout - assert "Resources: 1" in result.stdout - - # Check the JSON output - with open(output_file) as f: - data = json.load(f) - - assert data["name"] == "TestServer" - assert data["instructions"] == "A test server" - assert "fastmcp_version" in data - assert "mcp_version" in data - assert "server_version" in data - - # Check tools - assert len(data["tools"]) == 1 - tool = data["tools"][0] - assert tool["key"] == "add" - assert tool["name"] == "add" - assert tool["description"] == "Add two numbers." - assert "input_schema" in tool - assert tool["enabled"] is True - - # Check resources - assert len(data["resources"]) == 1 - resource = data["resources"][0] - assert resource["key"] == "resource://data" - assert resource["uri"] == "resource://data" - assert resource["name"] == "get_data" - - # Check prompts - assert len(data["prompts"]) == 1 - prompt = data["prompts"][0] - assert prompt["key"] == "test_prompt" - assert prompt["name"] == "test_prompt" - assert prompt["description"] == "Test prompt." - - # Check capabilities - assert "capabilities" in data - assert "tools" in data["capabilities"] - - finally: - # Clean up - Path(server_file).unlink(missing_ok=True) - Path(output_file).unlink(missing_ok=True) - - def test_inspect_with_object_spec(self): - """Test inspecting a server with object specification.""" - server_content = ''' -from fastmcp import FastMCP - -server = FastMCP("ObjectSpecServer") - -@server.tool -def multiply(a: int, b: int) -> int: - """Multiply two numbers.""" - return a * b -''' - - with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: - f.write(server_content) - server_file = f.name - - with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: - output_file = f.name - - try: - # Run the inspect command with object specification - result = self.runner.invoke( - app, ["inspect", f"{server_file}:server", "-o", output_file] - ) - - assert result.exit_code == 0 - assert "✓ Inspected server: ObjectSpecServer" in result.stdout - - # Check the JSON output - with open(output_file) as f: - data = json.load(f) - - assert data["name"] == "ObjectSpecServer" - assert len(data["tools"]) == 1 - assert data["tools"][0]["name"] == "multiply" - - finally: - # Clean up - Path(server_file).unlink(missing_ok=True) - Path(output_file).unlink(missing_ok=True) - - def test_inspect_default_output(self): - """Test inspecting with default output filename.""" - server_content = ''' -from fastmcp import FastMCP - -mcp = FastMCP("DefaultOutputServer") - -@mcp.tool -def test_tool() -> str: - """Test tool.""" - return "test" -''' - - with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: - f.write(server_content) - server_file = f.name - - try: - # Run the inspect command without specifying output file - result = self.runner.invoke(app, ["inspect", server_file]) - - assert result.exit_code == 0 - assert "✓ Inspected server: DefaultOutputServer" in result.stdout - assert "Report saved to: server-info.json" in result.stdout - - # Check the default output file exists - default_output = Path("server-info.json") - assert default_output.exists() - - # Check the JSON content - with open(default_output) as f: - data = json.load(f) - - assert data["name"] == "DefaultOutputServer" - - finally: - # Clean up - Path(server_file).unlink(missing_ok=True) - Path("server-info.json").unlink(missing_ok=True) - - def test_inspect_invalid_server_file(self): - """Test inspecting a non-existent server file.""" - result = self.runner.invoke( - app, ["inspect", "nonexistent.py", "-o", "output.json"] - ) - - assert result.exit_code == 1 - # The error happens at the file parsing level, so no stdout output - - def test_inspect_server_with_error(self): - """Test inspecting a server file with syntax errors.""" - server_content = """ -from fastmcp import FastMCP - -mcp = FastMCP("ErrorServer") -# Syntax error below -@mcp.tool -def broken_tool( -""" - - with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: - f.write(server_content) - server_file = f.name - - try: - result = self.runner.invoke( - app, ["inspect", server_file, "-o", "output.json"] - ) - - assert result.exit_code == 1 - assert "✗ Failed to inspect server:" in result.stdout - - finally: - # Clean up - Path(server_file).unlink(missing_ok=True) - Path("output.json").unlink(missing_ok=True) - - def test_inspect_comprehensive_json_structure(self): - """Test that the JSON output has the correct structure.""" - server_content = ''' -from fastmcp import FastMCP - -mcp = FastMCP("ComprehensiveServer", instructions="Full test server") - -@mcp.tool -def calculate(x: int, y: int) -> int: - """Calculate something.""" - return x + y - -@mcp.resource("resource://static") -def static_resource() -> str: - """Static resource.""" - return "static" - -@mcp.resource("resource://template/{id}") -def template_resource(id: str) -> str: - """Template resource.""" - return f"data-{id}" - -@mcp.prompt -def analysis_prompt(data: str) -> list: - """Analysis prompt.""" - return [{"role": "user", "content": f"Analyze: {data}"}] -''' - - with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: - f.write(server_content) - server_file = f.name - - with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: - output_file = f.name - - try: - result = self.runner.invoke( - app, ["inspect", server_file, "-o", output_file] - ) - - assert result.exit_code == 0 - - # Load and validate JSON structure - with open(output_file) as f: - data = json.load(f) - - # Check top-level structure - required_fields = [ - "name", - "instructions", - "fastmcp_version", - "mcp_version", - "server_version", - "tools", - "prompts", - "resources", - "templates", - "capabilities", - ] - for field in required_fields: - assert field in data, f"Missing field: {field}" - - # Check version fields are strings - assert isinstance(data["fastmcp_version"], str) - assert isinstance(data["mcp_version"], str) - assert isinstance(data["server_version"], str) - - # Check that we have the expected components - assert len(data["tools"]) == 1 - assert len(data["resources"]) == 1 - assert len(data["templates"]) == 1 - assert len(data["prompts"]) == 1 - - # Check tool structure - tool = data["tools"][0] - tool_fields = [ - "key", - "name", - "description", - "input_schema", - "annotations", - "tags", - "enabled", - ] - for field in tool_fields: - assert field in tool, f"Missing tool field: {field}" - - # Check resource structure - resource = data["resources"][0] - resource_fields = [ - "key", - "uri", - "name", - "description", - "mime_type", - "tags", - "enabled", - ] - for field in resource_fields: - assert field in resource, f"Missing resource field: {field}" - - # Check template structure - template = data["templates"][0] - template_fields = [ - "key", - "uri_template", - "name", - "description", - "mime_type", - "tags", - "enabled", - ] - for field in template_fields: - assert field in template, f"Missing template field: {field}" - - # Check prompt structure - prompt = data["prompts"][0] - prompt_fields = [ - "key", - "name", - "description", - "arguments", - "tags", - "enabled", - ] - for field in prompt_fields: - assert field in prompt, f"Missing prompt field: {field}" - - finally: - # Clean up - Path(server_file).unlink(missing_ok=True) - Path(output_file).unlink(missing_ok=True) diff --git a/tests/utilities/test_inspect.py b/tests/utilities/test_inspect.py index 0ca03b6c2..723738242 100644 --- a/tests/utilities/test_inspect.py +++ b/tests/utilities/test_inspect.py @@ -9,8 +9,8 @@ from fastmcp.utilities.inspect import ( FastMCPInfo, ToolInfo, _is_fastmcp_v1, - get_fastmcp_info, - get_fastmcp_info_v1, + inspect_fastmcp, + inspect_fastmcp_v1, ) @@ -69,7 +69,7 @@ class TestGetFastMCPInfo: """Test get_fastmcp_info with an empty server.""" mcp = FastMCP("EmptyServer", instructions="Empty server for testing") - info = await get_fastmcp_info(mcp) + info = await inspect_fastmcp(mcp) assert info.name == "EmptyServer" assert info.instructions == "Empty server for testing" @@ -97,7 +97,7 @@ class TestGetFastMCPInfo: def greet(name: str) -> str: return f"Hello, {name}!" - info = await get_fastmcp_info(mcp) + info = await inspect_fastmcp(mcp) assert info.name == "ToolServer" assert len(info.tools) == 2 @@ -117,7 +117,7 @@ class TestGetFastMCPInfo: def get_dynamic_data(param: str) -> str: return f"Dynamic data: {param}" - info = await get_fastmcp_info(mcp) + info = await inspect_fastmcp(mcp) assert info.name == "ResourceServer" assert len(info.resources) == 1 # Static resource @@ -139,7 +139,7 @@ class TestGetFastMCPInfo: def custom_analysis(text: str) -> list: return [{"role": "user", "content": f"Custom: {text}"}] - info = await get_fastmcp_info(mcp) + info = await inspect_fastmcp(mcp) assert info.name == "PromptServer" assert len(info.prompts) == 2 @@ -171,7 +171,7 @@ class TestGetFastMCPInfo: def analyze(content: str) -> list: return [{"role": "user", "content": content}] - info = await get_fastmcp_info(mcp) + info = await inspect_fastmcp(mcp) assert info.name == "ComprehensiveServer" assert info.instructions == "A server with everything" @@ -204,7 +204,7 @@ class TestGetFastMCPInfo: """Test get_fastmcp_info with a server that has no instructions.""" mcp = FastMCP("NoInstructionsServer") - info = await get_fastmcp_info(mcp) + info = await inspect_fastmcp(mcp) assert info.name == "NoInstructionsServer" assert info.instructions is None @@ -226,7 +226,7 @@ class TestGetFastMCPInfo: return [{"role": "user", "content": "test"}] # Get info using our function - info = await get_fastmcp_info(mcp) + info = await inspect_fastmcp(mcp) # Verify using client async with Client(mcp) as client: @@ -258,7 +258,7 @@ class TestFastMCP1xCompatibility: """Test get_fastmcp_info_v1 with an empty FastMCP1x server.""" mcp = FastMCP1x("Test1x") - info = await get_fastmcp_info_v1(mcp) + info = await inspect_fastmcp_v1(mcp) assert info.name == "Test1x" assert info.instructions is None @@ -283,7 +283,7 @@ class TestFastMCP1xCompatibility: def greet(name: str) -> str: return f"Hello, {name}!" - info = await get_fastmcp_info_v1(mcp) + info = await inspect_fastmcp_v1(mcp) assert info.name == "Test1x" assert len(info.tools) == 2 @@ -299,7 +299,7 @@ class TestFastMCP1xCompatibility: def get_data() -> str: return "Some data" - info = await get_fastmcp_info_v1(mcp) + info = await inspect_fastmcp_v1(mcp) assert info.name == "Test1x" assert len(info.resources) == 1 @@ -315,7 +315,7 @@ class TestFastMCP1xCompatibility: def analyze_data(data: str) -> list: return [{"role": "user", "content": f"Analyze: {data}"}] - info = await get_fastmcp_info_v1(mcp) + info = await inspect_fastmcp_v1(mcp) assert info.name == "Test1x" assert len(info.prompts) == 1 @@ -330,7 +330,7 @@ class TestFastMCP1xCompatibility: def test_tool() -> str: return "test" - info = await get_fastmcp_info(mcp) + info = await inspect_fastmcp(mcp) assert info.name == "Test1x" assert len(info.tools) == 1 @@ -346,7 +346,7 @@ class TestFastMCP1xCompatibility: def test_tool() -> str: return "test" - info = await get_fastmcp_info(mcp) + info = await inspect_fastmcp(mcp) assert info.name == "Test2x" assert len(info.tools) == 1 @@ -366,8 +366,8 @@ class TestFastMCP1xCompatibility: def tool2x() -> str: return "2x" - info1x = await get_fastmcp_info(mcp1x) - info2x = await get_fastmcp_info(mcp2x) + info1x = await inspect_fastmcp(mcp1x) + info2x = await inspect_fastmcp(mcp2x) assert info1x.name == "Test1x" assert info2x.name == "Test2x" From 84d5750f6d241214cf2c5ae56177c5f734f4cae5 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 21 Jun 2025 20:08:29 -0400 Subject: [PATCH 22/41] Update cli.mdx Co-Authored-By: Claude --- docs/patterns/cli.mdx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/patterns/cli.mdx b/docs/patterns/cli.mdx index 19e1ad1ab..84654975b 100644 --- a/docs/patterns/cli.mdx +++ b/docs/patterns/cli.mdx @@ -21,6 +21,7 @@ fastmcp --help | `run` | Run a FastMCP server directly | Uses your current environment; you are responsible for ensuring all dependencies are available | | `dev` | Run a server with the MCP Inspector for testing | Creates an isolated environment; dependencies must be explicitly specified with `--with` and/or `--with-editable` | | `install` | Install a server in the Claude desktop app | Creates an isolated environment; dependencies must be explicitly specified with `--with` and/or `--with-editable` | +| `inspect` | Generate a JSON report about a FastMCP server | Uses your current environment; you are responsible for ensuring all dependencies are available | | `version` | Display version information | N/A | ## Command Details @@ -179,6 +180,29 @@ fastmcp install server.py:my_server fastmcp install server.py:my_server -n "My Analysis Server" --with pandas ``` +### `inspect` + + + +Generate a detailed JSON report about a FastMCP server, including information about its tools, prompts, resources, and capabilities. + +```bash +fastmcp inspect server.py +``` + +The command supports the same server specification format as `run` and `install`: + +```bash +# Auto-detect server object +fastmcp inspect server.py + +# Specify server object +fastmcp inspect server.py:my_server + +# Custom output location +fastmcp inspect server.py --output analysis.json +``` + ### `version` Display version information about FastMCP and related components. From f2a38d275c79c230e29ffcf28778cdb08679148f Mon Sep 17 00:00:00 2001 From: Jason Cheng Date: Sun, 22 Jun 2025 14:03:50 +0800 Subject: [PATCH 23/41] custom auth configuration for remote mcp server with mcp config class --- src/fastmcp/utilities/mcp_config.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/fastmcp/utilities/mcp_config.py b/src/fastmcp/utilities/mcp_config.py index e50c97be2..40300d7eb 100644 --- a/src/fastmcp/utilities/mcp_config.py +++ b/src/fastmcp/utilities/mcp_config.py @@ -4,7 +4,8 @@ import re from typing import TYPE_CHECKING, Annotated, Any, Literal from urllib.parse import urlparse -from pydantic import AnyUrl, Field +import httpx +from pydantic import AnyUrl, ConfigDict, Field from fastmcp.utilities.types import FastMCPBaseModel @@ -59,12 +60,14 @@ class RemoteMCPServer(FastMCPBaseModel): headers: dict[str, str] = Field(default_factory=dict) transport: Literal["streamable-http", "sse"] | None = None auth: Annotated[ - str | Literal["oauth"] | None, + str | Literal["oauth"] | httpx.Auth | None, Field( - description='Either a string representing a Bearer token or the literal "oauth" to use OAuth authentication.' + description='Either a string representing a Bearer token, the literal "oauth" to use OAuth authentication, or an httpx.Auth instance for custom authentication.', ), ] = None + model_config = ConfigDict(arbitrary_types_allowed=True) + def to_transport(self) -> StreamableHttpTransport | SSETransport: from fastmcp.client.transports import SSETransport, StreamableHttpTransport From 7a793f4309f6bc0ce58064e69b68096b57aab739 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 08:34:49 -0400 Subject: [PATCH 24/41] Fix prompt argument type annotation to support mixed typing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated FunctionPrompt.render() to accept dict[str, Any] instead of dict[str, str | Context] to preserve the developer experience of passing properly typed arguments while also supporting string-only arguments from MCP clients. The _convert_string_arguments method now intelligently handles both scenarios: - Already-typed arguments are passed through unchanged - String arguments are converted to expected types when needed This maintains backward compatibility while enabling MCP spec compliance. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/prompts/prompt.py | 68 ++++++++++++++++--- tests/prompts/test_prompt.py | 124 ++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 8 deletions(-) diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index 48343e2bb..bf843d84a 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -5,13 +5,13 @@ from __future__ import annotations as _annotations import inspect from abc import ABC, abstractmethod from collections.abc import Awaitable, Callable, Sequence -from typing import TYPE_CHECKING, Any +from typing import Any import pydantic_core from mcp.types import Prompt as MCPPrompt from mcp.types import PromptArgument as MCPPromptArgument from mcp.types import PromptMessage, Role, TextContent -from pydantic import Field, TypeAdapter, validate_call +from pydantic import Field, TypeAdapter from fastmcp.exceptions import PromptError from fastmcp.server.dependencies import get_context @@ -25,10 +25,6 @@ from fastmcp.utilities.types import ( get_cached_typeadapter, ) -if TYPE_CHECKING: - pass - - logger = get_logger(__name__) @@ -189,8 +185,7 @@ class FunctionPrompt(Prompt): ) ) - # ensure the arguments are properly cast - fn = validate_call(fn) + # Store original function without validate_call to handle our own conversion return cls( name=func_name, @@ -201,6 +196,60 @@ class FunctionPrompt(Prompt): fn=fn, ) + def _convert_string_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]: + """Convert string arguments to expected types based on function signature.""" + from fastmcp.server.context import Context + + sig = inspect.signature(self.fn) + converted_kwargs = {} + + # Find context parameter name if any + context_param_name = find_kwarg_by_type(self.fn, kwarg_type=Context) + + for param_name, param_value in kwargs.items(): + if param_name in sig.parameters: + param = sig.parameters[param_name] + + # Skip Context parameters - they're handled separately + if param_name == context_param_name: + converted_kwargs[param_name] = param_value + continue + + # If parameter has no annotation or annotation is str, pass as-is + if ( + param.annotation == inspect.Parameter.empty + or param.annotation is str + ): + converted_kwargs[param_name] = param_value + # If argument is not a string, pass as-is (already properly typed) + elif not isinstance(param_value, str): + converted_kwargs[param_name] = param_value + else: + # Try to convert string argument using type adapter + try: + adapter = get_cached_typeadapter(param.annotation) + # Try JSON parsing first for complex types + try: + converted_kwargs[param_name] = adapter.validate_json( + param_value + ) + except (ValueError, TypeError, pydantic_core.ValidationError): + # Fallback to direct validation + converted_kwargs[param_name] = adapter.validate_python( + param_value + ) + except (ValueError, TypeError, pydantic_core.ValidationError) as e: + # If conversion fails, provide informative error + raise ValueError( + f"Could not convert argument '{param_name}' with value '{param_value}' " + f"to expected type {param.annotation}. Error: {e}" + ) + else: + # Parameter not in function signature, pass as-is + converted_kwargs[param_name] = param_value + + return converted_kwargs + async def render( self, arguments: dict[str, Any] | None = None, @@ -223,6 +272,9 @@ class FunctionPrompt(Prompt): if context_kwarg and context_kwarg not in kwargs: kwargs[context_kwarg] = get_context() + # Convert string arguments to expected types when needed + kwargs = self._convert_string_arguments(kwargs) + # Call function and check if result is a coroutine result = self.fn(**kwargs) if inspect.iscoroutine(result): diff --git a/tests/prompts/test_prompt.py b/tests/prompts/test_prompt.py index a0cda7b2d..2283c224d 100644 --- a/tests/prompts/test_prompt.py +++ b/tests/prompts/test_prompt.py @@ -240,3 +240,127 @@ class TestRenderPrompt: ), ) ] + + +class TestPromptTypeConversion: + async def test_list_of_integers_as_string_args(self): + """Test that prompts can handle complex types passed as strings from MCP spec.""" + + def sum_numbers(numbers: list[int]) -> str: + """Calculate the sum of a list of numbers.""" + total = sum(numbers) + return f"The sum is: {total}" + + prompt = Prompt.from_function(sum_numbers) + + # MCP spec only allows string arguments, so this should work + # after we implement type conversion + result_from_string = await prompt.render( + arguments={"numbers": "[1, 2, 3, 4, 5]"} + ) + assert result_from_string == [ + PromptMessage( + role="user", content=TextContent(type="text", text="The sum is: 15") + ) + ] + + # Both should work now with string conversion + result_from_list_string = await prompt.render( + arguments={"numbers": "[1, 2, 3, 4, 5]"} + ) + assert result_from_list_string == result_from_string + + async def test_various_type_conversions(self): + """Test type conversion for various data types.""" + + def process_data( + name: str, + age: int, + scores: list[float], + metadata: dict[str, str], + active: bool, + ) -> str: + return f"{name} ({age}): {len(scores)} scores, active={active}, metadata keys={list(metadata.keys())}" + + prompt = Prompt.from_function(process_data) + + # All arguments as strings (as MCP would send them) + result = await prompt.render( + arguments={ + "name": "Alice", + "age": "25", + "scores": "[1.5, 2.0, 3.5]", + "metadata": '{"project": "test", "version": "1.0"}', + "active": "true", + } + ) + + expected_text = ( + "Alice (25): 3 scores, active=True, metadata keys=['project', 'version']" + ) + assert result == [ + PromptMessage( + role="user", content=TextContent(type="text", text=expected_text) + ) + ] + + async def test_type_conversion_error_handling(self): + """Test that informative errors are raised for invalid type conversions.""" + from fastmcp.exceptions import PromptError + + def typed_prompt(numbers: list[int]) -> str: + return f"Got {len(numbers)} numbers" + + prompt = Prompt.from_function(typed_prompt) + + # Test with invalid JSON - should raise PromptError due to exception handling in render() + with pytest.raises(PromptError) as exc_info: + await prompt.render(arguments={"numbers": "not valid json"}) + + assert f"Error rendering prompt {prompt.name}" in str(exc_info.value) + + async def test_json_parsing_fallback(self): + """Test that JSON parsing falls back to direct validation when needed.""" + + def data_prompt(value: int) -> str: + return f"Value: {value}" + + prompt = Prompt.from_function(data_prompt) + + # This should work with JSON parsing (integer as string) + result1 = await prompt.render(arguments={"value": "42"}) + assert result1 == [ + PromptMessage( + role="user", content=TextContent(type="text", text="Value: 42") + ) + ] + + # This should work with direct validation (already an integer string) + result2 = await prompt.render(arguments={"value": "123"}) + assert result2 == [ + PromptMessage( + role="user", content=TextContent(type="text", text="Value: 123") + ) + ] + + async def test_mixed_string_and_typed_args(self): + """Test mixing string args (no conversion) with typed args (conversion needed).""" + + def mixed_prompt(message: str, count: int) -> str: + return f"{message} (repeated {count} times)" + + prompt = Prompt.from_function(mixed_prompt) + + result = await prompt.render( + arguments={ + "message": "Hello world", # str - no conversion needed + "count": "3", # int - conversion needed + } + ) + + assert result == [ + PromptMessage( + role="user", + content=TextContent(type="text", text="Hello world (repeated 3 times)"), + ) + ] From ef1368fda5f60c7b8def87bc30c1d9329035e756 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 08:35:52 -0400 Subject: [PATCH 25/41] Remove unclear comment about validate_call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/prompts/prompt.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index bf843d84a..37cf7c720 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -185,8 +185,6 @@ class FunctionPrompt(Prompt): ) ) - # Store original function without validate_call to handle our own conversion - return cls( name=func_name, description=description, From 817018bf3b45a5f12f57acd3d8ed96f565dfe5dd Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 09:38:57 -0400 Subject: [PATCH 26/41] Add automatic JSON schema descriptions for non-string prompt arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix ValueError -> PromptError for consistent error handling - Add automatic JSON schema descriptions to non-string prompt arguments - Include comprehensive tests for argument description enhancement - Verify enhanced descriptions are visible via MCP protocol This helps developers understand the expected string format for complex types when calling prompts from MCP clients. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/prompts/prompt.py | 34 ++++++++- tests/prompts/test_prompt.py | 95 ++++++++++++++++++++++++ tests/server/test_server_interactions.py | 52 +++++++++++++ 3 files changed, 179 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index 37cf7c720..b88001662 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -3,6 +3,7 @@ from __future__ import annotations as _annotations import inspect +import json from abc import ABC, abstractmethod from collections.abc import Awaitable, Callable, Sequence from typing import Any @@ -177,10 +178,39 @@ class FunctionPrompt(Prompt): arguments: list[PromptArgument] = [] if "properties" in parameters: for param_name, param in parameters["properties"].items(): + arg_description = param.get("description") + + # For non-string parameters, append JSON schema info to help users + # understand the expected format when passing as strings (MCP requirement) + if param_name in sig.parameters: + sig_param = sig.parameters[param_name] + if ( + sig_param.annotation != inspect.Parameter.empty + and sig_param.annotation is not str + and param_name != context_kwarg + ): + # Get the JSON schema for this specific parameter type + try: + param_adapter = get_cached_typeadapter(sig_param.annotation) + param_schema = param_adapter.json_schema() + + # Create compact schema representation + schema_str = json.dumps(param_schema, separators=(",", ":")) + + # Append schema info to description + schema_note = f"Arguments must be strings conforming to this JSON schema: {schema_str}" + if arg_description: + arg_description = f"{arg_description}\n\n{schema_note}" + else: + arg_description = schema_note + except Exception: + # If schema generation fails, skip enhancement + pass + arguments.append( PromptArgument( name=param_name, - description=param.get("description"), + description=arg_description, required=param_name in parameters.get("required", []), ) ) @@ -238,7 +268,7 @@ class FunctionPrompt(Prompt): ) except (ValueError, TypeError, pydantic_core.ValidationError) as e: # If conversion fails, provide informative error - raise ValueError( + raise PromptError( f"Could not convert argument '{param_name}' with value '{param_value}' " f"to expected type {param.annotation}. Error: {e}" ) diff --git a/tests/prompts/test_prompt.py b/tests/prompts/test_prompt.py index 2283c224d..47d241ae9 100644 --- a/tests/prompts/test_prompt.py +++ b/tests/prompts/test_prompt.py @@ -364,3 +364,98 @@ class TestPromptTypeConversion: content=TextContent(type="text", text="Hello world (repeated 3 times)"), ) ] + + +class TestPromptArgumentDescriptions: + def test_enhanced_descriptions_for_non_string_types(self): + """Test that non-string argument types get enhanced descriptions with JSON schema.""" + + def analyze_data( + name: str, + numbers: list[int], + metadata: dict[str, str], + threshold: float, + active: bool, + ) -> str: + """Analyze numerical data.""" + return f"Analyzed {name}" + + prompt = Prompt.from_function(analyze_data) + + # Check that string parameter has no schema enhancement + name_arg = next(arg for arg in prompt.arguments if arg.name == "name") + assert name_arg.description is None # No enhancement for string types + + # Check that non-string parameters have schema enhancements + numbers_arg = next(arg for arg in prompt.arguments if arg.name == "numbers") + assert ( + "Arguments must be strings conforming to this JSON schema:" + in numbers_arg.description + ) + assert '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description + + metadata_arg = next(arg for arg in prompt.arguments if arg.name == "metadata") + assert ( + "Arguments must be strings conforming to this JSON schema:" + in metadata_arg.description + ) + assert ( + '{"additionalProperties":{"type":"string"},"type":"object"}' + in metadata_arg.description + ) + + threshold_arg = next(arg for arg in prompt.arguments if arg.name == "threshold") + assert ( + "Arguments must be strings conforming to this JSON schema:" + in threshold_arg.description + ) + assert '{"type":"number"}' in threshold_arg.description + + active_arg = next(arg for arg in prompt.arguments if arg.name == "active") + assert ( + "Arguments must be strings conforming to this JSON schema:" + in active_arg.description + ) + assert '{"type":"boolean"}' in active_arg.description + + def test_enhanced_descriptions_with_existing_descriptions(self): + """Test that existing parameter descriptions are preserved with schema appended.""" + from typing import Annotated + + from pydantic import Field + + def documented_prompt( + numbers: Annotated[ + list[int], Field(description="A list of integers to process") + ], + ) -> str: + """Process numbers.""" + return "processed" + + prompt = Prompt.from_function(documented_prompt) + + numbers_arg = next(arg for arg in prompt.arguments if arg.name == "numbers") + # Should have both the original description and the schema + assert numbers_arg.description is not None + assert "A list of integers to process" in numbers_arg.description + assert "\n\n" in numbers_arg.description # Should have newline separator + assert ( + "Arguments must be strings conforming to this JSON schema:" + in numbers_arg.description + ) + + def test_string_parameters_no_enhancement(self): + """Test that string parameters don't get schema enhancement.""" + + def string_only_prompt(message: str, name: str) -> str: + return f"{message}, {name}" + + prompt = Prompt.from_function(string_only_prompt) + + for arg in prompt.arguments: + # String parameters should not have schema enhancement + if arg.description: + assert ( + "Arguments must be strings conforming to this JSON schema:" + not in arg.description + ) diff --git a/tests/server/test_server_interactions.py b/tests/server/test_server_interactions.py index 6198a9f70..485621a7d 100644 --- a/tests/server/test_server_interactions.py +++ b/tests/server/test_server_interactions.py @@ -1785,6 +1785,58 @@ class TestPrompts: assert prompts[0].arguments[1].name == "optional" assert prompts[0].arguments[1].required is False + async def test_list_prompts_with_enhanced_descriptions(self): + """Test that enhanced descriptions with JSON schema are visible via MCP protocol.""" + mcp = FastMCP() + + @mcp.prompt + def analyze_data( + name: str, numbers: list[int], metadata: dict[str, str], threshold: float + ) -> str: + """Analyze some data.""" + return f"Analyzed {name}" + + async with Client(mcp) as client: + prompts = await client.list_prompts() + assert len(prompts) == 1 + prompt = prompts[0] + assert prompt.name == "analyze_data" + assert prompt.description == "Analyze some data." + + # Find each argument and verify schema enhancements + args_by_name = {arg.name: arg for arg in prompt.arguments} + + # String parameter should not have schema enhancement + name_arg = args_by_name["name"] + assert name_arg.description is None + + # Non-string parameters should have schema enhancements + numbers_arg = args_by_name["numbers"] + assert ( + "Arguments must be strings conforming to this JSON schema:" + in numbers_arg.description + ) + assert ( + '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description + ) + + metadata_arg = args_by_name["metadata"] + assert ( + "Arguments must be strings conforming to this JSON schema:" + in metadata_arg.description + ) + assert ( + '{"additionalProperties":{"type":"string"},"type":"object"}' + in metadata_arg.description + ) + + threshold_arg = args_by_name["threshold"] + assert ( + "Arguments must be strings conforming to this JSON schema:" + in threshold_arg.description + ) + assert '{"type":"number"}' in threshold_arg.description + async def test_get_prompt(self): """Test getting a prompt through MCP protocol.""" mcp = FastMCP() From 7114242e1eebce6a3d657f5e6af19afd01133493 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 09:46:11 -0400 Subject: [PATCH 27/41] Update schema description wording for clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change from 'Arguments must be strings conforming to this JSON schema' to 'Provide as a JSON string matching the following schema' for clearer instruction to LLMs about string format requirements. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/prompts/prompt.py | 2 +- tests/prompts/test_prompt.py | 35 +++++------------------- tests/server/test_server_interactions.py | 24 ++++------------ 3 files changed, 13 insertions(+), 48 deletions(-) diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index b88001662..b0c99e971 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -198,7 +198,7 @@ class FunctionPrompt(Prompt): schema_str = json.dumps(param_schema, separators=(",", ":")) # Append schema info to description - schema_note = f"Arguments must be strings conforming to this JSON schema: {schema_str}" + schema_note = f"Provide as a JSON string matching the following schema: {schema_str}" if arg_description: arg_description = f"{arg_description}\n\n{schema_note}" else: diff --git a/tests/prompts/test_prompt.py b/tests/prompts/test_prompt.py index 47d241ae9..e43313860 100644 --- a/tests/prompts/test_prompt.py +++ b/tests/prompts/test_prompt.py @@ -388,34 +388,19 @@ class TestPromptArgumentDescriptions: # Check that non-string parameters have schema enhancements numbers_arg = next(arg for arg in prompt.arguments if arg.name == "numbers") - assert ( - "Arguments must be strings conforming to this JSON schema:" - in numbers_arg.description - ) + assert "Provide as a JSON string matching the following schema:" in numbers_arg.description assert '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description metadata_arg = next(arg for arg in prompt.arguments if arg.name == "metadata") - assert ( - "Arguments must be strings conforming to this JSON schema:" - in metadata_arg.description - ) - assert ( - '{"additionalProperties":{"type":"string"},"type":"object"}' - in metadata_arg.description - ) + assert "Provide as a JSON string matching the following schema:" in metadata_arg.description + assert '{"additionalProperties":{"type":"string"},"type":"object"}' in metadata_arg.description threshold_arg = next(arg for arg in prompt.arguments if arg.name == "threshold") - assert ( - "Arguments must be strings conforming to this JSON schema:" - in threshold_arg.description - ) + assert "Provide as a JSON string matching the following schema:" in threshold_arg.description assert '{"type":"number"}' in threshold_arg.description active_arg = next(arg for arg in prompt.arguments if arg.name == "active") - assert ( - "Arguments must be strings conforming to this JSON schema:" - in active_arg.description - ) + assert "Provide as a JSON string matching the following schema:" in active_arg.description assert '{"type":"boolean"}' in active_arg.description def test_enhanced_descriptions_with_existing_descriptions(self): @@ -439,10 +424,7 @@ class TestPromptArgumentDescriptions: assert numbers_arg.description is not None assert "A list of integers to process" in numbers_arg.description assert "\n\n" in numbers_arg.description # Should have newline separator - assert ( - "Arguments must be strings conforming to this JSON schema:" - in numbers_arg.description - ) + assert "Provide as a JSON string matching the following schema:" in numbers_arg.description def test_string_parameters_no_enhancement(self): """Test that string parameters don't get schema enhancement.""" @@ -455,7 +437,4 @@ class TestPromptArgumentDescriptions: for arg in prompt.arguments: # String parameters should not have schema enhancement if arg.description: - assert ( - "Arguments must be strings conforming to this JSON schema:" - not in arg.description - ) + assert "Provide as a JSON string matching the following schema:" not in arg.description diff --git a/tests/server/test_server_interactions.py b/tests/server/test_server_interactions.py index 485621a7d..28797e521 100644 --- a/tests/server/test_server_interactions.py +++ b/tests/server/test_server_interactions.py @@ -1812,29 +1812,15 @@ class TestPrompts: # Non-string parameters should have schema enhancements numbers_arg = args_by_name["numbers"] - assert ( - "Arguments must be strings conforming to this JSON schema:" - in numbers_arg.description - ) - assert ( - '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description - ) + assert "Provide as a JSON string matching the following schema:" in numbers_arg.description + assert '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description metadata_arg = args_by_name["metadata"] - assert ( - "Arguments must be strings conforming to this JSON schema:" - in metadata_arg.description - ) - assert ( - '{"additionalProperties":{"type":"string"},"type":"object"}' - in metadata_arg.description - ) + assert "Provide as a JSON string matching the following schema:" in metadata_arg.description + assert '{"additionalProperties":{"type":"string"},"type":"object"}' in metadata_arg.description threshold_arg = args_by_name["threshold"] - assert ( - "Arguments must be strings conforming to this JSON schema:" - in threshold_arg.description - ) + assert "Provide as a JSON string matching the following schema:" in threshold_arg.description assert '{"type":"number"}' in threshold_arg.description async def test_get_prompt(self): From 7dd2a13ec23dd006d27d5e8810a8b8cb7e6fa2d9 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 09:52:52 -0400 Subject: [PATCH 28/41] Update docs Co-Authored-By: Claude --- docs/servers/prompts.mdx | 103 +++++++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 27 deletions(-) diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx index 4862e0c2a..02fa4991e 100644 --- a/docs/servers/prompts.mdx +++ b/docs/servers/prompts.mdx @@ -57,6 +57,82 @@ def generate_code_request(language: str, task_description: str) -> PromptMessage Functions with `*args` or `**kwargs` are not supported as prompts. This restriction exists because FastMCP needs to generate a complete parameter schema for the MCP protocol, which isn't possible with variable argument lists. +### Argument Types + +The MCP specification requires that all prompt arguments be passed as strings, but FastMCP allows you to use typed annotations for better developer experience. When you use complex types like `list[int]` or `dict[str, str]`, FastMCP: + +1. **Automatically converts** string arguments from MCP clients to the expected types +2. **Generates helpful descriptions** showing the exact JSON string format needed +3. **Preserves direct usage** - you can still call prompts with properly typed arguments + +Since the MCP specification only allows string arguments, clients need to know what string format to use for complex types. FastMCP solves this by automatically enhancing the argument descriptions with JSON schema information, making it clear to both humans and LLMs how to format their arguments. + + + +```python Python Code +@mcp.prompt +def analyze_data( + numbers: list[int], + metadata: dict[str, str], + threshold: float +) -> str: + """Analyze numerical data.""" + avg = sum(numbers) / len(numbers) + return f"Average: {avg}, above threshold: {avg > threshold}" +``` + +```json Resulting MCP Prompt +{ + "name": "analyze_data", + "description": "Analyze numerical data.", + "arguments": [ + { + "name": "numbers", + "description": "Provide as a JSON string matching the following schema: {\"items\":{\"type\":\"integer\"},\"type\":\"array\"}", + "required": true + }, + { + "name": "metadata", + "description": "Provide as a JSON string matching the following schema: {\"additionalProperties\":{\"type\":\"string\"},\"type\":\"object\"}", + "required": true + }, + { + "name": "threshold", + "description": "Provide as a JSON string matching the following schema: {\"type\":\"number\"}", + "required": true + } + ] +} +``` + + + +**MCP clients will call this prompt with string arguments:** +```json +{ + "numbers": "[1, 2, 3, 4, 5]", + "metadata": "{\"source\": \"api\", \"version\": \"1.0\"}", + "threshold": "2.5" +} +``` + +**But you can still call it directly with proper types:** +```python +# This also works for direct calls +result = await prompt.render({ + "numbers": [1, 2, 3, 4, 5], + "metadata": {"source": "api", "version": "1.0"}, + "threshold": 2.5 +}) +``` + + +Keep your type annotations simple when using this feature. Complex nested types or custom classes may not convert reliably from JSON strings. The automatically generated schema descriptions are the only guidance users receive about the expected format. + +Good choices: `list[int]`, `dict[str, str]`, `float`, `bool` +Avoid: Complex Pydantic models, deeply nested structures, custom classes + + ### Return Values FastMCP intelligently handles different return types from your prompt function: @@ -78,33 +154,6 @@ def roleplay_scenario(character: str, situation: str) -> list[Message]: ] ``` -### Type Annotations - -Type annotations are important for prompts. They: -1. Inform FastMCP about the expected types for each parameter. -2. Allow validation of parameters received from clients. -3. Are used to generate the prompt's schema for the MCP protocol. - -```python -from pydantic import Field -from typing import Literal, Optional - -@mcp.prompt -def generate_content_request( - topic: str = Field(description="The main subject to cover"), - format: Literal["blog", "email", "social"] = "blog", - tone: str = "professional", - word_count: Optional[int] = None -) -> str: - """Create a request for generating content in a specific format.""" - prompt = f"Please write a {format} post about {topic} in a {tone} tone." - - if word_count: - prompt += f" It should be approximately {word_count} words long." - - return prompt -``` - ### Required vs. Optional Parameters From 9714028cac52b9e6aa91d206d3a031db23dfe649 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 09:55:40 -0400 Subject: [PATCH 29/41] Fix pyright type checking issues in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add proper null checks and type assertions for prompt argument handling in tests to satisfy pyright strict typing. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- tests/prompts/test_prompt.py | 72 +++++++++++++++++++----- tests/server/test_server_interactions.py | 28 +++++++-- 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/tests/prompts/test_prompt.py b/tests/prompts/test_prompt.py index e43313860..be5d0a1f3 100644 --- a/tests/prompts/test_prompt.py +++ b/tests/prompts/test_prompt.py @@ -382,25 +382,58 @@ class TestPromptArgumentDescriptions: prompt = Prompt.from_function(analyze_data) + assert prompt.arguments is not None # Check that string parameter has no schema enhancement - name_arg = next(arg for arg in prompt.arguments if arg.name == "name") + name_arg = next((arg for arg in prompt.arguments if arg.name == "name"), None) + assert name_arg is not None assert name_arg.description is None # No enhancement for string types # Check that non-string parameters have schema enhancements - numbers_arg = next(arg for arg in prompt.arguments if arg.name == "numbers") - assert "Provide as a JSON string matching the following schema:" in numbers_arg.description + numbers_arg = next( + (arg for arg in prompt.arguments if arg.name == "numbers"), None + ) + assert numbers_arg is not None + assert numbers_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in numbers_arg.description + ) assert '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description - metadata_arg = next(arg for arg in prompt.arguments if arg.name == "metadata") - assert "Provide as a JSON string matching the following schema:" in metadata_arg.description - assert '{"additionalProperties":{"type":"string"},"type":"object"}' in metadata_arg.description + metadata_arg = next( + (arg for arg in prompt.arguments if arg.name == "metadata"), None + ) + assert metadata_arg is not None + assert metadata_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in metadata_arg.description + ) + assert ( + '{"additionalProperties":{"type":"string"},"type":"object"}' + in metadata_arg.description + ) - threshold_arg = next(arg for arg in prompt.arguments if arg.name == "threshold") - assert "Provide as a JSON string matching the following schema:" in threshold_arg.description + threshold_arg = next( + (arg for arg in prompt.arguments if arg.name == "threshold"), None + ) + assert threshold_arg is not None + assert threshold_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in threshold_arg.description + ) assert '{"type":"number"}' in threshold_arg.description - active_arg = next(arg for arg in prompt.arguments if arg.name == "active") - assert "Provide as a JSON string matching the following schema:" in active_arg.description + active_arg = next( + (arg for arg in prompt.arguments if arg.name == "active"), None + ) + assert active_arg is not None + assert active_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in active_arg.description + ) assert '{"type":"boolean"}' in active_arg.description def test_enhanced_descriptions_with_existing_descriptions(self): @@ -419,12 +452,19 @@ class TestPromptArgumentDescriptions: prompt = Prompt.from_function(documented_prompt) - numbers_arg = next(arg for arg in prompt.arguments if arg.name == "numbers") + assert prompt.arguments is not None + numbers_arg = next( + (arg for arg in prompt.arguments if arg.name == "numbers"), None + ) + assert numbers_arg is not None # Should have both the original description and the schema assert numbers_arg.description is not None assert "A list of integers to process" in numbers_arg.description assert "\n\n" in numbers_arg.description # Should have newline separator - assert "Provide as a JSON string matching the following schema:" in numbers_arg.description + assert ( + "Provide as a JSON string matching the following schema:" + in numbers_arg.description + ) def test_string_parameters_no_enhancement(self): """Test that string parameters don't get schema enhancement.""" @@ -434,7 +474,11 @@ class TestPromptArgumentDescriptions: prompt = Prompt.from_function(string_only_prompt) + assert prompt.arguments is not None for arg in prompt.arguments: # String parameters should not have schema enhancement - if arg.description: - assert "Provide as a JSON string matching the following schema:" not in arg.description + if arg.description is not None: + assert ( + "Provide as a JSON string matching the following schema:" + not in arg.description + ) diff --git a/tests/server/test_server_interactions.py b/tests/server/test_server_interactions.py index 28797e521..11ca06b26 100644 --- a/tests/server/test_server_interactions.py +++ b/tests/server/test_server_interactions.py @@ -1804,6 +1804,7 @@ class TestPrompts: assert prompt.description == "Analyze some data." # Find each argument and verify schema enhancements + assert prompt.arguments is not None args_by_name = {arg.name: arg for arg in prompt.arguments} # String parameter should not have schema enhancement @@ -1812,15 +1813,32 @@ class TestPrompts: # Non-string parameters should have schema enhancements numbers_arg = args_by_name["numbers"] - assert "Provide as a JSON string matching the following schema:" in numbers_arg.description - assert '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description + assert numbers_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in numbers_arg.description + ) + assert ( + '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description + ) metadata_arg = args_by_name["metadata"] - assert "Provide as a JSON string matching the following schema:" in metadata_arg.description - assert '{"additionalProperties":{"type":"string"},"type":"object"}' in metadata_arg.description + assert metadata_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in metadata_arg.description + ) + assert ( + '{"additionalProperties":{"type":"string"},"type":"object"}' + in metadata_arg.description + ) threshold_arg = args_by_name["threshold"] - assert "Provide as a JSON string matching the following schema:" in threshold_arg.description + assert threshold_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in threshold_arg.description + ) assert '{"type":"number"}' in threshold_arg.description async def test_get_prompt(self): From 4a038d8f5489e4afad52ebb948ead676d59b72ce Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 09:57:25 -0400 Subject: [PATCH 30/41] Update prompts.mdx Co-Authored-By: Claude --- docs/servers/prompts.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx index 02fa4991e..80c799781 100644 --- a/docs/servers/prompts.mdx +++ b/docs/servers/prompts.mdx @@ -59,6 +59,8 @@ Functions with `*args` or `**kwargs` are not supported as prompts. This restrict ### Argument Types + + The MCP specification requires that all prompt arguments be passed as strings, but FastMCP allows you to use typed annotations for better developer experience. When you use complex types like `list[int]` or `dict[str, str]`, FastMCP: 1. **Automatically converts** string arguments from MCP clients to the expected types From a7f14d90a42cc62e40253368a2bfee4a81e58fe0 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 10:18:48 -0400 Subject: [PATCH 31/41] Implement client-side argument serialization with focused tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add pydantic_core.to_json() serialization for non-string prompt arguments - Update type annotations to accept dict[str, Any] instead of dict[str, str] - Add focused tests covering specific scenarios: * Client always serializes non-string args regardless of server types * Integration with server-side type conversion * Client serialization error with specific PydanticSerializationError * Server deserialization error with specific McpError match This ensures MCP protocol compliance while maintaining developer experience with typed arguments. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/client/client.py | 24 ++++++++-- tests/client/test_client.py | 93 ++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 5 deletions(-) diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py index 26b14586b..c0e78a2d5 100644 --- a/src/fastmcp/client/client.py +++ b/src/fastmcp/client/client.py @@ -7,6 +7,7 @@ from typing import Any, Generic, Literal, cast, overload import anyio import httpx import mcp.types +import pydantic_core from exceptiongroup import catch from mcp import ClientSession from pydantic import AnyUrl @@ -508,13 +509,13 @@ class Client(Generic[ClientTransportT]): # --- Prompt --- async def get_prompt_mcp( - self, name: str, arguments: dict[str, str] | None = None + self, name: str, arguments: dict[str, Any] | None = None ) -> mcp.types.GetPromptResult: """Send a prompts/get request and return the complete MCP protocol result. Args: name (str): The name of the prompt to retrieve. - arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None. + arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None. Returns: mcp.types.GetPromptResult: The complete response object from the protocol, @@ -523,17 +524,30 @@ class Client(Generic[ClientTransportT]): Raises: RuntimeError: If called while the client is not connected. """ - result = await self.session.get_prompt(name=name, arguments=arguments) + # Serialize arguments for MCP protocol - convert non-string values to JSON + serialized_arguments: dict[str, str] | None = None + if arguments: + serialized_arguments = {} + for key, value in arguments.items(): + if isinstance(value, str): + serialized_arguments[key] = value + else: + # Use pydantic_core.to_json for consistent serialization + serialized_arguments[key] = pydantic_core.to_json(value).decode() + + result = await self.session.get_prompt( + name=name, arguments=serialized_arguments + ) return result async def get_prompt( - self, name: str, arguments: dict[str, str] | None = None + self, name: str, arguments: dict[str, Any] | None = None ) -> mcp.types.GetPromptResult: """Retrieve a rendered prompt message list from the server. Args: name (str): The name of the prompt to retrieve. - arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None. + arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None. Returns: mcp.types.GetPromptResult: The complete response object from the protocol, diff --git a/tests/client/test_client.py b/tests/client/test_client.py index f792a15e0..991f23de9 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -220,6 +220,99 @@ async def test_get_prompt_mcp(fastmcp_server): assert result.description == "Example greeting prompt." +async def test_client_serializes_all_non_string_arguments(): + """Test that client always serializes non-string arguments to JSON, regardless of server types.""" + server = FastMCP("TestServer") + + @server.prompt + def echo_args(arg1: str, arg2: str, arg3: str) -> str: + """Server accepts all string args but client sends mixed types.""" + return f"arg1: {arg1}, arg2: {arg2}, arg3: {arg3}" + + client = Client(transport=FastMCPTransport(server)) + + async with client: + result = await client.get_prompt( + "echo_args", + { + "arg1": "hello", # string - should pass through + "arg2": [1, 2, 3], # list - should be JSON serialized + "arg3": {"key": "value"}, # dict - should be JSON serialized + }, + ) + + content = result.messages[0].content.text # type: ignore[attr-defined] + assert "arg1: hello" in content + assert "arg2: [1,2,3]" in content # JSON serialized list + assert 'arg3: {"key":"value"}' in content # JSON serialized dict + + +async def test_client_server_type_conversion_integration(): + """Test that client serialization works with server-side type conversion.""" + server = FastMCP("TestServer") + + @server.prompt + def typed_prompt(numbers: list[int], config: dict[str, str]) -> str: + """Server expects typed args - will convert from JSON strings.""" + return f"Got {len(numbers)} numbers and {len(config)} config items" + + client = Client(transport=FastMCPTransport(server)) + + async with client: + result = await client.get_prompt( + "typed_prompt", + {"numbers": [1, 2, 3, 4], "config": {"theme": "dark", "lang": "en"}}, + ) + + content = result.messages[0].content.text # type: ignore[attr-defined] + assert "Got 4 numbers and 2 config items" in content + + +async def test_client_serialization_error(): + """Test client error when object cannot be serialized.""" + import pydantic_core + + server = FastMCP("TestServer") + + @server.prompt + def any_prompt(data: str) -> str: + return f"Got: {data}" + + # Create an unserializable object + class UnserializableClass: + def __init__(self): + self.func = lambda x: x # functions can't be JSON serialized + + client = Client(transport=FastMCPTransport(server)) + + async with client: + with pytest.raises(pydantic_core.PydanticSerializationError, match="Unable to serialize"): + await client.get_prompt("any_prompt", {"data": UnserializableClass()}) + + +async def test_server_deserialization_error(): + """Test server error when JSON string cannot be converted to expected type.""" + from mcp import McpError + + server = FastMCP("TestServer") + + @server.prompt + def strict_typed_prompt(numbers: list[int]) -> str: + """Expects list of integers but will receive invalid JSON.""" + return f"Got {len(numbers)} numbers" + + client = Client(transport=FastMCPTransport(server)) + + async with client: + with pytest.raises(McpError, match="Error rendering prompt"): + await client.get_prompt( + "strict_typed_prompt", + { + "numbers": "not valid json" # This will fail server-side conversion + }, + ) + + async def test_read_resource_invalid_uri(fastmcp_server): """Test reading a resource with an invalid URI.""" client = Client(transport=FastMCPTransport(fastmcp_server)) From dbf125b6ac4114944e217440bcfb55279cf771a3 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 10:20:03 -0400 Subject: [PATCH 32/41] Update client.mdx Co-Authored-By: Claude --- docs/clients/client.mdx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx index 3c4db65aa..2c097724f 100644 --- a/docs/clients/client.mdx +++ b/docs/clients/client.mdx @@ -234,6 +234,30 @@ The standard client methods return user-friendly representations that may change * **`list_prompts()`**: Retrieves available prompt templates. * **`get_prompt(name: str, arguments: dict[str, Any] | None = None)`**: Retrieves a rendered prompt message list. + + +**Automatic Argument Serialization**: When calling prompts with complex arguments, the FastMCP client automatically serializes non-string values to JSON strings as required by the MCP specification. This allows you to pass typed objects directly while maintaining protocol compliance. + +```python +from dataclasses import dataclass + +@dataclass +class UserData: + name: str + age: int + +async with client: + # You can pass complex objects directly + result = await client.get_prompt("analyze_user", { + "user": UserData(name="Alice", age=30), # Automatically serialized to JSON + "preferences": {"theme": "dark"}, # Dict serialized to JSON string + "scores": [85, 92, 78], # List serialized to JSON string + "simple_name": "Bob" # Strings passed through unchanged + }) +``` + +The client handles the serialization automatically using `pydantic_core.to_json()` for consistent formatting, while the server can deserialize these JSON strings back to the expected types if using FastMCP's server-side type conversion. + ### Raw MCP Protocol Objects From 73e1aa21122de3c6bd162f5fa3b8e632b714c721 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 10:29:15 -0400 Subject: [PATCH 33/41] Apply pre-commit formatting changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- tests/client/test_client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 991f23de9..55210c432 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -271,7 +271,7 @@ async def test_client_server_type_conversion_integration(): async def test_client_serialization_error(): """Test client error when object cannot be serialized.""" import pydantic_core - + server = FastMCP("TestServer") @server.prompt @@ -286,7 +286,9 @@ async def test_client_serialization_error(): client = Client(transport=FastMCPTransport(server)) async with client: - with pytest.raises(pydantic_core.PydanticSerializationError, match="Unable to serialize"): + with pytest.raises( + pydantic_core.PydanticSerializationError, match="Unable to serialize" + ): await client.get_prompt("any_prompt", {"data": UnserializableClass()}) From 32269f514094dc0dc3dca745edd44300c1f0ad73 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 10:33:22 -0400 Subject: [PATCH 34/41] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d7f921d0f..143d664f7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -fail_fast: true +fail_fast: false repos: - repo: https://github.com/abravalheri/validate-pyproject From a19a84e7a891318cccc798fcf28537eaaebeff89 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 10:33:59 -0400 Subject: [PATCH 35/41] Update src/fastmcp/client/client.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/fastmcp/client/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py index c0e78a2d5..952a94796 100644 --- a/src/fastmcp/client/client.py +++ b/src/fastmcp/client/client.py @@ -533,7 +533,7 @@ class Client(Generic[ClientTransportT]): serialized_arguments[key] = value else: # Use pydantic_core.to_json for consistent serialization - serialized_arguments[key] = pydantic_core.to_json(value).decode() + serialized_arguments[key] = pydantic_core.to_json(value).decode('utf-8') result = await self.session.get_prompt( name=name, arguments=serialized_arguments From 993f3d7979d4db3a0f6519d54b0dff0629809a15 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 10:35:37 -0400 Subject: [PATCH 36/41] Update client.py --- src/fastmcp/client/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py index 952a94796..203fcea14 100644 --- a/src/fastmcp/client/client.py +++ b/src/fastmcp/client/client.py @@ -533,7 +533,9 @@ class Client(Generic[ClientTransportT]): serialized_arguments[key] = value else: # Use pydantic_core.to_json for consistent serialization - serialized_arguments[key] = pydantic_core.to_json(value).decode('utf-8') + serialized_arguments[key] = pydantic_core.to_json(value).decode( + "utf-8" + ) result = await self.session.get_prompt( name=name, arguments=serialized_arguments From 8e98d711fc129357ab0e982836fb572dca69697b Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 13:18:28 -0400 Subject: [PATCH 37/41] Update client docs Co-Authored-By: Claude --- docs/clients/advanced-features.mdx | 152 ---------- docs/clients/client.mdx | 456 +++++++++-------------------- docs/clients/logging.mdx | 63 ++++ docs/clients/progress.mdx | 59 ++++ docs/clients/prompts.mdx | 187 ++++++++++++ docs/clients/resources.mdx | 171 +++++++++++ docs/clients/roots.mdx | 42 +++ docs/clients/sampling.mdx | 94 ++++++ docs/clients/tools.mdx | 143 +++++++++ docs/docs.json | 22 +- justfile | 5 +- 11 files changed, 916 insertions(+), 478 deletions(-) delete mode 100644 docs/clients/advanced-features.mdx create mode 100644 docs/clients/logging.mdx create mode 100644 docs/clients/progress.mdx create mode 100644 docs/clients/prompts.mdx create mode 100644 docs/clients/resources.mdx create mode 100644 docs/clients/roots.mdx create mode 100644 docs/clients/sampling.mdx create mode 100644 docs/clients/tools.mdx diff --git a/docs/clients/advanced-features.mdx b/docs/clients/advanced-features.mdx deleted file mode 100644 index cee3c461b..000000000 --- a/docs/clients/advanced-features.mdx +++ /dev/null @@ -1,152 +0,0 @@ ---- -title: Advanced Features -sidebarTitle: Advanced Features -description: Learn about the advanced features of the FastMCP Client. -icon: stars ---- - -import { VersionBadge } from '/snippets/version-badge.mdx' - -In addition to basic server interaction, FastMCP clients can also handle more advanced features and server interaction patterns. The `Client` constructor accepts additional configuration to handle these server requests. - - -To enable many of these features, you must provide an appropriate handler or callback function. For example. In most cases, if you do not provide a handler, FastMCP's default handler will emit a `DEBUG` level log. - - -## Logging and Notifications - - -MCP servers can emit logs to clients. To process these logs, you can provide a `log_handler` to the client. - -The `log_handler` must be an async function that accepts a single argument, which is an instance of `fastmcp.client.logging.LogMessage`. This has attributes like `level`, `logger`, and `data`. - -```python {2, 12} -from fastmcp import Client -from fastmcp.client.logging import LogMessage - -async def log_handler(message: LogMessage): - level = message.level.upper() - logger = message.logger or 'default' - data = message.data - print(f"[Server Log - {level}] {logger}: {data}") - -client_with_logging = Client( - ..., - log_handler=log_handler, -) -``` -## Progress Monitoring - - - -MCP servers can report progress during long-running operations. The client can set a progress handler to receive and process these updates. - -```python {2, 13} -from fastmcp import Client -from fastmcp.client.progress import ProgressHandler - -async def my_progress_handler( - progress: float, - total: float | None, - message: str | None -) -> None: - print(f"Progress: {progress} / {total} ({message})") - -client = Client( - ..., - progress_handler=my_progress_handler -) -``` - -By default, FastMCP uses a handler that logs progress updates at the debug level. This default handler properly handles cases where `total` or `message` might be None. - -You can override the progress handler for specific tool calls: - -```python -# Client uses the default debug logger for progress -client = Client(...) - -async with client: - # Use default progress handler (debug logging) - result1 = await client.call_tool("long_task", {"param": "value"}) - - # Override with custom progress handler just for this call - result2 = await client.call_tool( - "another_task", - {"param": "value"}, - progress_handler=my_progress_handler - ) -``` - -A typical progress update includes: -- Current progress value (e.g., 2 of 5 steps completed) -- Total expected value (may be None) -- Status message (may be None) - -## LLM Sampling - - - -MCP Servers can request LLM completions from clients. The client can provide a `sampling_handler` to handle these requests. The sampling handler receives a list of messages and other parameters from the server, and should return a string completion. - -The following example uses the `marvin` library to generate a completion: - -```python {8-17, 21} -import marvin -from fastmcp import Client -from fastmcp.client.sampling import ( - SamplingMessage, - SamplingParams, - RequestContext, -) - -async def sampling_handler( - messages: list[SamplingMessage], - params: SamplingParams, - context: RequestContext -) -> str: - return await marvin.say_async( - message=[m.content.text for m in messages], - instructions=params.systemPrompt, - ) - -client = Client( - ..., - sampling_handler=sampling_handler, -) -``` - - -## Roots - - - -Roots are a way for clients to inform servers about the resources they have access to or certain boundaries on their access. The server can use this information to adjust behavior or provide more accurate responses. - -Servers can request roots from clients, and clients can notify servers when their roots change. - -To set the roots when creating a client, users can either provide a list of roots (which can be a list of strings) or an async function that returns a list of roots. - - -```python Static Roots {5} -from fastmcp import Client - -client = Client( - ..., - roots=["/path/to/root1", "/path/to/root2"], -) -``` -```python Dynamic Roots Callback {4-6, 10} -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( - ..., - roots=roots_callback, -) -``` - \ No newline at end of file diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx index 2c097724f..a260343c3 100644 --- a/docs/clients/client.mdx +++ b/docs/clients/client.mdx @@ -1,7 +1,7 @@ --- title: Client Overview sidebarTitle: Overview -description: Learn how to use the FastMCP Client to interact with MCP servers. +description: Learn how to use the FastMCP Client to programmatically interact with MCP servers. icon: user-robot --- @@ -9,388 +9,198 @@ import { VersionBadge } from '/snippets/version-badge.mdx' -The `fastmcp.Client` provides a high-level, asynchronous interface for interacting with any Model Context Protocol (MCP) server, whether it's built with FastMCP or another implementation. It simplifies communication by handling protocol details and connection management. +The `fastmcp.Client` is a **programmatic client** for interacting with any Model Context Protocol (MCP) server. It provides a high-level, well-typed, Pythonic interface for deterministic MCP access, making it ideal for: -## FastMCP Client +- **Testing MCP servers** during development +- **Building deterministic applications** that need reliable MCP interactions +- **Creating the foundation for agentic or LLM-based clients** with structured, type-safe operations -The FastMCP Client architecture separates the protocol logic (`Client`) from the connection mechanism (`Transport`). +All client operations require using the `async with` context manager for proper connection lifecycle management. -- **`Client`**: Handles sending MCP requests (like `tools/call`, `resources/read`), receiving responses, and managing callbacks. -- **`Transport`**: Responsible for establishing and maintaining the connection to the server (e.g., via WebSockets, SSE, Stdio, or in-memory). + +This is not an agentic client - it requires explicit function calls and provides direct control over all MCP operations. Use it as a building block for higher-level systems. + -### Transports +## Quick Start -Clients must be initialized with a `transport`. You can either provide an already instantiated transport object, or provide a transport source and let FastMCP attempt to infer the correct transport to use. - -The following inference rules are used to determine the appropriate `ClientTransport` based on the input type: - -1. **`ClientTransport` Instance**: If you provide an already instantiated transport object, it's used directly. -2. **`FastMCP` Instance**: Creates a `FastMCPTransport` for efficient in-memory communication (ideal for testing). This also works with a **FastMCP 1.0 server** created via `mcp.server.fastmcp.FastMCP`. -3. **`Path` or `str` pointing to an existing file**: - * If it ends with `.py`: Creates a `PythonStdioTransport` to run the script using `python`. - * If it ends with `.js`: Creates a `NodeStdioTransport` to run the script using `node`. -4. **`AnyUrl` or `str` pointing to a URL that begins with `http://` or `https://`**: - * Creates a `StreamableHttpTransport` -5. **`MCPConfig` or dictionary matching MCPConfig schema**: Creates a client that connects to one or more MCP servers specified in the config. -6. **Other**: Raises a `ValueError` if the type cannot be inferred. +The client uses transport inference to automatically determine the connection method: ```python import asyncio from fastmcp import Client, FastMCP -# Example transports (more details in Transports page) -server_instance = FastMCP(name="TestServer") # In-memory server -http_url = "https://example.com/mcp" # HTTP server URL -server_script = "my_mcp_server.py" # Path to a Python server file +# In-memory server (ideal for testing) +server = FastMCP("TestServer") +client = Client(server) -# Client automatically infers the transport type -client_in_memory = Client(server_instance) -client_http = Client(http_url) +# HTTP server +client = Client("https://example.com/mcp") -client_stdio = Client(server_script) +# Local Python script +client = Client("my_mcp_server.py") -print(client_in_memory.transport) -print(client_http.transport) -print(client_stdio.transport) +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) -# Expected Output (types may vary slightly based on environment): -# -# -# +asyncio.run(main()) ``` -You can also initialize a client from an MCP configuration dictionary or `MCPConfig` file: +## Client-Transport Architecture + +The FastMCP Client separates concerns between protocol and connection: + +- **`Client`**: Handles MCP protocol operations (tools, resources, prompts) and manages callbacks +- **`Transport`**: Establishes and maintains the connection (WebSockets, HTTP, Stdio, in-memory) + +### Transport Inference + +The client automatically infers the appropriate transport based on the input: + +1. **`FastMCP` instance** → In-memory transport (perfect for testing) +2. **File path ending in `.py`** → Python Stdio transport +3. **File path ending in `.js`** → Node.js Stdio transport +4. **URL starting with `http://` or `https://`** → HTTP transport +5. **`MCPConfig` dictionary** → Multi-server client ```python -from fastmcp import Client +from fastmcp import Client, FastMCP -config = { - "mcpServers": { - "local": {"command": "python", "args": ["local_server.py"]}, - "remote": {"url": "https://example.com/mcp"}, - } -} - -client_config = Client(config) +# Examples of transport inference +client_memory = Client(FastMCP("TestServer")) +client_script = Client("./server.py") +client_http = Client("https://api.example.com/mcp") ``` + -For more control over connection details (like headers for SSE, environment variables for Stdio), you can instantiate the specific `ClientTransport` class yourself and pass it to the `Client`. See the [Transports](/clients/transports) page for details. +For testing and development, always prefer the in-memory transport by passing a `FastMCP` server directly to the client. This eliminates network complexity and separate processes. -### Multi-Server Clients +## Multi-Server Clients -FastMCP supports creating clients that connect to multiple MCP servers through a single client interface using a standard MCP configuration format (`MCPConfig`). This configuration approach makes it easy to connect to multiple specialized servers or create composable systems with a simple, declarative syntax. - - -The MCP configuration format follows an emerging standard and may evolve as the specification matures. FastMCP will strive to maintain compatibility with future versions, but be aware that field names or structure might change. - - -When you create a client with an `MCPConfig` containing multiple servers: - -1. FastMCP creates a composite client that internally mounts all servers using their config names as prefixes -2. Tools and resources from each server are accessible with appropriate prefixes in the format `servername_toolname` and `protocol://servername/resource/path` -3. You interact with this as a single unified client, with requests automatically routed to the appropriate server +Connect to multiple MCP servers through a single client using MCP configuration: ```python -from fastmcp import Client - -# Create a standard MCP configuration with multiple servers config = { "mcpServers": { - # A remote HTTP server - "weather": { - "url": "https://weather-api.example.com/mcp", - "transport": "streamable-http" - }, - # A local server running via stdio - "assistant": { - "command": "python", - "args": ["./my_assistant_server.py"], - "env": {"DEBUG": "true"} - } + "weather": {"url": "https://weather-api.example.com/mcp"}, + "assistant": {"command": "python", "args": ["./assistant_server.py"]} } } -# Create a client that connects to both servers client = Client(config) -async def main(): - async with client: - # Access tools from different servers with prefixes - 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?"}) - - # Access resources with prefixed URIs - weather_icons = await client.read_resource("weather://weather/icons/sunny") - templates = await client.read_resource("resource://assistant/templates/list") - - print(f"Weather: {weather_data}") - print(f"Assistant: {response}") - -if __name__ == "__main__": - asyncio.run(main()) -``` - -If your configuration has only a single server, FastMCP will create a direct client to that server without any prefixing. - -## Client Usage - -### Connection Lifecycle - -The client operates asynchronously and must be used within an `async with` block. This context manager handles establishing the connection, initializing the MCP session, and cleaning up resources upon exit. - -```python -import asyncio -from fastmcp import Client - -client = Client("my_mcp_server.py") # Assumes my_mcp_server.py exists - -async def main(): - # Connection is established here - async with client: - print(f"Client connected: {client.is_connected()}") - - # Make MCP calls within the context - tools = await client.list_tools() - print(f"Available tools: {tools}") - - if any(tool.name == "greet" for tool in tools): - result = await client.call_tool("greet", {"name": "World"}) - print(f"Greet result: {result}") - - # Connection is closed automatically here - print(f"Client connected: {client.is_connected()}") - -if __name__ == "__main__": - asyncio.run(main()) -``` - -You can make multiple calls to the server within the same `async with` block using the established session. - -### Client Methods - -The `Client` provides methods corresponding to standard MCP requests: - - -The standard client methods return user-friendly representations that may change as the protocol evolves. For consistent access to the complete data structure, use the `*_mcp` methods described later. - - -#### Tool Operations - -* **`list_tools()`**: Retrieves a list of tools available on the server. - ```python - tools = await client.list_tools() - # tools -> list[mcp.types.Tool] - ``` -* **`call_tool(name: str, arguments: dict[str, Any] | None = None, timeout: float | None = None, progress_handler: ProgressHandler | None = None)`**: Executes a tool on the server. - ```python - result = await client.call_tool("add", {"a": 5, "b": 3}) - # result -> list[mcp.types.TextContent | mcp.types.ImageContent | ...] - print(result[0].text) # Assuming TextContent, e.g., '8' - - # 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 (to track execution progress) - result = await client.call_tool( - "long_running_task", - {"param": "value"}, - progress_handler=my_progress_handler - ) - ``` - * Arguments are passed as a dictionary. FastMCP servers automatically handle JSON string parsing for complex types if needed. - * Returns a list of content objects (usually `TextContent` or `ImageContent`). - * The optional `timeout` parameter limits the maximum execution time (in seconds) for this specific call, overriding any client-level timeout. - * The optional `progress_handler` parameter receives progress updates during execution, overriding any client-level progress handler. - -#### Resource Operations - -* **`list_resources()`**: Retrieves a list of static resources. - ```python - resources = await client.list_resources() - # resources -> list[mcp.types.Resource] - ``` -* **`list_resource_templates()`**: Retrieves a list of resource templates. - ```python - templates = await client.list_resource_templates() - # templates -> list[mcp.types.ResourceTemplate] - ``` -* **`read_resource(uri: str | AnyUrl)`**: Reads the content of a resource or a resolved template. - ```python - # Read a static resource - readme_content = await client.read_resource("file:///path/to/README.md") - # readme_content -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] - print(readme_content[0].text) # Assuming text - - # Read a resource generated from a template - weather_content = await client.read_resource("data://weather/london") - print(weather_content[0].text) # Assuming text JSON - ``` - -#### Prompt Operations - -* **`list_prompts()`**: Retrieves available prompt templates. -* **`get_prompt(name: str, arguments: dict[str, Any] | None = None)`**: Retrieves a rendered prompt message list. - - - -**Automatic Argument Serialization**: When calling prompts with complex arguments, the FastMCP client automatically serializes non-string values to JSON strings as required by the MCP specification. This allows you to pass typed objects directly while maintaining protocol compliance. - -```python -from dataclasses import dataclass - -@dataclass -class UserData: - name: str - age: int - async with client: - # You can pass complex objects directly - result = await client.get_prompt("analyze_user", { - "user": UserData(name="Alice", age=30), # Automatically serialized to JSON - "preferences": {"theme": "dark"}, # Dict serialized to JSON string - "scores": [85, 92, 78], # List serialized to JSON string - "simple_name": "Bob" # Strings passed through unchanged - }) + # 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") + templates = await client.read_resource("resource://assistant/templates/list") ``` -The client handles the serialization automatically using `pydantic_core.to_json()` for consistent formatting, while the server can deserialize these JSON strings back to the expected types if using FastMCP's server-side type conversion. +## Connection Lifecycle -### Raw MCP Protocol Objects - - - -The FastMCP client attempts to provide a "friendly" interface to the MCP protocol, but sometimes you may need access to the raw MCP protocol objects. Each of the main client methods that returns data has a corresponding `*_mcp` method that returns the raw MCP protocol objects directly. - - -The standard client methods (without `_mcp`) return user-friendly representations of MCP data, while `*_mcp` methods will always return the complete MCP protocol objects. As the protocol evolves, changes to these user-friendly representations may occur and could potentially be breaking. If you need consistent, stable access to the full data structure, prefer using the `*_mcp` methods. - +The client operates asynchronously and uses context managers for connection management: ```python -# Standard method - returns just the list of tools -tools = await client.list_tools() -# tools -> list[mcp.types.Tool] - -# Raw MCP method - returns the full protocol object -result = await client.list_tools_mcp() -# result -> mcp.types.ListToolsResult -tools = result.tools -``` - -Available raw MCP methods: - -* **`list_tools_mcp()`**: Returns `mcp.types.ListToolsResult` -* **`call_tool_mcp(name, arguments)`**: Returns `mcp.types.CallToolResult` -* **`list_resources_mcp()`**: Returns `mcp.types.ListResourcesResult` -* **`list_resource_templates_mcp()`**: Returns `mcp.types.ListResourceTemplatesResult` -* **`read_resource_mcp(uri)`**: Returns `mcp.types.ReadResourceResult` -* **`list_prompts_mcp()`**: Returns `mcp.types.ListPromptsResult` -* **`get_prompt_mcp(name, arguments)`**: Returns `mcp.types.GetPromptResult` -* **`complete_mcp(ref, argument)`**: Returns `mcp.types.CompleteResult` - -These methods are especially useful for debugging or when you need to access metadata or fields that aren't exposed by the simplified methods. - -### Additional Features - -#### Pinging the Server - -The client can be used to ping the server to verify connectivity. - -```python -async with client: - await client.ping() - print("Server is reachable") -``` - -#### Session Management - -When using stdio transports, clients support a `keep_alive` feature (enabled by default) that maintains subprocess sessions between connection contexts. You can manually control this behavior using the client's `close()` method. - -When `keep_alive=False`, the client will automatically close the session when the context manager exits. - -```python -from fastmcp import Client - -client = Client("my_mcp_server.py") # keep_alive=True by default - async def example(): - async with client: - await client.ping() + client = Client("my_mcp_server.py") + # Connection established here async with client: - await client.ping() # Same subprocess as above + print(f"Connected: {client.is_connected()}") + + # Make multiple calls within the same session + tools = await client.list_tools() + result = await client.call_tool("greet", {"name": "World"}) + + # Connection closed automatically here + print(f"Connected: {client.is_connected()}") ``` - -For detailed examples and configuration options, see [Session Management in Transports](/clients/transports#session-management). - +## Core Operations -#### Timeouts +The client provides methods for all standard MCP operations: - +| Operation | Method | Description | +|-----------|--------|-------------| +| **Tools** | `list_tools()`, `call_tool()` | Execute server-side functions | +| **Resources** | `list_resources()`, `read_resource()` | Access server data sources | +| **Prompts** | `list_prompts()`, `get_prompt()` | Retrieve message templates | +| **Utility** | `ping()` | Test server connectivity | -You can control request timeouts at both the client level and individual request level: +### Quick Examples + +```python +async with client: + # Tool operations + tools = await client.list_tools() + result = await client.call_tool("calculate", {"a": 5, "b": 3}) + + # Resource operations + resources = await client.list_resources() + content = await client.read_resource("file:///config/settings.json") + + # Prompt operations + prompts = await client.list_prompts() + messages = await client.get_prompt("welcome", {"name": "Alice"}) +``` + +## Advanced Configuration + +The client supports additional configuration for specialized use cases: ```python from fastmcp import Client -from fastmcp.exceptions import McpError +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}") -# Client with a global 5-second timeout for all requests client = Client( - my_mcp_server, - timeout=5.0 # Default timeout in seconds + "my_mcp_server.py", + log_handler=log_handler, # Handle server logs + progress_handler=progress_handler, # Monitor long operations + timeout=30.0 # Set request timeout ) - -async with client: - # This uses the global 5-second timeout - result1 = await client.call_tool("quick_task", {"param": "value"}) - - # This specifies a 10-second timeout for this specific call - result2 = await client.call_tool("slow_task", {"param": "value"}, timeout=10.0) - - try: - # This will likely timeout - result3 = await client.call_tool("medium_task", {"param": "value"}, timeout=0.01) - except McpError as e: - # Handle timeout error - print(f"The task timed out: {e}") ``` - -Timeout behavior varies between transport types: +## Next Steps -- With **SSE** transport, the per-request (tool call) timeout **always** takes precedence, regardless of which is lower. -- With **HTTP** transport, the **lower** of the two timeouts (client or tool call) takes precedence. +Explore the detailed documentation for each operation type: -For consistent behavior across all transports, we recommend explicitly setting timeouts at the individual tool call level when needed, rather than relying on client-level timeouts. - +### Core Interactions +- **[Tools](/clients/tools)** - Execute server-side functions and handle results +- **[Resources](/clients/resources)** - Access static and templated resources +- **[Prompts](/clients/prompts)** - Work with message templates and argument serialization -#### Error Handling +### Advanced Features +- **[Logging](/clients/logging)** - Handle server log messages +- **[Progress](/clients/progress)** - Monitor long-running operations +- **[Sampling](/clients/sampling)** - Respond to server LLM requests +- **[Roots](/clients/roots)** - Provide local context to servers -When a `call_tool` request results in an error on the server (e.g., the tool function raised an exception), the `client.call_tool()` method will raise a `fastmcp.exceptions.ClientError`. - -```python -async def safe_call_tool(): - async with client: - try: - # Assume 'divide' tool exists and might raise ZeroDivisionError - result = await client.call_tool("divide", {"a": 10, "b": 0}) - print(f"Result: {result}") - except ClientError as e: - print(f"Tool call failed: {e}") - except ConnectionError as e: - print(f"Connection failed: {e}") - except Exception as e: - print(f"An unexpected error occurred: {e}") - -# Example Output if division by zero occurs: -# Tool call failed: Division by zero is not allowed. -``` - -Other errors, like connection failures, will raise standard Python exceptions (e.g., `ConnectionError`, `TimeoutError`). +### Connection Details +- **[Transports](/clients/transports)** - Configure connection methods and parameters +- **[Authentication](/clients/auth/oauth)** - Set up OAuth and bearer token authentication -The client transport often has its own error-handling mechanisms, so you can not always trap errors like those raised by `call_tool` outside of the `async with` block. Instead, you can use `call_tool_mcp()` to get the raw `mcp.types.CallToolResult` object and handle errors yourself by checking its `isError` attribute. - +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. + \ No newline at end of file diff --git a/docs/clients/logging.mdx b/docs/clients/logging.mdx new file mode 100644 index 000000000..ff1a60893 --- /dev/null +++ b/docs/clients/logging.mdx @@ -0,0 +1,63 @@ +--- +title: Server Logging +sidebarTitle: Logging +description: Learn how to receive and handle log messages from MCP servers. +icon: file-text +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +MCP servers can emit log messages to clients. The client can handle these logs through a log handler callback. + +## Setting Up Log Handling + +Provide a `log_handler` function when creating the client: + +```python +from fastmcp import Client +from fastmcp.client.logging import LogMessage + +async def log_handler(message: LogMessage): + level = message.level.upper() + logger = message.logger or 'server' + data = message.data + print(f"[{level}] {logger}: {data}") + +client = Client( + "my_mcp_server.py", + log_handler=log_handler, +) +``` + +## LogMessage Structure + +The `log_handler` receives a `LogMessage` object with: + +- **`level`**: Log level (e.g., "debug", "info", "warning", "error") +- **`logger`**: Logger name (optional, may be None) +- **`data`**: The actual log message content + +```python +async def detailed_log_handler(message: LogMessage): + if message.level == "error": + print(f"ERROR: {message.data}") + elif message.level == "warning": + print(f"WARNING: {message.data}") + else: + print(f"{message.level.upper()}: {message.data}") +``` + +## Default Log Handling + +If you don't provide a custom `log_handler`, FastMCP uses a default handler that emits DEBUG level logs: + +```python +# Without custom handler - uses default DEBUG logging +client = Client("my_mcp_server.py") + +async with client: + # Server logs will be emitted at DEBUG level + await client.call_tool("some_tool") +``` \ No newline at end of file diff --git a/docs/clients/progress.mdx b/docs/clients/progress.mdx new file mode 100644 index 000000000..cb9ee0031 --- /dev/null +++ b/docs/clients/progress.mdx @@ -0,0 +1,59 @@ +--- +title: Progress Monitoring +sidebarTitle: Progress +description: Learn how to handle progress notifications from long-running server operations. +icon: chart-line +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +MCP servers can report progress during long-running operations. The client can receive these updates through a progress handler. + +## Setting Up Progress Handling + +Set a progress handler when creating the client: + +```python +from fastmcp import Client + +async def my_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=my_progress_handler +) +``` + +## Per-Call Progress Handler + +Override the progress handler for specific tool calls: + +```python +async with client: + # Override with specific progress handler for this call + result = await client.call_tool( + "long_running_task", + {"param": "value"}, + progress_handler=my_progress_handler + ) +``` + +## Handler Parameters + +The progress handler receives: + +- **`progress`** (float): Current progress value +- **`total`** (float | None): Expected total value (may be None) +- **`message`** (str | None): Optional status message (may be None) + diff --git a/docs/clients/prompts.mdx b/docs/clients/prompts.mdx new file mode 100644 index 000000000..f4c135953 --- /dev/null +++ b/docs/clients/prompts.mdx @@ -0,0 +1,187 @@ +--- +title: Prompt Operations +sidebarTitle: Prompts +description: Learn how to list and use server-side prompts with automatic argument serialization. +icon: message-square +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +Prompts are reusable message templates exposed by MCP servers. They can accept arguments to generate personalized message sequences for LLM interactions. + +## Listing Prompts + +Use `list_prompts()` to retrieve all available prompt templates: + +```python +async with client: + prompts = await client.list_prompts() + # prompts -> list[mcp.types.Prompt] + + for prompt in prompts: + print(f"Prompt: {prompt.name}") + print(f"Description: {prompt.description}") + if prompt.arguments: + print(f"Arguments: {[arg.name for arg in prompt.arguments]}") +``` + +## Using Prompts + +### Basic Usage + +Request a rendered prompt using `get_prompt()` with the prompt name and arguments: + +```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}") +``` + +### Prompts with Arguments + +Pass arguments as a dictionary to customize the prompt: + +```python +async with client: + # Prompt with simple arguments + result = await client.get_prompt("user_greeting", { + "name": "Alice", + "role": "administrator" + }) + + # Access the personalized messages + for message in result.messages: + print(f"Generated message: {message.content}") +``` + +## Automatic Argument Serialization + + + +FastMCP automatically serializes complex arguments to JSON strings as required by the MCP specification. This allows you to pass typed objects directly: + +```python +from dataclasses import dataclass + +@dataclass +class UserData: + name: str + age: int + +async with client: + # Complex arguments are automatically serialized + result = await client.get_prompt("analyze_user", { + "user": UserData(name="Alice", age=30), # Automatically serialized to JSON + "preferences": {"theme": "dark"}, # Dict serialized to JSON string + "scores": [85, 92, 78], # List serialized to JSON string + "simple_name": "Bob" # Strings passed through unchanged + }) +``` + +The client handles serialization using `pydantic_core.to_json()` for consistent formatting. FastMCP servers can automatically deserialize these JSON strings back to the expected types. + +### Serialization Examples + +```python +async with client: + result = await client.get_prompt("data_analysis", { + # These will be automatically serialized to JSON strings: + "config": { + "format": "csv", + "include_headers": True, + "delimiter": "," + }, + "filters": [ + {"field": "age", "operator": ">", "value": 18}, + {"field": "status", "operator": "==", "value": "active"} + ], + # This remains a string: + "report_title": "Monthly Analytics Report" + }) +``` + +## Working with Prompt Results + +The `get_prompt()` method returns a `GetPromptResult` object containing a list of messages: + +```python +async with client: + result = await client.get_prompt("conversation_starter", {"topic": "climate"}) + + # Access individual messages + 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}") +``` + +## Raw MCP Protocol Access + +For access to the complete MCP protocol objects, use the `*_mcp` methods: + +```python +async with client: + # Raw MCP method returns full protocol object + prompts_result = await client.list_prompts_mcp() + # prompts_result -> mcp.types.ListPromptsResult + + prompt_result = await client.get_prompt_mcp("example_prompt", {"arg": "value"}) + # prompt_result -> mcp.types.GetPromptResult +``` + +## Multi-Server Clients + +When using multi-server clients, prompts are accessible without prefixing (unlike tools): + +```python +async with client: # Multi-server client + # Prompts from any server are directly accessible + result1 = await client.get_prompt("weather_prompt", {"city": "London"}) + result2 = await client.get_prompt("assistant_prompt", {"query": "help"}) +``` + +## Common Prompt Patterns + +### System Messages + +Many prompts generate system messages for LLM configuration: + +```python +async with client: + result = await client.get_prompt("system_configuration", { + "role": "helpful assistant", + "expertise": "python programming" + }) + + # Typically returns messages with role="system" + system_message = result.messages[0] + print(f"System prompt: {system_message.content}") +``` + +### Conversation Templates + +Prompts can generate multi-turn conversation templates: + +```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}") +``` + + +Prompt arguments and their expected types depend on the specific prompt implementation. Check the server's documentation or use `list_prompts()` to see available arguments for each prompt. + \ No newline at end of file diff --git a/docs/clients/resources.mdx b/docs/clients/resources.mdx new file mode 100644 index 000000000..705770879 --- /dev/null +++ b/docs/clients/resources.mdx @@ -0,0 +1,171 @@ +--- +title: Resource Operations +sidebarTitle: Resources +description: Learn how to list and read static and templated resources from MCP servers. +icon: folder-open +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +Resources are data sources exposed by MCP servers. They can be static files or dynamic templates that generate content based on parameters. + +## Types of Resources + +MCP servers expose two types of resources: + +- **Static Resources**: Fixed content accessible via URI (e.g., configuration files, documentation) +- **Resource Templates**: Dynamic resources that accept parameters to generate content (e.g., API endpoints, database queries) + +## Listing Resources + +### Static Resources + +Use `list_resources()` to retrieve all static resources available on the server: + +```python +async with client: + resources = await client.list_resources() + # resources -> list[mcp.types.Resource] + + for resource in resources: + print(f"Resource URI: {resource.uri}") + print(f"Name: {resource.name}") + print(f"Description: {resource.description}") + print(f"MIME Type: {resource.mimeType}") +``` + +### Resource Templates + +Use `list_resource_templates()` to retrieve available resource templates: + +```python +async with client: + templates = await client.list_resource_templates() + # templates -> list[mcp.types.ResourceTemplate] + + for template in templates: + print(f"Template URI: {template.uriTemplate}") + print(f"Name: {template.name}") + print(f"Description: {template.description}") +``` + +## Reading Resources + +### Static Resources + +Read a static resource using its URI: + +```python +async with client: + # Read a static resource + content = await client.read_resource("file:///path/to/README.md") + # content -> list[mcp.types.TextResourceContents | mcp.types.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 + +Read from a resource template by providing the URI with parameters: + +```python +async with client: + # Read a resource generated from a template + # For example, a template like "weather://{{city}}/current" + weather_content = await client.read_resource("weather://london/current") + + # Access the generated content + print(weather_content[0].text) # Assuming text JSON response +``` + +## Content Types + +Resources can return different content types: + +### Text Resources + +```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 + +```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) +``` + +## Working with Multi-Server Clients + +When using multi-server clients, resource URIs are automatically prefixed with the server name: + +```python +async with client: # Multi-server client + # Access resources from different servers + weather_icons = await client.read_resource("weather://weather/icons/sunny") + templates = await client.read_resource("resource://assistant/templates/list") + + print(f"Weather icon: {weather_icons[0].blob}") + print(f"Templates: {templates[0].text}") +``` + +## Raw MCP Protocol Access + +For access to the complete MCP protocol objects, use the `*_mcp` methods: + +```python +async with client: + # Raw MCP methods return full protocol objects + resources_result = await client.list_resources_mcp() + # resources_result -> mcp.types.ListResourcesResult + + templates_result = await client.list_resource_templates_mcp() + # templates_result -> mcp.types.ListResourceTemplatesResult + + content_result = await client.read_resource_mcp("resource://example") + # content_result -> mcp.types.ReadResourceResult +``` + +## Common Resource URI Patterns + +Different MCP servers may use various URI schemes: + +```python +# File system resources +"file:///path/to/file.txt" + +# Custom protocol resources +"weather://london/current" +"database://users/123" + +# Generic resource protocol +"resource://config/settings" +"resource://templates/email" +``` + + +Resource URIs and their formats depend on the specific MCP server implementation. Check the server's documentation for available resources and their URI patterns. + \ No newline at end of file diff --git a/docs/clients/roots.mdx b/docs/clients/roots.mdx new file mode 100644 index 000000000..2db66a37b --- /dev/null +++ b/docs/clients/roots.mdx @@ -0,0 +1,42 @@ +--- +title: Client Roots +sidebarTitle: Roots +description: Learn how to provide local context to MCP servers. +icon: tree +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +Roots are a way for clients to inform servers about the resources they have access to. Servers can use this information to adjust behavior or provide more relevant responses. + +## Setting Static Roots + +Provide a list of roots when creating the client: + + +```python Static Roots +from fastmcp import Client + +client = Client( + "my_mcp_server.py", + roots=["/path/to/root1", "/path/to/root2"] +) +``` + +```python Dynamic Roots Callback +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 +) +``` + + diff --git a/docs/clients/sampling.mdx b/docs/clients/sampling.mdx new file mode 100644 index 000000000..887541698 --- /dev/null +++ b/docs/clients/sampling.mdx @@ -0,0 +1,94 @@ +--- +title: LLM Sampling +sidebarTitle: Sampling +description: Learn how to handle server-initiated LLM sampling requests. +icon: brain +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +MCP servers can request LLM completions from clients. The client handles these requests through a sampling handler callback. + +## Setting Up Sampling Handling + +Provide a `sampling_handler` function when creating the client: + +```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: + # Your LLM integration logic here + # Extract text from messages and generate a response + return "Generated response based on the messages" + +client = Client( + "my_mcp_server.py", + sampling_handler=sampling_handler, +) +``` + +## Handler Parameters + +The sampling handler receives: + +- **`messages`**: List of `SamplingMessage` objects representing the conversation +- **`params`**: `SamplingParams` object with generation parameters (systemPrompt, maxTokens, temperature, etc.) +- **`context`**: `RequestContext` object with request metadata + +## Basic Example + +```python +async def basic_sampling_handler( + messages: list[SamplingMessage], + params: SamplingParams, + context: RequestContext +) -> str: + # 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." + + # Here you would integrate with your preferred LLM service + # This is just a placeholder response + return f"Response based on conversation: {' | '.join(conversation)}" + +client = Client( + "my_mcp_server.py", + sampling_handler=basic_sampling_handler +) +``` + +## Accessing Parameters + +```python +async def parameter_handler( + messages: list[SamplingMessage], + params: SamplingParams, + context: RequestContext +) -> str: + # Available parameters from the server + system_prompt = params.systemPrompt + max_tokens = params.maxTokens + temperature = params.temperature + top_p = params.topP + stop_sequences = params.stopSequences + + # Use these parameters with your LLM service + return "Generated response" +``` + diff --git a/docs/clients/tools.mdx b/docs/clients/tools.mdx new file mode 100644 index 000000000..95ec06165 --- /dev/null +++ b/docs/clients/tools.mdx @@ -0,0 +1,143 @@ +--- +title: Tool Operations +sidebarTitle: Tools +description: Learn how to discover and execute tools on MCP servers. +icon: wrench +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +Tools are executable functions exposed by MCP servers. The FastMCP client provides methods to discover available tools and execute them with arguments. + +## Discovering Tools + +Use `list_tools()` to retrieve all tools available on the server: + +```python +async with client: + tools = await client.list_tools() + # tools -> list[mcp.types.Tool] + + for tool in tools: + print(f"Tool: {tool.name}") + print(f"Description: {tool.description}") + if tool.inputSchema: + print(f"Parameters: {tool.inputSchema}") +``` + +## Executing Tools + +### Basic Execution + +Execute a tool using `call_tool()` with the tool name and arguments: + +```python +async with client: + # Simple tool call + result = await client.call_tool("add", {"a": 5, "b": 3}) + # result -> list[mcp.types.TextContent | mcp.types.ImageContent | ...] + + # Access the result content + print(result[0].text) # Assuming TextContent, e.g., '8' +``` + +### Advanced Execution Options + +The `call_tool()` method supports additional parameters for 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 (to track execution progress) + result = await client.call_tool( + "long_running_task", + {"param": "value"}, + progress_handler=my_progress_handler + ) +``` + +**Parameters:** +- `name`: The tool name (string) +- `arguments`: Dictionary of arguments to pass to the tool (optional) +- `timeout`: Maximum execution time in seconds (optional, overrides client-level timeout) +- `progress_handler`: Progress callback function (optional, overrides client-level handler) + +## Handling Results + +Tool execution returns a list of content objects. The most common types are: + +- **`TextContent`**: Text-based results with a `.text` attribute +- **`ImageContent`**: Image data with image-specific attributes +- **`BlobContent`**: Binary data content + +```python +async with client: + result = await client.call_tool("get_weather", {"city": "London"}) + + for content in result: + if hasattr(content, 'text'): + print(f"Text result: {content.text}") + elif hasattr(content, 'data'): + print(f"Binary data: {len(content.data)} bytes") +``` + +## Error Handling + +### Exception-Based 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) + except ToolError as e: + print(f"Tool failed: {e}") +``` + +### Manual Error Checking + +For more granular control, use `call_tool_mcp()` which returns the raw MCP protocol object with an `isError` flag: + +```python +async with client: + result = await client.call_tool_mcp("potentially_failing_tool", {"param": "value"}) + # result -> mcp.types.CallToolResult + + if result.isError: + print(f"Tool failed: {result.content}") + else: + print(f"Tool succeeded: {result.content}") +``` + +## Argument Handling + +Arguments are passed as a dictionary to the tool: + +```python +async with client: + # Simple arguments + result = await client.call_tool("greet", {"name": "World"}) + + # Complex arguments + result = await client.call_tool("process_data", { + "config": {"format": "json", "validate": True}, + "items": [1, 2, 3, 4, 5], + "metadata": {"source": "api", "version": "1.0"} + }) +``` + + +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). + \ No newline at end of file diff --git a/docs/docs.json b/docs/docs.json index 3f17a0342..90f983e72 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -94,13 +94,31 @@ "group": "Clients", "pages": [ "clients/client", + { + "group": "Core Interactions", + "icon": "handshake", + "pages": [ + "clients/tools", + "clients/resources", + "clients/prompts" + ] + }, + { + "group": "Advanced Features", + "icon": "stars", + "pages": [ + "clients/logging", + "clients/progress", + "clients/sampling", + "clients/roots" + ] + }, "clients/transports", { "group": "Authentication", "icon": "user-shield", "pages": ["clients/auth/oauth", "clients/auth/bearer"] - }, - "clients/advanced-features" + } ] }, { diff --git a/justfile b/justfile index a18451447..fc2f33501 100644 --- a/justfile +++ b/justfile @@ -24,4 +24,7 @@ api-ref *MODULES: # Clean up API reference documentation api-ref-clean: - rm -rf docs/python-sdk \ No newline at end of file + rm -rf docs/python-sdk + +copy-context: + uvx --with-editable . --refresh-package copychat copychat@latest src/ docs/ -x changelog.mdx -x python-sdk/ -v \ No newline at end of file From 1adca653e38af8af66928d70ec68c50978db2e76 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 13:23:52 -0400 Subject: [PATCH 38/41] Update overview Co-Authored-By: Claude --- docs/clients/client.mdx | 72 ++++++++++++++++++-------------------- docs/clients/prompts.mdx | 4 +-- docs/servers/resources.mdx | 2 +- 3 files changed, 37 insertions(+), 41 deletions(-) diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx index a260343c3..e69d45e42 100644 --- a/docs/clients/client.mdx +++ b/docs/clients/client.mdx @@ -15,7 +15,6 @@ The `fastmcp.Client` is a **programmatic client** for interacting with any Model - **Building deterministic applications** that need reliable MCP interactions - **Creating the foundation for agentic or LLM-based clients** with structured, type-safe operations -All client operations require using the `async with` context manager for proper connection lifecycle management. This is not an agentic client - it requires explicit function calls and provides direct control over all MCP operations. Use it as a building block for higher-level systems. @@ -23,7 +22,7 @@ This is not an agentic client - it requires explicit function calls and provides ## Quick Start -The client uses transport inference to automatically determine the connection method: +Note that all client operations require using the `async with` context manager for proper connection lifecycle management. The client uses transport inference to automatically determine the connection method. ```python import asyncio @@ -86,11 +85,37 @@ client_http = Client("https://api.example.com/mcp") For testing and development, always prefer the in-memory transport by passing a `FastMCP` server directly to the client. This eliminates network complexity and separate processes. -## Multi-Server Clients +## Configuration-Based Clients -Connect to multiple MCP servers through a single client using MCP configuration: +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. + +### Configuration Format + +```python +config = { + "mcpServers": { + "server_name": { + # Remote HTTP/SSE server + "transport": "streamable-http", # or "sse" + "url": "https://api.example.com/mcp", + "headers": {"Authorization": "Bearer token"}, + "auth": "oauth" # or bearer token string + }, + "local_server": { + # Local stdio server + "transport": "stdio" + "command": "python", + "args": ["./server.py", "--verbose"], + "env": {"DEBUG": "true"}, + "cwd": "/path/to/server", + } + } +} +``` + +### Multi-Server Example ```python config = { @@ -143,43 +168,14 @@ The client provides methods for all standard MCP operations: | **Prompts** | `list_prompts()`, `get_prompt()` | Retrieve message templates | | **Utility** | `ping()` | Test server connectivity | -### Quick Examples +### Server Connectivity + +Use `ping()` to verify the server is reachable: ```python async with client: - # Tool operations - tools = await client.list_tools() - result = await client.call_tool("calculate", {"a": 5, "b": 3}) - - # Resource operations - resources = await client.list_resources() - content = await client.read_resource("file:///config/settings.json") - - # Prompt operations - prompts = await client.list_prompts() - messages = await client.get_prompt("welcome", {"name": "Alice"}) -``` - -## Advanced Configuration - -The client supports additional configuration for specialized use cases: - -```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}") - -client = Client( - "my_mcp_server.py", - log_handler=log_handler, # Handle server logs - progress_handler=progress_handler, # Monitor long operations - timeout=30.0 # Set request timeout -) + await client.ping() + print("Server is reachable") ``` ## Next Steps diff --git a/docs/clients/prompts.mdx b/docs/clients/prompts.mdx index f4c135953..7ccfbd501 100644 --- a/docs/clients/prompts.mdx +++ b/docs/clients/prompts.mdx @@ -1,8 +1,8 @@ --- -title: Prompt Operations +title: Prompts sidebarTitle: Prompts description: Learn how to list and use server-side prompts with automatic argument serialization. -icon: message-square +icon: message-lines --- import { VersionBadge } from '/snippets/version-badge.mdx' diff --git a/docs/servers/resources.mdx b/docs/servers/resources.mdx index 3a7914505..f38834980 100644 --- a/docs/servers/resources.mdx +++ b/docs/servers/resources.mdx @@ -2,7 +2,7 @@ title: Resources & Templates sidebarTitle: Resources description: Expose data sources and dynamic content generators to your MCP client. -icon: database +icon: folder-open --- import { VersionBadge } from "/snippets/version-badge.mdx" From 3d2e2a5954df5072149e40ce7330dfadef7d7737 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 13:30:25 -0400 Subject: [PATCH 39/41] Update client features --- docs/clients/client.mdx | 2 +- docs/clients/logging.mdx | 2 +- docs/clients/progress.mdx | 2 +- docs/clients/roots.mdx | 2 +- docs/clients/sampling.mdx | 45 ++++++++++++++++++--------------------- docs/docs.json | 2 +- 6 files changed, 26 insertions(+), 29 deletions(-) diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx index e69d45e42..d246c88fe 100644 --- a/docs/clients/client.mdx +++ b/docs/clients/client.mdx @@ -182,7 +182,7 @@ async with client: Explore the detailed documentation for each operation type: -### Core Interactions +### Core Operations - **[Tools](/clients/tools)** - Execute server-side functions and handle results - **[Resources](/clients/resources)** - Access static and templated resources - **[Prompts](/clients/prompts)** - Work with message templates and argument serialization diff --git a/docs/clients/logging.mdx b/docs/clients/logging.mdx index ff1a60893..a9ea86a48 100644 --- a/docs/clients/logging.mdx +++ b/docs/clients/logging.mdx @@ -2,7 +2,7 @@ title: Server Logging sidebarTitle: Logging description: Learn how to receive and handle log messages from MCP servers. -icon: file-text +icon: receipt --- import { VersionBadge } from '/snippets/version-badge.mdx' diff --git a/docs/clients/progress.mdx b/docs/clients/progress.mdx index cb9ee0031..f8dd9db1c 100644 --- a/docs/clients/progress.mdx +++ b/docs/clients/progress.mdx @@ -2,7 +2,7 @@ title: Progress Monitoring sidebarTitle: Progress description: Learn how to handle progress notifications from long-running server operations. -icon: chart-line +icon: bars-progress --- import { VersionBadge } from '/snippets/version-badge.mdx' diff --git a/docs/clients/roots.mdx b/docs/clients/roots.mdx index 2db66a37b..48c4aa258 100644 --- a/docs/clients/roots.mdx +++ b/docs/clients/roots.mdx @@ -2,7 +2,7 @@ title: Client Roots sidebarTitle: Roots description: Learn how to provide local context to MCP servers. -icon: tree +icon: folder-tree --- import { VersionBadge } from '/snippets/version-badge.mdx' diff --git a/docs/clients/sampling.mdx b/docs/clients/sampling.mdx index 887541698..0003999f4 100644 --- a/docs/clients/sampling.mdx +++ b/docs/clients/sampling.mdx @@ -2,7 +2,7 @@ title: LLM Sampling sidebarTitle: Sampling description: Learn how to handle server-initiated LLM sampling requests. -icon: brain +icon: robot --- import { VersionBadge } from '/snippets/version-badge.mdx' @@ -40,15 +40,31 @@ client = Client( ## Handler Parameters -The sampling handler receives: +The sampling handler receives three parameters: -- **`messages`**: List of `SamplingMessage` objects representing the conversation -- **`params`**: `SamplingParams` object with generation parameters (systemPrompt, maxTokens, temperature, etc.) -- **`context`**: `RequestContext` object with request metadata +### SamplingMessage + +- **`role`**: Message role (e.g., "user", "assistant", "system") +- **`content`**: Message content (usually has `.text` attribute) + +### SamplingParams + +- **`systemPrompt`**: System prompt string (optional) +- **`maxTokens`**: Maximum tokens to generate (optional) +- **`temperature`**: Sampling temperature (optional) +- **`topP`**: Top-p sampling parameter (optional) +- **`stopSequences`**: List of stop sequences (optional) + +### RequestContext + +- **`request_id`**: Unique identifier for the sampling request ## Basic Example ```python +from fastmcp import Client +from fastmcp.client.sampling import SamplingMessage, SamplingParams, RequestContext + async def basic_sampling_handler( messages: list[SamplingMessage], params: SamplingParams, @@ -73,22 +89,3 @@ client = Client( ) ``` -## Accessing Parameters - -```python -async def parameter_handler( - messages: list[SamplingMessage], - params: SamplingParams, - context: RequestContext -) -> str: - # Available parameters from the server - system_prompt = params.systemPrompt - max_tokens = params.maxTokens - temperature = params.temperature - top_p = params.topP - stop_sequences = params.stopSequences - - # Use these parameters with your LLM service - return "Generated response" -``` - diff --git a/docs/docs.json b/docs/docs.json index 90f983e72..d35476ca6 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -95,7 +95,7 @@ "pages": [ "clients/client", { - "group": "Core Interactions", + "group": "Core Operations", "icon": "handshake", "pages": [ "clients/tools", From 133254ee252d8279cbc6db0b9d271fe629fb86c8 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 13:34:36 -0400 Subject: [PATCH 40/41] Update docs --- docs/clients/client.mdx | 117 ++++++++++++++++++++--- docs/clients/logging.mdx | 2 +- docs/clients/progress.mdx | 2 +- docs/clients/prompts.mdx | 2 +- docs/clients/resources.mdx | 2 +- docs/clients/roots.mdx | 2 +- docs/clients/sampling.mdx | 2 +- docs/clients/tools.mdx | 2 +- docs/docs.json | 2 +- docs/servers/{fastmcp.mdx => server.mdx} | 4 +- 10 files changed, 113 insertions(+), 24 deletions(-) rename docs/servers/{fastmcp.mdx => server.mdx} (98%) diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx index d246c88fe..c56ce634e 100644 --- a/docs/clients/client.mdx +++ b/docs/clients/client.mdx @@ -1,7 +1,7 @@ --- -title: Client Overview +title: The FastMCP Client sidebarTitle: Overview -description: Learn how to use the FastMCP Client to programmatically interact with MCP servers. +description: Programmatic client for interacting with MCP servers through a well-typed, Pythonic interface. icon: user-robot --- @@ -9,20 +9,24 @@ import { VersionBadge } from '/snippets/version-badge.mdx' -The `fastmcp.Client` is a **programmatic client** for interacting with any Model Context Protocol (MCP) server. It provides a high-level, well-typed, Pythonic interface for deterministic MCP access, making it ideal for: +The central piece of MCP client applications is the `fastmcp.Client` class. This class provides a **programmatic interface** for interacting with any Model Context Protocol (MCP) server, handling protocol details and connection management automatically. + +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 +- **Building deterministic applications** that need reliable MCP interactions - **Creating the foundation for agentic or LLM-based clients** with structured, type-safe operations +All client operations require using the `async with` context manager for proper connection lifecycle management. + This is not an agentic client - it requires explicit function calls and provides direct control over all MCP operations. Use it as a building block for higher-level systems. -## Quick Start +## Creating a Client -Note that all client operations require using the `async with` context manager for proper connection lifecycle management. The client uses transport inference to automatically determine the connection method. +Creating a client is straightforward. You provide a server source and the client automatically infers the appropriate transport mechanism. ```python import asyncio @@ -157,16 +161,57 @@ async def example(): print(f"Connected: {client.is_connected()}") ``` -## Core Operations +## Operations -The client provides methods for all standard MCP operations: +FastMCP clients can interact with several types of server components: -| Operation | Method | Description | -|-----------|--------|-------------| -| **Tools** | `list_tools()`, `call_tool()` | Execute server-side functions | -| **Resources** | `list_resources()`, `read_resource()` | Access server data sources | -| **Prompts** | `list_prompts()`, `get_prompt()` | Retrieve message templates | -| **Utility** | `ping()` | Test server connectivity | +### Tools + +Tools are server-side functions that the client can execute with arguments. + +```python +async with client: + # List available tools + tools = await client.list_tools() + + # Execute a tool + result = await client.call_tool("multiply", {"a": 5, "b": 3}) + print(result[0].text) # "15" +``` + +See [Tools](/clients/tools) for detailed documentation. + +### Resources + +Resources are data sources that the client can read, either static or templated. + +```python +async with client: + # List available resources + resources = await client.list_resources() + + # Read a resource + content = await client.read_resource("file:///config/settings.json") + print(content[0].text) +``` + +See [Resources](/clients/resources) for detailed documentation. + +### Prompts + +Prompts are reusable message templates that can accept arguments. + +```python +async with client: + # List available prompts + prompts = await client.list_prompts() + + # Get a rendered prompt + messages = await client.get_prompt("analyze_data", {"data": [1, 2, 3]}) + print(messages.messages) +``` + +See [Prompts](/clients/prompts) for detailed documentation. ### Server Connectivity @@ -178,6 +223,50 @@ async with client: print("Server is reachable") ``` +## Client Configuration + +Clients can be configured with additional handlers and settings for specialized use cases. + +### Callback Handlers + +The client supports several callback handlers for advanced server interactions: + +```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 +) +``` + +The `Client` constructor accepts several configuration options: + +- `transport`: Transport instance or source for automatic inference +- `log_handler`: Handle server log messages +- `progress_handler`: Monitor long-running operations +- `sampling_handler`: Respond to server LLM requests +- `roots`: Provide local context to servers +- `timeout`: Default timeout for requests (in seconds) + +### Transport Configuration + +For detailed transport configuration (headers, authentication, environment variables), see the [Transports](/clients/transports) documentation. + ## Next Steps Explore the detailed documentation for each operation type: diff --git a/docs/clients/logging.mdx b/docs/clients/logging.mdx index a9ea86a48..9c28a5d25 100644 --- a/docs/clients/logging.mdx +++ b/docs/clients/logging.mdx @@ -1,7 +1,7 @@ --- title: Server Logging sidebarTitle: Logging -description: Learn how to receive and handle log messages from MCP servers. +description: Receive and handle log messages from MCP servers. icon: receipt --- diff --git a/docs/clients/progress.mdx b/docs/clients/progress.mdx index f8dd9db1c..bd500fa26 100644 --- a/docs/clients/progress.mdx +++ b/docs/clients/progress.mdx @@ -1,7 +1,7 @@ --- title: Progress Monitoring sidebarTitle: Progress -description: Learn how to handle progress notifications from long-running server operations. +description: Handle progress notifications from long-running server operations. icon: bars-progress --- diff --git a/docs/clients/prompts.mdx b/docs/clients/prompts.mdx index 7ccfbd501..0ba4d2765 100644 --- a/docs/clients/prompts.mdx +++ b/docs/clients/prompts.mdx @@ -1,7 +1,7 @@ --- title: Prompts sidebarTitle: Prompts -description: Learn how to list and use server-side prompts with automatic argument serialization. +description: Use server-side prompt templates with automatic argument serialization. icon: message-lines --- diff --git a/docs/clients/resources.mdx b/docs/clients/resources.mdx index 705770879..ecad582e0 100644 --- a/docs/clients/resources.mdx +++ b/docs/clients/resources.mdx @@ -1,7 +1,7 @@ --- title: Resource Operations sidebarTitle: Resources -description: Learn how to list and read static and templated resources from MCP servers. +description: Access static and templated resources from MCP servers. icon: folder-open --- diff --git a/docs/clients/roots.mdx b/docs/clients/roots.mdx index 48c4aa258..2a8d8c1f9 100644 --- a/docs/clients/roots.mdx +++ b/docs/clients/roots.mdx @@ -1,7 +1,7 @@ --- title: Client Roots sidebarTitle: Roots -description: Learn how to provide local context to MCP servers. +description: Provide local context and resource boundaries to MCP servers. icon: folder-tree --- diff --git a/docs/clients/sampling.mdx b/docs/clients/sampling.mdx index 0003999f4..25d035478 100644 --- a/docs/clients/sampling.mdx +++ b/docs/clients/sampling.mdx @@ -1,7 +1,7 @@ --- title: LLM Sampling sidebarTitle: Sampling -description: Learn how to handle server-initiated LLM sampling requests. +description: Handle server-initiated LLM sampling requests. icon: robot --- diff --git a/docs/clients/tools.mdx b/docs/clients/tools.mdx index 95ec06165..3821725cb 100644 --- a/docs/clients/tools.mdx +++ b/docs/clients/tools.mdx @@ -1,7 +1,7 @@ --- title: Tool Operations sidebarTitle: Tools -description: Learn how to discover and execute tools on MCP servers. +description: Discover and execute server-side tools with the FastMCP client. icon: wrench --- diff --git a/docs/docs.json b/docs/docs.json index d35476ca6..399c1f146 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -63,7 +63,7 @@ { "group": "Servers", "pages": [ - "servers/fastmcp", + "servers/server", { "group": "Core Components", "icon": "toolbox", diff --git a/docs/servers/fastmcp.mdx b/docs/servers/server.mdx similarity index 98% rename from docs/servers/fastmcp.mdx rename to docs/servers/server.mdx index 12aa08fd8..1cb5f089b 100644 --- a/docs/servers/fastmcp.mdx +++ b/docs/servers/server.mdx @@ -1,7 +1,7 @@ --- title: The FastMCP Server -sidebarTitle: FastMCP Servers -description: Learn about the core FastMCP server class and how to run it. +sidebarTitle: Overview +description: The core FastMCP server class for building MCP applications with tools, resources, and prompts. icon: server --- From 4d571e3711183af82f5b115624eb884922906b04 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 13:43:15 -0400 Subject: [PATCH 41/41] Update nav --- docs/docs.json | 409 +++++++++++++++++++++++++------------------------ 1 file changed, 210 insertions(+), 199 deletions(-) diff --git a/docs/docs.json b/docs/docs.json index 399c1f146..66971c813 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -44,224 +44,235 @@ } }, "navigation": { - "anchors": [ + "tabs": [ { - "anchor": "Documentation", - "groups": [ + "tab": "Documentation", + "anchors": [ { - "group": "Get Started", - "pages": [ - "getting-started/welcome", - "getting-started/installation", - "getting-started/quickstart" - ] - }, - { - "group": "What's New", - "pages": ["updates", "changelog"] - }, - { - "group": "Servers", - "pages": [ - "servers/server", + "anchor": "Documentation", + "groups": [ { - "group": "Core Components", - "icon": "toolbox", + "group": "Get Started", "pages": [ - "servers/tools", - "servers/resources", - "servers/prompts", - "servers/context" + "getting-started/welcome", + "getting-started/installation", + "getting-started/quickstart" ] }, { - "group": "Authentication", - "icon": "shield-check", - "pages": ["servers/auth/bearer"] - }, - "servers/middleware", - "servers/openapi", - "servers/proxy", - "servers/composition", - { - "group": "Deployment", - "icon": "upload", - "pages": ["deployment/running-server", "deployment/asgi"] - } - ] - }, - { - "group": "Clients", - "pages": [ - "clients/client", - { - "group": "Core Operations", - "icon": "handshake", + "group": "Servers", "pages": [ - "clients/tools", - "clients/resources", - "clients/prompts" - ] - }, - { - "group": "Advanced Features", - "icon": "stars", - "pages": [ - "clients/logging", - "clients/progress", - "clients/sampling", - "clients/roots" - ] - }, - "clients/transports", - { - "group": "Authentication", - "icon": "user-shield", - "pages": ["clients/auth/oauth", "clients/auth/bearer"] - } - ] - }, - { - "group": "Integrations", - "pages": [ - "integrations/anthropic", - "integrations/claude-desktop", - "integrations/openai", - "integrations/gemini", - "integrations/contrib" - ] - }, - { - "group": "Patterns", - "pages": [ - "patterns/tool-transformation", - "patterns/decorating-methods", - "patterns/http-requests", - "patterns/testing", - "patterns/cli" - ] - }, - { - "group": "Tutorials", - "pages": [ - "tutorials/mcp", - "tutorials/create-mcp-server", - "tutorials/rest-api" - ] - } - ], - "icon": "book" - }, - { - "anchor": "Community", - "icon": "users", - "pages": ["community/showcase"] - }, - { - "anchor": "SDK Reference", - "icon": "code", - "pages": [ - "python-sdk/fastmcp-exceptions", - "python-sdk/fastmcp-settings", - { - "group": "fastmcp.cli", - "pages": [ - "python-sdk/fastmcp-cli-__init__", - "python-sdk/fastmcp-cli-claude", - "python-sdk/fastmcp-cli-cli", - "python-sdk/fastmcp-cli-run" - ] - }, - { - "group": "fastmcp.client", - "pages": [ - "python-sdk/fastmcp-client-__init__", - { - "group": "auth", - "pages": [ - "python-sdk/fastmcp-client-auth-__init__", - "python-sdk/fastmcp-client-auth-bearer", - "python-sdk/fastmcp-client-auth-oauth" - ] - }, - "python-sdk/fastmcp-client-client", - "python-sdk/fastmcp-client-logging", - "python-sdk/fastmcp-client-oauth_callback", - "python-sdk/fastmcp-client-progress", - "python-sdk/fastmcp-client-roots", - "python-sdk/fastmcp-client-sampling", - "python-sdk/fastmcp-client-transports" - ] - }, - { - "group": "fastmcp.prompts", - "pages": [ - "python-sdk/fastmcp-prompts-__init__", - "python-sdk/fastmcp-prompts-prompt", - "python-sdk/fastmcp-prompts-prompt_manager" - ] - }, - { - "group": "fastmcp.resources", - "pages": [ - "python-sdk/fastmcp-resources-__init__", - "python-sdk/fastmcp-resources-resource", - "python-sdk/fastmcp-resources-resource_manager", - "python-sdk/fastmcp-resources-template", - "python-sdk/fastmcp-resources-types" - ] - }, - { - "group": "fastmcp.server", - "pages": [ - "python-sdk/fastmcp-server-__init__", - { - "group": "auth", - "pages": [ - "python-sdk/fastmcp-server-auth-__init__", - "python-sdk/fastmcp-server-auth-auth", + "servers/server", { - "group": "providers", + "group": "Core Components", + "icon": "toolbox", "pages": [ - "python-sdk/fastmcp-server-auth-providers-__init__", - "python-sdk/fastmcp-server-auth-providers-bearer", - "python-sdk/fastmcp-server-auth-providers-bearer_env", - "python-sdk/fastmcp-server-auth-providers-in_memory" + "servers/tools", + "servers/resources", + "servers/prompts", + "servers/context" ] + }, + { + "group": "Authentication", + "icon": "shield-check", + "pages": ["servers/auth/bearer"] + }, + "servers/middleware", + "servers/openapi", + "servers/proxy", + "servers/composition", + { + "group": "Deployment", + "icon": "upload", + "pages": ["deployment/running-server", "deployment/asgi"] } ] }, - "python-sdk/fastmcp-server-context", - "python-sdk/fastmcp-server-dependencies", - "python-sdk/fastmcp-server-http", - "python-sdk/fastmcp-server-middleware", - "python-sdk/fastmcp-server-openapi", - "python-sdk/fastmcp-server-proxy", - "python-sdk/fastmcp-server-server" - ] + { + "group": "Clients", + "pages": [ + "clients/client", + { + "group": "Core Operations", + "icon": "handshake", + "pages": [ + "clients/tools", + "clients/resources", + "clients/prompts" + ] + }, + { + "group": "Advanced Features", + "icon": "stars", + "pages": [ + "clients/logging", + "clients/progress", + "clients/sampling", + "clients/roots" + ] + }, + "clients/transports", + { + "group": "Authentication", + "icon": "user-shield", + "pages": ["clients/auth/oauth", "clients/auth/bearer"] + } + ] + }, + { + "group": "Integrations", + "pages": [ + "integrations/anthropic", + "integrations/claude-desktop", + "integrations/openai", + "integrations/gemini", + "integrations/contrib" + ] + }, + { + "group": "Patterns", + "pages": [ + "patterns/tool-transformation", + "patterns/decorating-methods", + "patterns/http-requests", + "patterns/testing", + "patterns/cli" + ] + }, + { + "group": "Tutorials", + "pages": [ + "tutorials/mcp", + "tutorials/create-mcp-server", + "tutorials/rest-api" + ] + } + ], + "icon": "book" }, { - "group": "fastmcp.tools", - "pages": [ - "python-sdk/fastmcp-tools-__init__", - "python-sdk/fastmcp-tools-tool", - "python-sdk/fastmcp-tools-tool_manager", - "python-sdk/fastmcp-tools-tool_transform" - ] + "anchor": "What's New", + "pages": ["updates", "changelog"] }, + { - "group": "fastmcp.utilities", + "anchor": "Community", + "icon": "users", + "pages": ["community/showcase"] + } + ] + }, + { + "tab": "SDK Reference", + "anchors": [ + { + "anchor": "Python SDK", + "icon": "python", "pages": [ - "python-sdk/fastmcp-utilities-__init__", - "python-sdk/fastmcp-utilities-cache", - "python-sdk/fastmcp-utilities-components", - "python-sdk/fastmcp-utilities-exceptions", - "python-sdk/fastmcp-utilities-http", - "python-sdk/fastmcp-utilities-json_schema", - "python-sdk/fastmcp-utilities-logging", - "python-sdk/fastmcp-utilities-mcp_config", - "python-sdk/fastmcp-utilities-openapi", - "python-sdk/fastmcp-utilities-types" + "python-sdk/fastmcp-exceptions", + "python-sdk/fastmcp-settings", + { + "group": "fastmcp.cli", + "pages": [ + "python-sdk/fastmcp-cli-__init__", + "python-sdk/fastmcp-cli-claude", + "python-sdk/fastmcp-cli-cli", + "python-sdk/fastmcp-cli-run" + ] + }, + { + "group": "fastmcp.client", + "pages": [ + "python-sdk/fastmcp-client-__init__", + { + "group": "auth", + "pages": [ + "python-sdk/fastmcp-client-auth-__init__", + "python-sdk/fastmcp-client-auth-bearer", + "python-sdk/fastmcp-client-auth-oauth" + ] + }, + "python-sdk/fastmcp-client-client", + "python-sdk/fastmcp-client-logging", + "python-sdk/fastmcp-client-oauth_callback", + "python-sdk/fastmcp-client-progress", + "python-sdk/fastmcp-client-roots", + "python-sdk/fastmcp-client-sampling", + "python-sdk/fastmcp-client-transports" + ] + }, + { + "group": "fastmcp.prompts", + "pages": [ + "python-sdk/fastmcp-prompts-__init__", + "python-sdk/fastmcp-prompts-prompt", + "python-sdk/fastmcp-prompts-prompt_manager" + ] + }, + { + "group": "fastmcp.resources", + "pages": [ + "python-sdk/fastmcp-resources-__init__", + "python-sdk/fastmcp-resources-resource", + "python-sdk/fastmcp-resources-resource_manager", + "python-sdk/fastmcp-resources-template", + "python-sdk/fastmcp-resources-types" + ] + }, + { + "group": "fastmcp.server", + "pages": [ + "python-sdk/fastmcp-server-__init__", + { + "group": "auth", + "pages": [ + "python-sdk/fastmcp-server-auth-__init__", + "python-sdk/fastmcp-server-auth-auth", + { + "group": "providers", + "pages": [ + "python-sdk/fastmcp-server-auth-providers-__init__", + "python-sdk/fastmcp-server-auth-providers-bearer", + "python-sdk/fastmcp-server-auth-providers-bearer_env", + "python-sdk/fastmcp-server-auth-providers-in_memory" + ] + } + ] + }, + "python-sdk/fastmcp-server-context", + "python-sdk/fastmcp-server-dependencies", + "python-sdk/fastmcp-server-http", + "python-sdk/fastmcp-server-middleware", + "python-sdk/fastmcp-server-openapi", + "python-sdk/fastmcp-server-proxy", + "python-sdk/fastmcp-server-server" + ] + }, + { + "group": "fastmcp.tools", + "pages": [ + "python-sdk/fastmcp-tools-__init__", + "python-sdk/fastmcp-tools-tool", + "python-sdk/fastmcp-tools-tool_manager", + "python-sdk/fastmcp-tools-tool_transform" + ] + }, + { + "group": "fastmcp.utilities", + "pages": [ + "python-sdk/fastmcp-utilities-__init__", + "python-sdk/fastmcp-utilities-cache", + "python-sdk/fastmcp-utilities-components", + "python-sdk/fastmcp-utilities-exceptions", + "python-sdk/fastmcp-utilities-http", + "python-sdk/fastmcp-utilities-json_schema", + "python-sdk/fastmcp-utilities-logging", + "python-sdk/fastmcp-utilities-mcp_config", + "python-sdk/fastmcp-utilities-openapi", + "python-sdk/fastmcp-utilities-types" + ] + } ] } ]