fastmcp 2.14.1__py3-none-any.whl → 2.14.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/prompts/prompt_manager.py +3 -4
- fastmcp/resources/resource_manager.py +9 -14
- fastmcp/server/auth/auth.py +20 -5
- fastmcp/server/auth/oauth_proxy.py +73 -15
- fastmcp/server/auth/providers/supabase.py +11 -6
- fastmcp/server/dependencies.py +5 -0
- fastmcp/server/elicitation.py +7 -3
- fastmcp/server/middleware/error_handling.py +1 -1
- fastmcp/server/openapi/components.py +2 -4
- fastmcp/server/server.py +17 -53
- fastmcp/tools/tool.py +5 -1
- fastmcp/tools/tool_manager.py +5 -7
- fastmcp/utilities/cli.py +23 -43
- fastmcp/utilities/json_schema.py +40 -0
- fastmcp/utilities/openapi/schemas.py +4 -4
- {fastmcp-2.14.1.dist-info → fastmcp-2.14.2.dist-info}/METADATA +6 -3
- {fastmcp-2.14.1.dist-info → fastmcp-2.14.2.dist-info}/RECORD +20 -20
- {fastmcp-2.14.1.dist-info → fastmcp-2.14.2.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.1.dist-info → fastmcp-2.14.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.1.dist-info → fastmcp-2.14.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,7 +7,7 @@ from typing import Any
|
|
|
7
7
|
from mcp import GetPromptResult
|
|
8
8
|
|
|
9
9
|
from fastmcp import settings
|
|
10
|
-
from fastmcp.exceptions import NotFoundError, PromptError
|
|
10
|
+
from fastmcp.exceptions import FastMCPError, NotFoundError, PromptError
|
|
11
11
|
from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
|
|
12
12
|
from fastmcp.settings import DuplicateBehavior
|
|
13
13
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -107,9 +107,8 @@ class PromptManager:
|
|
|
107
107
|
try:
|
|
108
108
|
messages = await prompt.render(arguments)
|
|
109
109
|
return GetPromptResult(description=prompt.description, messages=messages)
|
|
110
|
-
except
|
|
111
|
-
|
|
112
|
-
raise e
|
|
110
|
+
except FastMCPError:
|
|
111
|
+
raise
|
|
113
112
|
except Exception as e:
|
|
114
113
|
logger.exception(f"Error rendering prompt {name!r}")
|
|
115
114
|
if self.mask_error_details:
|
|
@@ -10,7 +10,7 @@ from typing import Any
|
|
|
10
10
|
from pydantic import AnyUrl
|
|
11
11
|
|
|
12
12
|
from fastmcp import settings
|
|
13
|
-
from fastmcp.exceptions import NotFoundError, ResourceError
|
|
13
|
+
from fastmcp.exceptions import FastMCPError, NotFoundError, ResourceError
|
|
14
14
|
from fastmcp.resources.resource import Resource
|
|
15
15
|
from fastmcp.resources.template import (
|
|
16
16
|
ResourceTemplate,
|
|
@@ -268,10 +268,9 @@ class ResourceManager:
|
|
|
268
268
|
uri_str,
|
|
269
269
|
params=params,
|
|
270
270
|
)
|
|
271
|
-
# Pass through
|
|
272
|
-
except
|
|
273
|
-
|
|
274
|
-
raise e
|
|
271
|
+
# Pass through FastMCPErrors as-is
|
|
272
|
+
except FastMCPError:
|
|
273
|
+
raise
|
|
275
274
|
# Handle other exceptions
|
|
276
275
|
except Exception as e:
|
|
277
276
|
logger.error(f"Error creating resource from template: {e}")
|
|
@@ -299,10 +298,9 @@ class ResourceManager:
|
|
|
299
298
|
try:
|
|
300
299
|
return await resource.read()
|
|
301
300
|
|
|
302
|
-
# raise
|
|
303
|
-
except
|
|
304
|
-
|
|
305
|
-
raise e
|
|
301
|
+
# raise FastMCPErrors as-is
|
|
302
|
+
except FastMCPError:
|
|
303
|
+
raise
|
|
306
304
|
|
|
307
305
|
# Handle other exceptions
|
|
308
306
|
except Exception as e:
|
|
@@ -322,11 +320,8 @@ class ResourceManager:
|
|
|
322
320
|
try:
|
|
323
321
|
resource = await template.create_resource(uri_str, params=params)
|
|
324
322
|
return await resource.read()
|
|
325
|
-
except
|
|
326
|
-
|
|
327
|
-
f"Error reading resource from template {uri_str!r}"
|
|
328
|
-
)
|
|
329
|
-
raise e
|
|
323
|
+
except FastMCPError:
|
|
324
|
+
raise
|
|
330
325
|
except Exception as e:
|
|
331
326
|
logger.exception(
|
|
332
327
|
f"Error reading resource from template {uri_str!r}"
|
fastmcp/server/auth/auth.py
CHANGED
|
@@ -114,6 +114,8 @@ class AuthProvider(TokenVerifierProtocol):
|
|
|
114
114
|
base_url = AnyHttpUrl(base_url)
|
|
115
115
|
self.base_url = base_url
|
|
116
116
|
self.required_scopes = required_scopes or []
|
|
117
|
+
self._mcp_path: str | None = None
|
|
118
|
+
self._resource_url: AnyHttpUrl | None = None
|
|
117
119
|
|
|
118
120
|
async def verify_token(self, token: str) -> AccessToken | None:
|
|
119
121
|
"""Verify a bearer token and return access info if valid.
|
|
@@ -128,6 +130,20 @@ class AuthProvider(TokenVerifierProtocol):
|
|
|
128
130
|
"""
|
|
129
131
|
raise NotImplementedError("Subclasses must implement verify_token")
|
|
130
132
|
|
|
133
|
+
def set_mcp_path(self, mcp_path: str | None) -> None:
|
|
134
|
+
"""Set the MCP endpoint path and compute resource URL.
|
|
135
|
+
|
|
136
|
+
This method is called by get_routes() to configure the expected
|
|
137
|
+
resource URL before route creation. Subclasses can override to
|
|
138
|
+
perform additional initialization that depends on knowing the
|
|
139
|
+
MCP endpoint path.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
|
|
143
|
+
"""
|
|
144
|
+
self._mcp_path = mcp_path
|
|
145
|
+
self._resource_url = self._get_resource_url(mcp_path)
|
|
146
|
+
|
|
131
147
|
def get_routes(
|
|
132
148
|
self,
|
|
133
149
|
mcp_path: str | None = None,
|
|
@@ -407,6 +423,8 @@ class OAuthProvider(
|
|
|
407
423
|
Returns:
|
|
408
424
|
List of OAuth routes
|
|
409
425
|
"""
|
|
426
|
+
# Configure resource URL before creating routes
|
|
427
|
+
self.set_mcp_path(mcp_path)
|
|
410
428
|
|
|
411
429
|
# Create standard OAuth authorization server routes
|
|
412
430
|
# Pass base_url as issuer_url to ensure metadata declares endpoints where
|
|
@@ -451,11 +469,8 @@ class OAuthProvider(
|
|
|
451
469
|
else:
|
|
452
470
|
oauth_routes.append(route)
|
|
453
471
|
|
|
454
|
-
# Get the resource URL based on the MCP path
|
|
455
|
-
resource_url = self._get_resource_url(mcp_path)
|
|
456
|
-
|
|
457
472
|
# Add protected resource routes if this server is also acting as a resource server
|
|
458
|
-
if
|
|
473
|
+
if self._resource_url:
|
|
459
474
|
supported_scopes = (
|
|
460
475
|
self.client_registration_options.valid_scopes
|
|
461
476
|
if self.client_registration_options
|
|
@@ -463,7 +478,7 @@ class OAuthProvider(
|
|
|
463
478
|
else self.required_scopes
|
|
464
479
|
)
|
|
465
480
|
protected_routes = create_protected_resource_routes(
|
|
466
|
-
resource_url=
|
|
481
|
+
resource_url=self._resource_url,
|
|
467
482
|
authorization_servers=[cast(AnyHttpUrl, self.issuer_url)],
|
|
468
483
|
scopes_supported=supported_scopes,
|
|
469
484
|
)
|
|
@@ -34,7 +34,6 @@ from authlib.integrations.httpx_client import AsyncOAuth2Client
|
|
|
34
34
|
from cryptography.fernet import Fernet
|
|
35
35
|
from key_value.aio.adapters.pydantic import PydanticAdapter
|
|
36
36
|
from key_value.aio.protocols import AsyncKeyValue
|
|
37
|
-
from key_value.aio.stores.disk import DiskStore
|
|
38
37
|
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
|
|
39
38
|
from mcp.server.auth.provider import (
|
|
40
39
|
AccessToken,
|
|
@@ -805,14 +804,16 @@ class OAuthProxy(OAuthProvider):
|
|
|
805
804
|
salt="fastmcp-jwt-signing-key",
|
|
806
805
|
)
|
|
807
806
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
)
|
|
807
|
+
# Store JWT signing key for deferred JWTIssuer creation in set_mcp_path()
|
|
808
|
+
self._jwt_signing_key: bytes = jwt_signing_key
|
|
809
|
+
# JWTIssuer will be created in set_mcp_path() with correct audience
|
|
810
|
+
self._jwt_issuer: JWTIssuer | None = None
|
|
813
811
|
|
|
814
812
|
# If the user does not provide a store, we will provide an encrypted disk store
|
|
815
813
|
if client_storage is None:
|
|
814
|
+
# Import lazily to avoid sqlite3 dependency when not using OAuthProxy
|
|
815
|
+
from key_value.aio.stores.disk import DiskStore
|
|
816
|
+
|
|
816
817
|
storage_encryption_key = derive_jwt_key(
|
|
817
818
|
high_entropy_material=jwt_signing_key.decode(),
|
|
818
819
|
salt="fastmcp-storage-encryption-key",
|
|
@@ -897,6 +898,47 @@ class OAuthProxy(OAuthProvider):
|
|
|
897
898
|
self._upstream_authorization_endpoint,
|
|
898
899
|
)
|
|
899
900
|
|
|
901
|
+
# -------------------------------------------------------------------------
|
|
902
|
+
# MCP Path Configuration
|
|
903
|
+
# -------------------------------------------------------------------------
|
|
904
|
+
|
|
905
|
+
def set_mcp_path(self, mcp_path: str | None) -> None:
|
|
906
|
+
"""Set the MCP endpoint path and create JWTIssuer with correct audience.
|
|
907
|
+
|
|
908
|
+
This method is called by get_routes() to configure the resource URL
|
|
909
|
+
and create the JWTIssuer. The JWT audience is set to the full resource
|
|
910
|
+
URL (e.g., http://localhost:8000/mcp) to ensure tokens are bound to
|
|
911
|
+
this specific MCP endpoint.
|
|
912
|
+
|
|
913
|
+
Args:
|
|
914
|
+
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
|
|
915
|
+
"""
|
|
916
|
+
super().set_mcp_path(mcp_path)
|
|
917
|
+
|
|
918
|
+
# Create JWT issuer with correct audience based on actual MCP path
|
|
919
|
+
# This ensures tokens are bound to the specific resource URL
|
|
920
|
+
self._jwt_issuer = JWTIssuer(
|
|
921
|
+
issuer=str(self.base_url),
|
|
922
|
+
audience=str(self._resource_url),
|
|
923
|
+
signing_key=self._jwt_signing_key,
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
logger.debug("Configured OAuth proxy for resource URL: %s", self._resource_url)
|
|
927
|
+
|
|
928
|
+
@property
|
|
929
|
+
def jwt_issuer(self) -> JWTIssuer:
|
|
930
|
+
"""Get the JWT issuer, ensuring it has been initialized.
|
|
931
|
+
|
|
932
|
+
The JWT issuer is created when set_mcp_path() is called (via get_routes()).
|
|
933
|
+
This property ensures a clear error if used before initialization.
|
|
934
|
+
"""
|
|
935
|
+
if self._jwt_issuer is None:
|
|
936
|
+
raise RuntimeError(
|
|
937
|
+
"JWT issuer not initialized. Ensure get_routes() is called "
|
|
938
|
+
"before token operations."
|
|
939
|
+
)
|
|
940
|
+
return self._jwt_issuer
|
|
941
|
+
|
|
900
942
|
# -------------------------------------------------------------------------
|
|
901
943
|
# PKCE Helper Methods
|
|
902
944
|
# -------------------------------------------------------------------------
|
|
@@ -998,13 +1040,29 @@ class OAuthProxy(OAuthProvider):
|
|
|
998
1040
|
"""Start OAuth transaction and route through consent interstitial.
|
|
999
1041
|
|
|
1000
1042
|
Flow:
|
|
1001
|
-
1.
|
|
1002
|
-
2.
|
|
1003
|
-
3.
|
|
1043
|
+
1. Validate client's resource matches server's resource URL (security check)
|
|
1044
|
+
2. Store transaction with client details and PKCE (if forwarding)
|
|
1045
|
+
3. Return local /consent URL; browser visits consent first
|
|
1046
|
+
4. Consent handler redirects to upstream IdP if approved/already approved
|
|
1004
1047
|
|
|
1005
1048
|
If consent is disabled (require_authorization_consent=False), skip the consent screen
|
|
1006
1049
|
and redirect directly to the upstream IdP.
|
|
1007
1050
|
"""
|
|
1051
|
+
# Security check: validate client's requested resource matches this server
|
|
1052
|
+
# This prevents tokens intended for one server from being used on another
|
|
1053
|
+
client_resource = getattr(params, "resource", None)
|
|
1054
|
+
if client_resource and self._resource_url:
|
|
1055
|
+
if str(client_resource) != str(self._resource_url):
|
|
1056
|
+
logger.warning(
|
|
1057
|
+
"Resource mismatch: client requested %s but server is %s",
|
|
1058
|
+
client_resource,
|
|
1059
|
+
self._resource_url,
|
|
1060
|
+
)
|
|
1061
|
+
raise AuthorizeError(
|
|
1062
|
+
error="invalid_target", # type: ignore[arg-type]
|
|
1063
|
+
error_description="Resource does not match this server",
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1008
1066
|
# Generate transaction ID for this authorization request
|
|
1009
1067
|
txn_id = secrets.token_urlsafe(32)
|
|
1010
1068
|
|
|
@@ -1216,7 +1274,7 @@ class OAuthProxy(OAuthProvider):
|
|
|
1216
1274
|
# Issue minimal FastMCP access token (just a reference via JTI)
|
|
1217
1275
|
if client.client_id is None:
|
|
1218
1276
|
raise TokenError("invalid_client", "Client ID is required")
|
|
1219
|
-
fastmcp_access_token = self.
|
|
1277
|
+
fastmcp_access_token = self.jwt_issuer.issue_access_token(
|
|
1220
1278
|
client_id=client.client_id,
|
|
1221
1279
|
scopes=authorization_code.scopes,
|
|
1222
1280
|
jti=access_jti,
|
|
@@ -1227,7 +1285,7 @@ class OAuthProxy(OAuthProvider):
|
|
|
1227
1285
|
# Use upstream refresh token expiry to align lifetimes
|
|
1228
1286
|
fastmcp_refresh_token = None
|
|
1229
1287
|
if refresh_jti and refresh_expires_in:
|
|
1230
|
-
fastmcp_refresh_token = self.
|
|
1288
|
+
fastmcp_refresh_token = self.jwt_issuer.issue_refresh_token(
|
|
1231
1289
|
client_id=client.client_id,
|
|
1232
1290
|
scopes=authorization_code.scopes,
|
|
1233
1291
|
jti=refresh_jti,
|
|
@@ -1352,7 +1410,7 @@ class OAuthProxy(OAuthProvider):
|
|
|
1352
1410
|
"""
|
|
1353
1411
|
# Verify FastMCP refresh token
|
|
1354
1412
|
try:
|
|
1355
|
-
refresh_payload = self.
|
|
1413
|
+
refresh_payload = self.jwt_issuer.verify_token(refresh_token.token)
|
|
1356
1414
|
refresh_jti = refresh_payload["jti"]
|
|
1357
1415
|
except Exception as e:
|
|
1358
1416
|
logger.debug("FastMCP refresh token validation failed: %s", e)
|
|
@@ -1461,7 +1519,7 @@ class OAuthProxy(OAuthProvider):
|
|
|
1461
1519
|
if client.client_id is None:
|
|
1462
1520
|
raise TokenError("invalid_client", "Client ID is required")
|
|
1463
1521
|
new_access_jti = secrets.token_urlsafe(32)
|
|
1464
|
-
new_fastmcp_access = self.
|
|
1522
|
+
new_fastmcp_access = self.jwt_issuer.issue_access_token(
|
|
1465
1523
|
client_id=client.client_id,
|
|
1466
1524
|
scopes=scopes,
|
|
1467
1525
|
jti=new_access_jti,
|
|
@@ -1482,7 +1540,7 @@ class OAuthProxy(OAuthProvider):
|
|
|
1482
1540
|
# Issue NEW minimal FastMCP refresh token (rotation for security)
|
|
1483
1541
|
# Use upstream refresh token expiry to align lifetimes
|
|
1484
1542
|
new_refresh_jti = secrets.token_urlsafe(32)
|
|
1485
|
-
new_fastmcp_refresh = self.
|
|
1543
|
+
new_fastmcp_refresh = self.jwt_issuer.issue_refresh_token(
|
|
1486
1544
|
client_id=client.client_id,
|
|
1487
1545
|
scopes=scopes,
|
|
1488
1546
|
jti=new_refresh_jti,
|
|
@@ -1558,7 +1616,7 @@ class OAuthProxy(OAuthProvider):
|
|
|
1558
1616
|
"""
|
|
1559
1617
|
try:
|
|
1560
1618
|
# 1. Verify FastMCP JWT signature and claims
|
|
1561
|
-
payload = self.
|
|
1619
|
+
payload = self.jwt_issuer.verify_token(token)
|
|
1562
1620
|
jti = payload["jti"]
|
|
1563
1621
|
|
|
1564
1622
|
# 2. Look up upstream token via JTI mapping
|
|
@@ -34,6 +34,7 @@ class SupabaseProviderSettings(BaseSettings):
|
|
|
34
34
|
|
|
35
35
|
project_url: AnyHttpUrl
|
|
36
36
|
base_url: AnyHttpUrl
|
|
37
|
+
auth_route: str = "/auth/v1"
|
|
37
38
|
algorithm: Literal["HS256", "RS256", "ES256"] = "ES256"
|
|
38
39
|
required_scopes: list[str] | None = None
|
|
39
40
|
|
|
@@ -59,8 +60,8 @@ class SupabaseProvider(RemoteAuthProvider):
|
|
|
59
60
|
- Asymmetric keys (RS256/ES256) are recommended for production
|
|
60
61
|
|
|
61
62
|
2. JWT Verification:
|
|
62
|
-
- FastMCP verifies JWTs using the JWKS endpoint at {project_url}
|
|
63
|
-
- JWTs are issued by {project_url}
|
|
63
|
+
- FastMCP verifies JWTs using the JWKS endpoint at {project_url}{auth_route}/.well-known/jwks.json
|
|
64
|
+
- JWTs are issued by {project_url}{auth_route}
|
|
64
65
|
- Tokens are cached for up to 10 minutes by Supabase's edge servers
|
|
65
66
|
- Algorithm must match your Supabase Auth configuration
|
|
66
67
|
|
|
@@ -93,6 +94,7 @@ class SupabaseProvider(RemoteAuthProvider):
|
|
|
93
94
|
*,
|
|
94
95
|
project_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
95
96
|
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
97
|
+
auth_route: str | NotSetT = NotSet,
|
|
96
98
|
algorithm: Literal["HS256", "RS256", "ES256"] | NotSetT = NotSet,
|
|
97
99
|
required_scopes: list[str] | NotSetT | None = NotSet,
|
|
98
100
|
token_verifier: TokenVerifier | None = None,
|
|
@@ -102,6 +104,7 @@ class SupabaseProvider(RemoteAuthProvider):
|
|
|
102
104
|
Args:
|
|
103
105
|
project_url: Your Supabase project URL (e.g., "https://abc123.supabase.co")
|
|
104
106
|
base_url: Public URL of this FastMCP server
|
|
107
|
+
auth_route: Supabase Auth route. Defaults to "/auth/v1".
|
|
105
108
|
algorithm: JWT signing algorithm (HS256, RS256, or ES256). Must match your
|
|
106
109
|
Supabase Auth configuration. Defaults to ES256.
|
|
107
110
|
required_scopes: Optional list of scopes to require for all requests.
|
|
@@ -115,6 +118,7 @@ class SupabaseProvider(RemoteAuthProvider):
|
|
|
115
118
|
for k, v in {
|
|
116
119
|
"project_url": project_url,
|
|
117
120
|
"base_url": base_url,
|
|
121
|
+
"auth_route": auth_route,
|
|
118
122
|
"algorithm": algorithm,
|
|
119
123
|
"required_scopes": required_scopes,
|
|
120
124
|
}.items()
|
|
@@ -124,12 +128,13 @@ class SupabaseProvider(RemoteAuthProvider):
|
|
|
124
128
|
|
|
125
129
|
self.project_url = str(settings.project_url).rstrip("/")
|
|
126
130
|
self.base_url = AnyHttpUrl(str(settings.base_url).rstrip("/"))
|
|
131
|
+
self.auth_route = settings.auth_route.strip("/")
|
|
127
132
|
|
|
128
133
|
# Create default JWT verifier if none provided
|
|
129
134
|
if token_verifier is None:
|
|
130
135
|
token_verifier = JWTVerifier(
|
|
131
|
-
jwks_uri=f"{self.project_url}/
|
|
132
|
-
issuer=f"{self.project_url}/
|
|
136
|
+
jwks_uri=f"{self.project_url}/{self.auth_route}/.well-known/jwks.json",
|
|
137
|
+
issuer=f"{self.project_url}/{self.auth_route}",
|
|
133
138
|
algorithm=settings.algorithm,
|
|
134
139
|
required_scopes=settings.required_scopes,
|
|
135
140
|
)
|
|
@@ -137,7 +142,7 @@ class SupabaseProvider(RemoteAuthProvider):
|
|
|
137
142
|
# Initialize RemoteAuthProvider with Supabase as the authorization server
|
|
138
143
|
super().__init__(
|
|
139
144
|
token_verifier=token_verifier,
|
|
140
|
-
authorization_servers=[AnyHttpUrl(f"{self.project_url}/
|
|
145
|
+
authorization_servers=[AnyHttpUrl(f"{self.project_url}/{self.auth_route}")],
|
|
141
146
|
base_url=self.base_url,
|
|
142
147
|
)
|
|
143
148
|
|
|
@@ -162,7 +167,7 @@ class SupabaseProvider(RemoteAuthProvider):
|
|
|
162
167
|
try:
|
|
163
168
|
async with httpx.AsyncClient() as client:
|
|
164
169
|
response = await client.get(
|
|
165
|
-
f"{self.project_url}/
|
|
170
|
+
f"{self.project_url}/{self.auth_route}/.well-known/oauth-authorization-server"
|
|
166
171
|
)
|
|
167
172
|
response.raise_for_status()
|
|
168
173
|
metadata = response.json()
|
fastmcp/server/dependencies.py
CHANGED
|
@@ -21,6 +21,7 @@ from mcp.server.auth.provider import (
|
|
|
21
21
|
from mcp.server.lowlevel.server import request_ctx
|
|
22
22
|
from starlette.requests import Request
|
|
23
23
|
|
|
24
|
+
from fastmcp.exceptions import FastMCPError
|
|
24
25
|
from fastmcp.server.auth import AccessToken
|
|
25
26
|
from fastmcp.server.http import _current_http_request
|
|
26
27
|
from fastmcp.utilities.types import is_class_member_of_type
|
|
@@ -188,6 +189,10 @@ async def _resolve_fastmcp_dependencies(
|
|
|
188
189
|
resolved[parameter] = await stack.enter_async_context(
|
|
189
190
|
dependency
|
|
190
191
|
)
|
|
192
|
+
except FastMCPError:
|
|
193
|
+
# Let FastMCPError subclasses (ToolError, ResourceError, etc.)
|
|
194
|
+
# propagate unchanged so they can be handled appropriately
|
|
195
|
+
raise
|
|
191
196
|
except Exception as error:
|
|
192
197
|
fn_name = getattr(fn, "__name__", repr(fn))
|
|
193
198
|
raise RuntimeError(
|
fastmcp/server/elicitation.py
CHANGED
|
@@ -304,15 +304,19 @@ def _dict_to_enum_schema(
|
|
|
304
304
|
multi_select: If True, use anyOf pattern; if False, use oneOf pattern
|
|
305
305
|
|
|
306
306
|
Returns:
|
|
307
|
-
{"
|
|
308
|
-
{"anyOf": [
|
|
307
|
+
{"type": "string", "oneOf": [...]} for single-select
|
|
308
|
+
{"anyOf": [...]} for multi-select (used as array items)
|
|
309
309
|
"""
|
|
310
310
|
pattern_key = "anyOf" if multi_select else "oneOf"
|
|
311
311
|
pattern = []
|
|
312
312
|
for value, metadata in enum_dict.items():
|
|
313
313
|
title = metadata.get("title", value)
|
|
314
314
|
pattern.append({"const": value, "title": title})
|
|
315
|
-
|
|
315
|
+
|
|
316
|
+
result: dict[str, Any] = {pattern_key: pattern}
|
|
317
|
+
if not multi_select:
|
|
318
|
+
result["type"] = "string"
|
|
319
|
+
return result
|
|
316
320
|
|
|
317
321
|
|
|
318
322
|
def get_elicitation_schema(response_type: type[T]) -> dict[str, Any]:
|
|
@@ -87,7 +87,7 @@ class ErrorHandlingMiddleware(Middleware):
|
|
|
87
87
|
return error
|
|
88
88
|
|
|
89
89
|
# Map common exceptions to appropriate MCP error codes
|
|
90
|
-
error_type = type(error)
|
|
90
|
+
error_type = type(error.__cause__) if error.__cause__ else type(error)
|
|
91
91
|
|
|
92
92
|
if error_type in (ValueError, TypeError):
|
|
93
93
|
return McpError(
|
|
@@ -64,10 +64,8 @@ class OpenAPITool(Tool):
|
|
|
64
64
|
try:
|
|
65
65
|
# Get base URL from client
|
|
66
66
|
base_url = (
|
|
67
|
-
str(self._client.base_url)
|
|
68
|
-
|
|
69
|
-
else "http://localhost"
|
|
70
|
-
)
|
|
67
|
+
str(self._client.base_url) if hasattr(self._client, "base_url") else ""
|
|
68
|
+
) or "http://localhost"
|
|
71
69
|
|
|
72
70
|
# Get Headers from client
|
|
73
71
|
cli_headers = (
|
fastmcp/server/server.py
CHANGED
|
@@ -105,23 +105,6 @@ if TYPE_CHECKING:
|
|
|
105
105
|
logger = get_logger(__name__)
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
def _create_named_fn_wrapper(fn: Callable[..., Any], name: str) -> Callable[..., Any]:
|
|
109
|
-
"""Create a wrapper function with a custom __name__ for Docket registration.
|
|
110
|
-
|
|
111
|
-
Docket uses fn.__name__ as the key for function registration and lookup.
|
|
112
|
-
When mounting servers, we need unique names to avoid collisions between
|
|
113
|
-
mounted servers that have identically-named functions.
|
|
114
|
-
"""
|
|
115
|
-
import functools
|
|
116
|
-
|
|
117
|
-
@functools.wraps(fn)
|
|
118
|
-
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
119
|
-
return await fn(*args, **kwargs)
|
|
120
|
-
|
|
121
|
-
wrapper.__name__ = name
|
|
122
|
-
return wrapper
|
|
123
|
-
|
|
124
|
-
|
|
125
108
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
126
109
|
Transport = Literal["stdio", "http", "sse", "streamable-http"]
|
|
127
110
|
|
|
@@ -437,7 +420,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
437
420
|
isinstance(tool, FunctionTool)
|
|
438
421
|
and tool.task_config.mode != "forbidden"
|
|
439
422
|
):
|
|
440
|
-
docket.register(tool.fn)
|
|
423
|
+
docket.register(tool.fn, names=[tool.key])
|
|
441
424
|
|
|
442
425
|
for prompt in self._prompt_manager._prompts.values():
|
|
443
426
|
if (
|
|
@@ -445,21 +428,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
445
428
|
and prompt.task_config.mode != "forbidden"
|
|
446
429
|
):
|
|
447
430
|
# task execution requires async fn (validated at creation time)
|
|
448
|
-
docket.register(
|
|
431
|
+
docket.register(
|
|
432
|
+
cast(Callable[..., Awaitable[Any]], prompt.fn),
|
|
433
|
+
names=[prompt.key],
|
|
434
|
+
)
|
|
449
435
|
|
|
450
436
|
for resource in self._resource_manager._resources.values():
|
|
451
437
|
if (
|
|
452
438
|
isinstance(resource, FunctionResource)
|
|
453
439
|
and resource.task_config.mode != "forbidden"
|
|
454
440
|
):
|
|
455
|
-
docket.register(resource.fn)
|
|
441
|
+
docket.register(resource.fn, names=[resource.name])
|
|
456
442
|
|
|
457
443
|
for template in self._resource_manager._templates.values():
|
|
458
444
|
if (
|
|
459
445
|
isinstance(template, FunctionResourceTemplate)
|
|
460
446
|
and template.task_config.mode != "forbidden"
|
|
461
447
|
):
|
|
462
|
-
docket.register(template.fn)
|
|
448
|
+
docket.register(template.fn, names=[template.name])
|
|
463
449
|
|
|
464
450
|
# Also register functions from mounted servers so tasks can
|
|
465
451
|
# execute in the parent's Docket context
|
|
@@ -489,12 +475,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
489
475
|
try:
|
|
490
476
|
yield
|
|
491
477
|
finally:
|
|
492
|
-
# Cancel worker task on exit with timeout to prevent hanging
|
|
493
478
|
worker_task.cancel()
|
|
494
|
-
with suppress(
|
|
495
|
-
|
|
496
|
-
):
|
|
497
|
-
await asyncio.wait_for(worker_task, timeout=2.0)
|
|
479
|
+
with suppress(asyncio.CancelledError):
|
|
480
|
+
await worker_task
|
|
498
481
|
finally:
|
|
499
482
|
_current_worker.reset(worker_token)
|
|
500
483
|
finally:
|
|
@@ -535,8 +518,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
535
518
|
fn_name = f"{prefix}_{tool.key}"
|
|
536
519
|
else:
|
|
537
520
|
fn_name = tool.key
|
|
538
|
-
|
|
539
|
-
docket.register(named_fn)
|
|
521
|
+
docket.register(tool.fn, names=[fn_name])
|
|
540
522
|
|
|
541
523
|
# Register prompts with prefixed names
|
|
542
524
|
for prompt in server._prompt_manager._prompts.values():
|
|
@@ -545,10 +527,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
545
527
|
and prompt.task_config.mode != "forbidden"
|
|
546
528
|
):
|
|
547
529
|
fn_name = f"{prefix}_{prompt.key}" if prefix else prompt.key
|
|
548
|
-
|
|
549
|
-
cast(Callable[..., Awaitable[Any]], prompt.fn),
|
|
530
|
+
docket.register(
|
|
531
|
+
cast(Callable[..., Awaitable[Any]], prompt.fn),
|
|
532
|
+
names=[fn_name],
|
|
550
533
|
)
|
|
551
|
-
docket.register(named_fn)
|
|
552
534
|
|
|
553
535
|
# Register resources with prefixed names (use name, not key/URI)
|
|
554
536
|
for resource in server._resource_manager._resources.values():
|
|
@@ -557,8 +539,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
557
539
|
and resource.task_config.mode != "forbidden"
|
|
558
540
|
):
|
|
559
541
|
fn_name = f"{prefix}_{resource.name}" if prefix else resource.name
|
|
560
|
-
|
|
561
|
-
docket.register(named_fn)
|
|
542
|
+
docket.register(resource.fn, names=[fn_name])
|
|
562
543
|
|
|
563
544
|
# Register resource templates with prefixed names (use name, not key/URI)
|
|
564
545
|
for template in server._resource_manager._templates.values():
|
|
@@ -567,8 +548,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
567
548
|
and template.task_config.mode != "forbidden"
|
|
568
549
|
):
|
|
569
550
|
fn_name = f"{prefix}_{template.name}" if prefix else template.name
|
|
570
|
-
|
|
571
|
-
docket.register(named_fn)
|
|
551
|
+
docket.register(template.fn, names=[fn_name])
|
|
572
552
|
|
|
573
553
|
# Recursively register from nested mounted servers with accumulated prefix
|
|
574
554
|
for nested in server._mounted_servers:
|
|
@@ -2516,10 +2496,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2516
2496
|
"""
|
|
2517
2497
|
# Display server banner
|
|
2518
2498
|
if show_banner:
|
|
2519
|
-
log_server_banner(
|
|
2520
|
-
server=self,
|
|
2521
|
-
transport="stdio",
|
|
2522
|
-
)
|
|
2499
|
+
log_server_banner(server=self)
|
|
2523
2500
|
|
|
2524
2501
|
with temporary_log_level(log_level):
|
|
2525
2502
|
async with self._lifespan_manager():
|
|
@@ -2582,22 +2559,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2582
2559
|
stateless_http=stateless_http,
|
|
2583
2560
|
)
|
|
2584
2561
|
|
|
2585
|
-
# Get the path for the server URL
|
|
2586
|
-
server_path = (
|
|
2587
|
-
app.state.path.lstrip("/")
|
|
2588
|
-
if hasattr(app, "state") and hasattr(app.state, "path")
|
|
2589
|
-
else path or ""
|
|
2590
|
-
)
|
|
2591
|
-
|
|
2592
2562
|
# Display server banner
|
|
2593
2563
|
if show_banner:
|
|
2594
|
-
log_server_banner(
|
|
2595
|
-
server=self,
|
|
2596
|
-
transport=transport,
|
|
2597
|
-
host=host,
|
|
2598
|
-
port=port,
|
|
2599
|
-
path=server_path,
|
|
2600
|
-
)
|
|
2564
|
+
log_server_banner(server=self)
|
|
2601
2565
|
uvicorn_config_from_user = uvicorn_config or {}
|
|
2602
2566
|
|
|
2603
2567
|
config_kwargs: dict[str, Any] = {
|
fastmcp/tools/tool.py
CHANGED
|
@@ -32,7 +32,7 @@ import fastmcp
|
|
|
32
32
|
from fastmcp.server.dependencies import get_context, without_injected_parameters
|
|
33
33
|
from fastmcp.server.tasks.config import TaskConfig
|
|
34
34
|
from fastmcp.utilities.components import FastMCPComponent
|
|
35
|
-
from fastmcp.utilities.json_schema import compress_schema
|
|
35
|
+
from fastmcp.utilities.json_schema import compress_schema, resolve_root_ref
|
|
36
36
|
from fastmcp.utilities.logging import get_logger
|
|
37
37
|
from fastmcp.utilities.types import (
|
|
38
38
|
Audio,
|
|
@@ -559,6 +559,10 @@ class ParsedFunction:
|
|
|
559
559
|
|
|
560
560
|
output_schema = compress_schema(output_schema, prune_titles=True)
|
|
561
561
|
|
|
562
|
+
# Resolve root-level $ref to meet MCP spec requirement for type: object
|
|
563
|
+
# Self-referential Pydantic models generate schemas with $ref at root
|
|
564
|
+
output_schema = resolve_root_ref(output_schema)
|
|
565
|
+
|
|
562
566
|
except PydanticSchemaGenerationError as e:
|
|
563
567
|
if "_UnserializableType" not in str(e):
|
|
564
568
|
logger.debug(f"Unable to generate schema for type {output_type!r}")
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -8,7 +8,7 @@ from mcp.types import ToolAnnotations
|
|
|
8
8
|
from pydantic import ValidationError
|
|
9
9
|
|
|
10
10
|
from fastmcp import settings
|
|
11
|
-
from fastmcp.exceptions import NotFoundError, ToolError
|
|
11
|
+
from fastmcp.exceptions import FastMCPError, NotFoundError, ToolError
|
|
12
12
|
from fastmcp.settings import DuplicateBehavior
|
|
13
13
|
from fastmcp.tools.tool import Tool, ToolResult
|
|
14
14
|
from fastmcp.tools.tool_transform import (
|
|
@@ -158,12 +158,10 @@ class ToolManager:
|
|
|
158
158
|
tool = await self.get_tool(key)
|
|
159
159
|
try:
|
|
160
160
|
return await tool.run(arguments)
|
|
161
|
-
except
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
logger.exception(f"Error calling tool {key!r}")
|
|
166
|
-
raise e
|
|
161
|
+
except FastMCPError:
|
|
162
|
+
raise
|
|
163
|
+
except ValidationError:
|
|
164
|
+
raise
|
|
167
165
|
except Exception as e:
|
|
168
166
|
logger.exception(f"Error calling tool {key!r}")
|
|
169
167
|
if self.mask_error_details:
|
fastmcp/utilities/cli.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
8
|
from pydantic import ValidationError
|
|
9
9
|
from rich.align import Align
|
|
@@ -197,23 +197,8 @@ LOGO_ASCII_4 = (
|
|
|
197
197
|
)
|
|
198
198
|
|
|
199
199
|
|
|
200
|
-
def log_server_banner(
|
|
201
|
-
server
|
|
202
|
-
transport: Literal["stdio", "http", "sse", "streamable-http"],
|
|
203
|
-
*,
|
|
204
|
-
host: str | None = None,
|
|
205
|
-
port: int | None = None,
|
|
206
|
-
path: str | None = None,
|
|
207
|
-
) -> None:
|
|
208
|
-
"""Creates and logs a formatted banner with server information and logo.
|
|
209
|
-
|
|
210
|
-
Args:
|
|
211
|
-
transport: The transport protocol being used
|
|
212
|
-
server_name: Optional server name to display
|
|
213
|
-
host: Host address (for HTTP transports)
|
|
214
|
-
port: Port number (for HTTP transports)
|
|
215
|
-
path: Server path (for HTTP transports)
|
|
216
|
-
"""
|
|
200
|
+
def log_server_banner(server: FastMCP[Any]) -> None:
|
|
201
|
+
"""Creates and logs a formatted banner with server information and logo."""
|
|
217
202
|
|
|
218
203
|
# Create the logo text
|
|
219
204
|
# Use Text with no_wrap and markup disabled to preserve ANSI escape codes
|
|
@@ -228,39 +213,34 @@ def log_server_banner(
|
|
|
228
213
|
info_table.add_column(style="cyan", justify="left") # Label column
|
|
229
214
|
info_table.add_column(style="dim", justify="left") # Value column
|
|
230
215
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
display_transport = "HTTP"
|
|
234
|
-
case "sse":
|
|
235
|
-
display_transport = "SSE"
|
|
236
|
-
case "stdio":
|
|
237
|
-
display_transport = "STDIO"
|
|
238
|
-
|
|
239
|
-
info_table.add_row("🖥", "Server name:", Text(server.name + "\n", style="bold blue"))
|
|
240
|
-
info_table.add_row("📦", "Transport:", display_transport)
|
|
241
|
-
|
|
242
|
-
# Show connection info based on transport
|
|
243
|
-
if transport in ("http", "streamable-http", "sse") and host and port:
|
|
244
|
-
server_url = f"http://{host}:{port}"
|
|
245
|
-
if path:
|
|
246
|
-
server_url += f"/{path.lstrip('/')}"
|
|
247
|
-
info_table.add_row("🔗", "Server URL:", server_url)
|
|
248
|
-
|
|
249
|
-
# Add documentation link
|
|
250
|
-
info_table.add_row("", "", "")
|
|
251
|
-
info_table.add_row("📚", "Docs:", "https://gofastmcp.com")
|
|
252
|
-
info_table.add_row("🚀", "Hosting:", "https://fastmcp.cloud")
|
|
216
|
+
info_table.add_row("🖥", "Server:", Text(server.name, style="dim"))
|
|
217
|
+
info_table.add_row("🚀", "Deploy free:", "https://fastmcp.cloud")
|
|
253
218
|
|
|
254
219
|
# Create panel with logo, title, and information using Group
|
|
220
|
+
docs_url = Text("https://gofastmcp.com", style="dim")
|
|
255
221
|
panel_content = Group(
|
|
222
|
+
"",
|
|
256
223
|
Align.center(logo_text),
|
|
257
224
|
"",
|
|
258
|
-
Align.center(title_text),
|
|
259
225
|
"",
|
|
226
|
+
Align.center(title_text),
|
|
227
|
+
Align.center(docs_url),
|
|
260
228
|
"",
|
|
261
229
|
Align.center(info_table),
|
|
262
230
|
)
|
|
263
231
|
|
|
232
|
+
# v3 notice banner (shown below main panel)
|
|
233
|
+
v3_line1 = Text("✨ FastMCP 3.0 is coming!", style="bold")
|
|
234
|
+
v3_line2 = Text(
|
|
235
|
+
"Pin fastmcp<3 in production, then upgrade when you're ready.", style="dim"
|
|
236
|
+
)
|
|
237
|
+
v3_notice = Panel(
|
|
238
|
+
Group(Align.center(v3_line1), Align.center(v3_line2)),
|
|
239
|
+
border_style="blue",
|
|
240
|
+
padding=(0, 2),
|
|
241
|
+
width=80,
|
|
242
|
+
)
|
|
243
|
+
|
|
264
244
|
panel = Panel(
|
|
265
245
|
panel_content,
|
|
266
246
|
border_style="dim",
|
|
@@ -270,5 +250,5 @@ def log_server_banner(
|
|
|
270
250
|
)
|
|
271
251
|
|
|
272
252
|
console = Console(stderr=True)
|
|
273
|
-
# Center
|
|
274
|
-
console.print(Group("\n", Align.center(panel), "\n"))
|
|
253
|
+
# Center both panels
|
|
254
|
+
console.print(Group("\n", Align.center(panel), Align.center(v3_notice), "\n"))
|
fastmcp/utilities/json_schema.py
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections import defaultdict
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def resolve_root_ref(schema: dict[str, Any]) -> dict[str, Any]:
|
|
8
|
+
"""Resolve $ref at root level to meet MCP spec requirements.
|
|
9
|
+
|
|
10
|
+
MCP specification requires outputSchema to have "type": "object" at the root level.
|
|
11
|
+
When Pydantic generates schemas for self-referential models, it uses $ref at the
|
|
12
|
+
root level pointing to $defs. This function resolves such references by inlining
|
|
13
|
+
the referenced definition while preserving $defs for nested references.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
schema: JSON schema dict that may have $ref at root level
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
A new schema dict with root-level $ref resolved, or the original schema
|
|
20
|
+
if no resolution is needed
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> schema = {
|
|
24
|
+
... "$defs": {"Node": {"type": "object", "properties": {...}}},
|
|
25
|
+
... "$ref": "#/$defs/Node"
|
|
26
|
+
... }
|
|
27
|
+
>>> resolved = resolve_root_ref(schema)
|
|
28
|
+
>>> # Result: {"type": "object", "properties": {...}, "$defs": {...}}
|
|
29
|
+
"""
|
|
30
|
+
# Only resolve if we have $ref at root level with $defs but no explicit type
|
|
31
|
+
if "$ref" in schema and "$defs" in schema and "type" not in schema:
|
|
32
|
+
ref = schema["$ref"]
|
|
33
|
+
# Only handle local $defs references
|
|
34
|
+
if isinstance(ref, str) and ref.startswith("#/$defs/"):
|
|
35
|
+
def_name = ref.split("/")[-1]
|
|
36
|
+
defs = schema["$defs"]
|
|
37
|
+
if def_name in defs:
|
|
38
|
+
# Create a new schema by copying the referenced definition
|
|
39
|
+
resolved = dict(defs[def_name])
|
|
40
|
+
# Preserve $defs for nested references (other fields may still use them)
|
|
41
|
+
resolved["$defs"] = defs
|
|
42
|
+
return resolved
|
|
43
|
+
return schema
|
|
4
44
|
|
|
5
45
|
|
|
6
46
|
def _prune_param(schema: dict, param: str) -> dict:
|
|
@@ -539,9 +539,9 @@ def extract_output_schema_from_responses(
|
|
|
539
539
|
# Replace $ref with the actual schema definition
|
|
540
540
|
output_schema = _replace_ref_with_defs(schema_definitions[schema_name])
|
|
541
541
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
542
|
+
if openapi_version and openapi_version.startswith("3"):
|
|
543
|
+
# Convert OpenAPI 3.x schema to JSON Schema format for proper handling
|
|
544
|
+
# of constructs like oneOf, anyOf, and nullable fields
|
|
545
545
|
from .json_schema_converter import convert_openapi_schema_to_json_schema
|
|
546
546
|
|
|
547
547
|
output_schema = convert_openapi_schema_to_json_schema(
|
|
@@ -570,7 +570,7 @@ def extract_output_schema_from_responses(
|
|
|
570
570
|
processed_defs[name] = _replace_ref_with_defs(schema)
|
|
571
571
|
|
|
572
572
|
# Convert OpenAPI schema definitions to JSON Schema format if needed
|
|
573
|
-
if openapi_version and openapi_version.startswith("3
|
|
573
|
+
if openapi_version and openapi_version.startswith("3"):
|
|
574
574
|
from .json_schema_converter import convert_openapi_schema_to_json_schema
|
|
575
575
|
|
|
576
576
|
for def_name in list(processed_defs.keys()):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastmcp
|
|
3
|
-
Version: 2.14.
|
|
3
|
+
Version: 2.14.2
|
|
4
4
|
Summary: The fast, Pythonic way to build MCP servers and clients.
|
|
5
5
|
Project-URL: Homepage, https://gofastmcp.com
|
|
6
6
|
Project-URL: Repository, https://github.com/jlowin/fastmcp
|
|
@@ -23,12 +23,12 @@ Requires-Dist: cyclopts>=4.0.0
|
|
|
23
23
|
Requires-Dist: exceptiongroup>=1.2.2
|
|
24
24
|
Requires-Dist: httpx>=0.28.1
|
|
25
25
|
Requires-Dist: jsonschema-path>=0.3.4
|
|
26
|
-
Requires-Dist: mcp
|
|
26
|
+
Requires-Dist: mcp<2.0,>=1.24.0
|
|
27
27
|
Requires-Dist: openapi-pydantic>=0.5.1
|
|
28
28
|
Requires-Dist: platformdirs>=4.0.0
|
|
29
29
|
Requires-Dist: py-key-value-aio[disk,keyring,memory]<0.4.0,>=0.3.0
|
|
30
30
|
Requires-Dist: pydantic[email]>=2.11.7
|
|
31
|
-
Requires-Dist: pydocket>=0.
|
|
31
|
+
Requires-Dist: pydocket>=0.16.3
|
|
32
32
|
Requires-Dist: pyperclip>=1.9.0
|
|
33
33
|
Requires-Dist: python-dotenv>=1.1.0
|
|
34
34
|
Requires-Dist: rich>=13.9.4
|
|
@@ -75,6 +75,9 @@ Description-Content-Type: text/markdown
|
|
|
75
75
|
>
|
|
76
76
|
> **For production MCP applications, install FastMCP:** `pip install fastmcp`
|
|
77
77
|
|
|
78
|
+
> [!Important]
|
|
79
|
+
> FastMCP 3.0 is in development and may include breaking changes. To avoid unexpected issues, pin your dependency to v2: `fastmcp<3`
|
|
80
|
+
|
|
78
81
|
---
|
|
79
82
|
|
|
80
83
|
**FastMCP is the standard framework for building MCP applications**, providing the fastest path from idea to production.
|
|
@@ -54,26 +54,26 @@ fastmcp/experimental/server/openapi/__init__.py,sha256=QNrM6ZNwJLk78jh7hq1tdZ-Wn
|
|
|
54
54
|
fastmcp/experimental/utilities/openapi/__init__.py,sha256=-SIYFQ4CE9MTxKQbksQ4J3lwm409EV3qKkHuTwAEyNk,907
|
|
55
55
|
fastmcp/prompts/__init__.py,sha256=BQ5ooDJcNhb5maYBcg2mF1VaHAY_A64cEU3UiCQ3Lw8,179
|
|
56
56
|
fastmcp/prompts/prompt.py,sha256=6Q6xKDIw5MdPC9dTAnM-9FRtA-34dv4qqTrD_5s2w0Y,14483
|
|
57
|
-
fastmcp/prompts/prompt_manager.py,sha256=
|
|
57
|
+
fastmcp/prompts/prompt_manager.py,sha256=OJiRAlWLPrPVfyzAzKfP_OHBXKqsU4eotrbUJNABz-A,4205
|
|
58
58
|
fastmcp/resources/__init__.py,sha256=si8aT_9taxUNN0vkfbifst_SCId56DZmYi4YOb4mtlE,463
|
|
59
59
|
fastmcp/resources/resource.py,sha256=PNzfTpywc5OIvDtFgAa8SGymzBbpwZCa32Mshh9YcGk,7890
|
|
60
|
-
fastmcp/resources/resource_manager.py,sha256=
|
|
60
|
+
fastmcp/resources/resource_manager.py,sha256=yG3EieKY9DqIcYTIFJkSJlRoXeffV6mTOnW3EwpoZfY,13008
|
|
61
61
|
fastmcp/resources/template.py,sha256=MSAK46bYk74nqJTQ923xb4KETlof9clfg_QaqLrJX_Y,15495
|
|
62
62
|
fastmcp/resources/types.py,sha256=efFLGD1Xc5Xq3sxlPaZ_8gtJ2UOixueTBV4KQTi4cOU,4936
|
|
63
63
|
fastmcp/server/__init__.py,sha256=qxNmIJcqsrpxpUvCv0mhdEAaUn1UZd1xLd8XRoWUlfY,119
|
|
64
64
|
fastmcp/server/context.py,sha256=nd0bME6I7aB9RlKiESNBay97P8yDHJJ-IcjVCbkc7bc,42256
|
|
65
|
-
fastmcp/server/dependencies.py,sha256=
|
|
66
|
-
fastmcp/server/elicitation.py,sha256=
|
|
65
|
+
fastmcp/server/dependencies.py,sha256=gRc60PhEvna9rlqMW-ZlYNszPlUeEeOWT5winYGNH2A,20928
|
|
66
|
+
fastmcp/server/elicitation.py,sha256=CmHi_SERmhEcNjwnM90_HGihUKlCM3RPGHI0uns2t7M,17912
|
|
67
67
|
fastmcp/server/event_store.py,sha256=ZiBbrUQHw9--G8lzK1qLZmUAF2le2XchFen4pGbFKsE,6170
|
|
68
68
|
fastmcp/server/http.py,sha256=_HjMSYWH8mfKugDODU4iV0AhKDU2VRc40tS56L6i-_s,12737
|
|
69
69
|
fastmcp/server/low_level.py,sha256=o3jDf5SuZBQeurhLWRzaSVCnvrmaKMH_w-TbHk6BuZ4,7963
|
|
70
70
|
fastmcp/server/proxy.py,sha256=bsgVkcdlRtVK3bB4EeVKrq4PLjIoUvWN_hgzr1hq8yE,26837
|
|
71
|
-
fastmcp/server/server.py,sha256=
|
|
71
|
+
fastmcp/server/server.py,sha256=3RmmJT2vM-ycXces_L6ORSQqlQvzMVITu37dCgqp-Ao,121385
|
|
72
72
|
fastmcp/server/auth/__init__.py,sha256=MTZvDKEUMqjs9-raRN0h8Zjx8pWFXs_iSRbB1UqBUqU,527
|
|
73
|
-
fastmcp/server/auth/auth.py,sha256=
|
|
73
|
+
fastmcp/server/auth/auth.py,sha256=Bvm98USOP0A0yTckKCN7yHJHS4JgCG804W5cQx6GgO4,20430
|
|
74
74
|
fastmcp/server/auth/jwt_issuer.py,sha256=lJYvrpC1ygI4jkoJlL_nTH6m7FKdTw2lbEycKo4eHLY,7197
|
|
75
75
|
fastmcp/server/auth/middleware.py,sha256=xwj3fUCLSlJK6n1Ehp-FN1qnjKqEz8b7LGAGMTqQ8Hk,3284
|
|
76
|
-
fastmcp/server/auth/oauth_proxy.py,sha256=
|
|
76
|
+
fastmcp/server/auth/oauth_proxy.py,sha256=c5BAyT07t3vZtfD9wCoN4f8hnSD1F5e0kCMqlUuO_00,93364
|
|
77
77
|
fastmcp/server/auth/oidc_proxy.py,sha256=gU_RgBbVMj-9vn0TSRTmT1YaT19VFmJLpARcIXn208k,17969
|
|
78
78
|
fastmcp/server/auth/redirect_validation.py,sha256=Jlhela9xpTbw4aWnQ04A5Z-TW0HYOC3f9BMsq3NXx1Q,2000
|
|
79
79
|
fastmcp/server/auth/handlers/authorize.py,sha256=1zrmXqRUhjiWSHgUhfj0CcCkj3uSlGkTnxHzaic0xYs,11617
|
|
@@ -91,11 +91,11 @@ fastmcp/server/auth/providers/introspection.py,sha256=v2hlcuxxwug5myCr4KcTZlawwa
|
|
|
91
91
|
fastmcp/server/auth/providers/jwt.py,sha256=c-2Wji-CvuYt3U3unxjJR-5-EABRDks_755EpxKBDH8,20798
|
|
92
92
|
fastmcp/server/auth/providers/oci.py,sha256=QxpsStKEyl_W4dcJOky4m6wdpGnCSnt7WQ8DWjGPmSU,9894
|
|
93
93
|
fastmcp/server/auth/providers/scalekit.py,sha256=30J2HImUAkyknMgH7lUGytcDOy4d01ClxTrBCO4E3GQ,9064
|
|
94
|
-
fastmcp/server/auth/providers/supabase.py,sha256=
|
|
94
|
+
fastmcp/server/auth/providers/supabase.py,sha256=T3Qq1mkkzZ9T9ah3uK7qRuMMLWeD_3eRLJRnpiqgTiY,7618
|
|
95
95
|
fastmcp/server/auth/providers/workos.py,sha256=_KWsgKPV4OJ6a37FaVgq2LIzM3Nx26G5QQhgS8x2MO4,17244
|
|
96
96
|
fastmcp/server/middleware/__init__.py,sha256=LXT2IcZI4gbAtR4TnA7v_1lOWBR6eaHiE3Cp32Pv0bc,155
|
|
97
97
|
fastmcp/server/middleware/caching.py,sha256=xYUXkFeuoLaIJ_TB2570qEBS1TtneJClJOpJGNsNbu8,18414
|
|
98
|
-
fastmcp/server/middleware/error_handling.py,sha256=
|
|
98
|
+
fastmcp/server/middleware/error_handling.py,sha256=TqERAA3qMvqb0Q0N_rwD5iOhoefOW2WT9IGSUZIWFik,7772
|
|
99
99
|
fastmcp/server/middleware/logging.py,sha256=Reta-f4z8suYkJn4rPyJWYrNBeU25w8Y40U0uaV9ygo,9427
|
|
100
100
|
fastmcp/server/middleware/middleware.py,sha256=-L4QuyyjIF1QIcydWzamrmpIE2w7d2f35-QyoXMZnZM,6643
|
|
101
101
|
fastmcp/server/middleware/rate_limiting.py,sha256=MwhMOhgsIhZjYwEQB8H8961hohV5564JlTwwYy_9ctU,7915
|
|
@@ -103,7 +103,7 @@ fastmcp/server/middleware/timing.py,sha256=lL_xc-ErLD5lplfvd5-HIyWEbZhgNBYkcQ74K
|
|
|
103
103
|
fastmcp/server/middleware/tool_injection.py,sha256=zElqBN-yjZvcTADp57e0dn86kpxT9xsFqvYztiXuA08,3595
|
|
104
104
|
fastmcp/server/openapi/README.md,sha256=1Mc1Ur15OxMn-wAPEa1rZIiNNSMdv9sboQ3YpvNpUXM,9886
|
|
105
105
|
fastmcp/server/openapi/__init__.py,sha256=cZPebMY9xwjW8nUgTN5MvawnZEFx9E0Oe_TFqSrevp0,728
|
|
106
|
-
fastmcp/server/openapi/components.py,sha256=
|
|
106
|
+
fastmcp/server/openapi/components.py,sha256=VdCwdyFh46Y8YIhz5qq1yVXhrQnIWu_KzWi9Ea2HOLc,13294
|
|
107
107
|
fastmcp/server/openapi/routing.py,sha256=_WWci6GNqtfF-5yO-uHwXXc9nNFNV-YlbIWHa7-lCk4,4018
|
|
108
108
|
fastmcp/server/openapi/server.py,sha256=aQ_VwvHxdsC-O-7k_uKmPDkOlcgtOW-gk-RtlLtEtuw,16069
|
|
109
109
|
fastmcp/server/sampling/__init__.py,sha256=u9jDHSE_yz6kTzbFqIOXqnM0PfIAiP-peAjHJBNqDd0,249
|
|
@@ -118,17 +118,17 @@ fastmcp/server/tasks/keys.py,sha256=w9diycj0N6ViVqe6stxUS9vg2H94bl_614Bu5kNRM-k,
|
|
|
118
118
|
fastmcp/server/tasks/protocol.py,sha256=g97D4k1U8ua_UBTyoqFXcPp5rf6KvuiY5d6mx5KMIPY,12222
|
|
119
119
|
fastmcp/server/tasks/subscriptions.py,sha256=iehPO2zx80aRIqKHCFj9kuR5NVMqYSkIepMXBifQFWw,6692
|
|
120
120
|
fastmcp/tools/__init__.py,sha256=XGcaMkBgwr-AHzbNjyjdb3ATgp5TQ0wzSq0nsrBD__E,201
|
|
121
|
-
fastmcp/tools/tool.py,sha256=
|
|
122
|
-
fastmcp/tools/tool_manager.py,sha256=
|
|
121
|
+
fastmcp/tools/tool.py,sha256=_l0HEnuTyYxm_xNWYxO2seRnzb6NunvjnEsWQIeKBDY,23394
|
|
122
|
+
fastmcp/tools/tool_manager.py,sha256=_SSHYgKygZaJ86B2pncmBm2Kbj0NLIDrpphsc9qgB3M,5788
|
|
123
123
|
fastmcp/tools/tool_transform.py,sha256=m1XDYuu_BDPxpH3yRNdT3jCca9KmVSO-Jd00BK4F5rw,38099
|
|
124
124
|
fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
|
|
125
125
|
fastmcp/utilities/auth.py,sha256=ZVHkNb4YBpLE1EmmFyhvFB2qfWDZdEYNH9TRI9jylOE,1140
|
|
126
|
-
fastmcp/utilities/cli.py,sha256=
|
|
126
|
+
fastmcp/utilities/cli.py,sha256=qjZtF9LvReKDOaLDIzcWWErMpWUBA8-Uvml8H4upuqs,11515
|
|
127
127
|
fastmcp/utilities/components.py,sha256=fF4M9cdqbZTlDAZ0hltcTTg_8IU2jNSzOyH4oqH49ig,6087
|
|
128
128
|
fastmcp/utilities/exceptions.py,sha256=7Z9j5IzM5rT27BC1Mcn8tkS-bjqCYqMKwb2MMTaxJYU,1350
|
|
129
129
|
fastmcp/utilities/http.py,sha256=1ns1ymBS-WSxbZjGP6JYjSO52Wa_ls4j4WbnXiupoa4,245
|
|
130
130
|
fastmcp/utilities/inspect.py,sha256=3wYUuQH1xCCCdzZwALHNqaRABH6iqpA43dIXEhqVb5Q,18030
|
|
131
|
-
fastmcp/utilities/json_schema.py,sha256
|
|
131
|
+
fastmcp/utilities/json_schema.py,sha256=H8RNucfulnXqYjCzRrlaWCBfToHmJGc7M32VJu5q7Eo,10587
|
|
132
132
|
fastmcp/utilities/json_schema_type.py,sha256=5cf1ZeHzqirrGx62kznqmgAWk0uCc29REVKcDRBeJX0,22348
|
|
133
133
|
fastmcp/utilities/logging.py,sha256=61wVk5yQ62km3K8kZtkKtT_3EN26VL85GYW0aMtnwKA,7175
|
|
134
134
|
fastmcp/utilities/mcp_config.py,sha256=lVllZtAXZ3Zy78D40aXN-S5fs-ms0lgryL1tY2WzwCY,1783
|
|
@@ -152,9 +152,9 @@ fastmcp/utilities/openapi/formatters.py,sha256=AWyETOfnBmTUcD1T2ajfkbsVyyMnN4tZ-
|
|
|
152
152
|
fastmcp/utilities/openapi/json_schema_converter.py,sha256=PxaYpgHBsdDTT0XSP6s4RZBMeDpAO_-dRXlBF2iYD9s,13089
|
|
153
153
|
fastmcp/utilities/openapi/models.py,sha256=-kfndwZSe92tVtKAgOuFn5rk1tN7oydCZKtLOEMEalA,2805
|
|
154
154
|
fastmcp/utilities/openapi/parser.py,sha256=qsa68Ro1c8ov77kdEP20IwZqD74E4IGKjtfeIkn3HdE,34338
|
|
155
|
-
fastmcp/utilities/openapi/schemas.py,sha256=
|
|
156
|
-
fastmcp-2.14.
|
|
157
|
-
fastmcp-2.14.
|
|
158
|
-
fastmcp-2.14.
|
|
159
|
-
fastmcp-2.14.
|
|
160
|
-
fastmcp-2.14.
|
|
155
|
+
fastmcp/utilities/openapi/schemas.py,sha256=UXHHjkJyDp1WwJ8kowYt79wnwdbDwAbUFfqwcIY6mIM,23359
|
|
156
|
+
fastmcp-2.14.2.dist-info/METADATA,sha256=pKt4x3lDwdG_CrWNz0oKxcuCYizqMuhFpUjK1vQsdCQ,20771
|
|
157
|
+
fastmcp-2.14.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
158
|
+
fastmcp-2.14.2.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
|
|
159
|
+
fastmcp-2.14.2.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
160
|
+
fastmcp-2.14.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|