fastmcp 2.10.6__py3-none-any.whl → 2.11.1__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/cli/cli.py +128 -33
- fastmcp/cli/install/claude_code.py +42 -1
- fastmcp/cli/install/claude_desktop.py +42 -1
- fastmcp/cli/install/cursor.py +42 -1
- fastmcp/cli/install/mcp_json.py +41 -0
- fastmcp/cli/run.py +127 -1
- fastmcp/client/__init__.py +2 -0
- fastmcp/client/auth/oauth.py +68 -99
- fastmcp/client/oauth_callback.py +18 -0
- fastmcp/client/transports.py +69 -15
- fastmcp/contrib/component_manager/example.py +2 -2
- fastmcp/experimental/server/openapi/README.md +266 -0
- fastmcp/experimental/server/openapi/__init__.py +38 -0
- fastmcp/experimental/server/openapi/components.py +348 -0
- fastmcp/experimental/server/openapi/routing.py +132 -0
- fastmcp/experimental/server/openapi/server.py +466 -0
- fastmcp/experimental/utilities/openapi/README.md +239 -0
- fastmcp/experimental/utilities/openapi/__init__.py +68 -0
- fastmcp/experimental/utilities/openapi/director.py +208 -0
- fastmcp/experimental/utilities/openapi/formatters.py +355 -0
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
- fastmcp/experimental/utilities/openapi/models.py +85 -0
- fastmcp/experimental/utilities/openapi/parser.py +618 -0
- fastmcp/experimental/utilities/openapi/schemas.py +538 -0
- fastmcp/mcp_config.py +125 -88
- fastmcp/prompts/prompt.py +11 -1
- fastmcp/resources/resource.py +21 -1
- fastmcp/resources/template.py +20 -1
- fastmcp/server/auth/__init__.py +18 -2
- fastmcp/server/auth/auth.py +225 -7
- fastmcp/server/auth/providers/bearer.py +25 -473
- fastmcp/server/auth/providers/in_memory.py +4 -2
- fastmcp/server/auth/providers/jwt.py +538 -0
- fastmcp/server/auth/providers/workos.py +151 -0
- fastmcp/server/auth/registry.py +52 -0
- fastmcp/server/context.py +107 -26
- fastmcp/server/dependencies.py +9 -2
- fastmcp/server/http.py +48 -57
- fastmcp/server/middleware/middleware.py +3 -23
- fastmcp/server/openapi.py +1 -1
- fastmcp/server/proxy.py +50 -11
- fastmcp/server/server.py +168 -59
- fastmcp/settings.py +73 -6
- fastmcp/tools/tool.py +36 -3
- fastmcp/tools/tool_manager.py +38 -2
- fastmcp/tools/tool_transform.py +112 -3
- fastmcp/utilities/components.py +41 -3
- fastmcp/utilities/json_schema.py +136 -98
- fastmcp/utilities/json_schema_type.py +1 -3
- fastmcp/utilities/mcp_config.py +28 -0
- fastmcp/utilities/openapi.py +243 -57
- fastmcp/utilities/tests.py +54 -6
- fastmcp/utilities/types.py +94 -11
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/METADATA +4 -3
- fastmcp-2.11.1.dist-info/RECORD +108 -0
- fastmcp/server/auth/providers/bearer_env.py +0 -63
- fastmcp/utilities/cache.py +0 -26
- fastmcp-2.10.6.dist-info/RECORD +0 -93
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/run.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
"""FastMCP run command implementation with enhanced type hints."""
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
|
+
import json
|
|
4
5
|
import re
|
|
6
|
+
import subprocess
|
|
5
7
|
import sys
|
|
8
|
+
from functools import partial
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
from typing import Any, Literal
|
|
8
11
|
|
|
12
|
+
from mcp.server.fastmcp import FastMCP as FastMCP1x
|
|
13
|
+
|
|
14
|
+
from fastmcp.server.server import FastMCP
|
|
9
15
|
from fastmcp.utilities.logging import get_logger
|
|
10
16
|
|
|
11
17
|
logger = get_logger("cli.run")
|
|
@@ -122,6 +128,84 @@ def import_server(file: Path, server_object: str | None = None) -> Any:
|
|
|
122
128
|
return server
|
|
123
129
|
|
|
124
130
|
|
|
131
|
+
def run_with_uv(
|
|
132
|
+
server_spec: str,
|
|
133
|
+
python_version: str | None = None,
|
|
134
|
+
with_packages: list[str] | None = None,
|
|
135
|
+
with_requirements: Path | None = None,
|
|
136
|
+
project: Path | None = None,
|
|
137
|
+
transport: TransportType | None = None,
|
|
138
|
+
host: str | None = None,
|
|
139
|
+
port: int | None = None,
|
|
140
|
+
path: str | None = None,
|
|
141
|
+
log_level: LogLevelType | None = None,
|
|
142
|
+
show_banner: bool = True,
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Run a MCP server using uv run subprocess.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
server_spec: Python file, object specification (file:obj), or URL
|
|
148
|
+
python_version: Python version to use (e.g. "3.10")
|
|
149
|
+
with_packages: Additional packages to install
|
|
150
|
+
with_requirements: Requirements file to use
|
|
151
|
+
project: Run the command within the given project directory
|
|
152
|
+
transport: Transport protocol to use
|
|
153
|
+
host: Host to bind to when using http transport
|
|
154
|
+
port: Port to bind to when using http transport
|
|
155
|
+
path: Path to bind to when using http transport
|
|
156
|
+
log_level: Log level
|
|
157
|
+
show_banner: Whether to show the server banner
|
|
158
|
+
"""
|
|
159
|
+
cmd = ["uv", "run"]
|
|
160
|
+
|
|
161
|
+
# Add Python version if specified
|
|
162
|
+
if python_version:
|
|
163
|
+
cmd.extend(["--python", python_version])
|
|
164
|
+
|
|
165
|
+
# Add project if specified
|
|
166
|
+
if project:
|
|
167
|
+
cmd.extend(["--project", str(project)])
|
|
168
|
+
|
|
169
|
+
# Add fastmcp package
|
|
170
|
+
cmd.extend(["--with", "fastmcp"])
|
|
171
|
+
|
|
172
|
+
# Add additional packages
|
|
173
|
+
if with_packages:
|
|
174
|
+
for pkg in with_packages:
|
|
175
|
+
if pkg:
|
|
176
|
+
cmd.extend(["--with", pkg])
|
|
177
|
+
|
|
178
|
+
# Add requirements file
|
|
179
|
+
if with_requirements:
|
|
180
|
+
cmd.extend(["--with-requirements", str(with_requirements)])
|
|
181
|
+
|
|
182
|
+
# Add fastmcp run command
|
|
183
|
+
cmd.extend(["fastmcp", "run", server_spec])
|
|
184
|
+
|
|
185
|
+
# Add transport options
|
|
186
|
+
if transport:
|
|
187
|
+
cmd.extend(["--transport", transport])
|
|
188
|
+
if host:
|
|
189
|
+
cmd.extend(["--host", host])
|
|
190
|
+
if port:
|
|
191
|
+
cmd.extend(["--port", str(port)])
|
|
192
|
+
if path:
|
|
193
|
+
cmd.extend(["--path", path])
|
|
194
|
+
if log_level:
|
|
195
|
+
cmd.extend(["--log-level", log_level])
|
|
196
|
+
if not show_banner:
|
|
197
|
+
cmd.append("--no-banner")
|
|
198
|
+
|
|
199
|
+
# Run the command
|
|
200
|
+
logger.debug(f"Running command: {' '.join(cmd)}")
|
|
201
|
+
try:
|
|
202
|
+
process = subprocess.run(cmd, check=True)
|
|
203
|
+
sys.exit(process.returncode)
|
|
204
|
+
except subprocess.CalledProcessError as e:
|
|
205
|
+
logger.error(f"Failed to run server: {e}")
|
|
206
|
+
sys.exit(e.returncode)
|
|
207
|
+
|
|
208
|
+
|
|
125
209
|
def create_client_server(url: str) -> Any:
|
|
126
210
|
"""Create a FastMCP server from a client URL.
|
|
127
211
|
|
|
@@ -142,6 +226,17 @@ def create_client_server(url: str) -> Any:
|
|
|
142
226
|
sys.exit(1)
|
|
143
227
|
|
|
144
228
|
|
|
229
|
+
def create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]:
|
|
230
|
+
"""Create a FastMCP server from a MCPConfig."""
|
|
231
|
+
from fastmcp import FastMCP
|
|
232
|
+
|
|
233
|
+
with mcp_config_path.open() as src:
|
|
234
|
+
mcp_config = json.load(src)
|
|
235
|
+
|
|
236
|
+
server = FastMCP.as_proxy(mcp_config)
|
|
237
|
+
return server
|
|
238
|
+
|
|
239
|
+
|
|
145
240
|
def import_server_with_args(
|
|
146
241
|
file: Path, server_object: str | None = None, server_args: list[str] | None = None
|
|
147
242
|
) -> Any:
|
|
@@ -175,11 +270,12 @@ def run_command(
|
|
|
175
270
|
log_level: LogLevelType | None = None,
|
|
176
271
|
server_args: list[str] | None = None,
|
|
177
272
|
show_banner: bool = True,
|
|
273
|
+
use_direct_import: bool = False,
|
|
178
274
|
) -> None:
|
|
179
275
|
"""Run a MCP server or connect to a remote one.
|
|
180
276
|
|
|
181
277
|
Args:
|
|
182
|
-
server_spec: Python file, object specification (file:obj), or URL
|
|
278
|
+
server_spec: Python file, object specification (file:obj), MCPConfig file, or URL
|
|
183
279
|
transport: Transport protocol to use
|
|
184
280
|
host: Host to bind to when using http transport
|
|
185
281
|
port: Port to bind to when using http transport
|
|
@@ -187,11 +283,14 @@ def run_command(
|
|
|
187
283
|
log_level: Log level
|
|
188
284
|
server_args: Additional arguments to pass to the server
|
|
189
285
|
show_banner: Whether to show the server banner
|
|
286
|
+
use_direct_import: Whether to use direct import instead of subprocess
|
|
190
287
|
"""
|
|
191
288
|
if is_url(server_spec):
|
|
192
289
|
# Handle URL case
|
|
193
290
|
server = create_client_server(server_spec)
|
|
194
291
|
logger.debug(f"Created client proxy server for {server_spec}")
|
|
292
|
+
elif server_spec.endswith(".json"):
|
|
293
|
+
server = create_mcp_config_server(Path(server_spec))
|
|
195
294
|
else:
|
|
196
295
|
# Handle file case
|
|
197
296
|
file, server_object = parse_file_path(server_spec)
|
|
@@ -199,6 +298,12 @@ def run_command(
|
|
|
199
298
|
logger.debug(f'Found server "{server.name}" in {file}')
|
|
200
299
|
|
|
201
300
|
# Run the server
|
|
301
|
+
|
|
302
|
+
# handle v1 servers
|
|
303
|
+
if isinstance(server, FastMCP1x):
|
|
304
|
+
run_v1_server(server, host=host, port=port, transport=transport)
|
|
305
|
+
return
|
|
306
|
+
|
|
202
307
|
kwargs = {}
|
|
203
308
|
if transport:
|
|
204
309
|
kwargs["transport"] = transport
|
|
@@ -219,3 +324,24 @@ def run_command(
|
|
|
219
324
|
except Exception as e:
|
|
220
325
|
logger.error(f"Failed to run server: {e}")
|
|
221
326
|
sys.exit(1)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def run_v1_server(
|
|
330
|
+
server: FastMCP1x,
|
|
331
|
+
host: str | None = None,
|
|
332
|
+
port: int | None = None,
|
|
333
|
+
transport: TransportType | None = None,
|
|
334
|
+
) -> None:
|
|
335
|
+
if host:
|
|
336
|
+
server.settings.host = host
|
|
337
|
+
if port:
|
|
338
|
+
server.settings.port = port
|
|
339
|
+
match transport:
|
|
340
|
+
case "stdio":
|
|
341
|
+
runner = partial(server.run)
|
|
342
|
+
case "http" | "streamable-http" | None:
|
|
343
|
+
runner = partial(server.run, transport="streamable-http")
|
|
344
|
+
case "sse":
|
|
345
|
+
runner = partial(server.run, transport="sse")
|
|
346
|
+
|
|
347
|
+
runner()
|
fastmcp/client/__init__.py
CHANGED
|
@@ -7,6 +7,7 @@ from .transports import (
|
|
|
7
7
|
PythonStdioTransport,
|
|
8
8
|
NodeStdioTransport,
|
|
9
9
|
UvxStdioTransport,
|
|
10
|
+
UvStdioTransport,
|
|
10
11
|
NpxStdioTransport,
|
|
11
12
|
FastMCPTransport,
|
|
12
13
|
StreamableHttpTransport,
|
|
@@ -22,6 +23,7 @@ __all__ = [
|
|
|
22
23
|
"PythonStdioTransport",
|
|
23
24
|
"NodeStdioTransport",
|
|
24
25
|
"UvxStdioTransport",
|
|
26
|
+
"UvStdioTransport",
|
|
25
27
|
"NpxStdioTransport",
|
|
26
28
|
"FastMCPTransport",
|
|
27
29
|
"StreamableHttpTransport",
|
fastmcp/client/auth/oauth.py
CHANGED
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import webbrowser
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, Literal
|
|
8
|
-
from urllib.parse import
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
9
|
|
|
10
10
|
import anyio
|
|
11
11
|
import httpx
|
|
@@ -13,7 +13,6 @@ from mcp.client.auth import OAuthClientProvider, TokenStorage
|
|
|
13
13
|
from mcp.shared.auth import (
|
|
14
14
|
OAuthClientInformationFull,
|
|
15
15
|
OAuthClientMetadata,
|
|
16
|
-
OAuthMetadata,
|
|
17
16
|
)
|
|
18
17
|
from mcp.shared.auth import (
|
|
19
18
|
OAuthToken as OAuthToken,
|
|
@@ -150,41 +149,6 @@ class FileTokenStorage(TokenStorage):
|
|
|
150
149
|
logger.info("Cleared all OAuth client cache data.")
|
|
151
150
|
|
|
152
151
|
|
|
153
|
-
async def discover_oauth_metadata(
|
|
154
|
-
server_base_url: str, httpx_kwargs: dict[str, Any] | None = None
|
|
155
|
-
) -> OAuthMetadata | None:
|
|
156
|
-
"""
|
|
157
|
-
Discover OAuth metadata from the server using RFC 8414 well-known endpoint.
|
|
158
|
-
|
|
159
|
-
Args:
|
|
160
|
-
server_base_url: Base URL of the OAuth server (e.g., "https://example.com")
|
|
161
|
-
httpx_kwargs: Additional kwargs for httpx client
|
|
162
|
-
|
|
163
|
-
Returns:
|
|
164
|
-
OAuth metadata if found, None otherwise
|
|
165
|
-
"""
|
|
166
|
-
well_known_url = urljoin(server_base_url, "/.well-known/oauth-authorization-server")
|
|
167
|
-
logger.debug(f"Discovering OAuth metadata from: {well_known_url}")
|
|
168
|
-
|
|
169
|
-
async with httpx.AsyncClient(**(httpx_kwargs or {})) as client:
|
|
170
|
-
try:
|
|
171
|
-
response = await client.get(well_known_url, timeout=10.0)
|
|
172
|
-
if response.status_code == 200:
|
|
173
|
-
logger.debug("Successfully discovered OAuth metadata")
|
|
174
|
-
return OAuthMetadata.model_validate(response.json())
|
|
175
|
-
elif response.status_code == 404:
|
|
176
|
-
logger.debug(
|
|
177
|
-
"OAuth metadata not found (404) - server may not require auth"
|
|
178
|
-
)
|
|
179
|
-
return None
|
|
180
|
-
else:
|
|
181
|
-
logger.warning(f"OAuth metadata request failed: {response.status_code}")
|
|
182
|
-
return None
|
|
183
|
-
except (httpx.RequestError, json.JSONDecodeError, ValidationError) as e:
|
|
184
|
-
logger.debug(f"OAuth metadata discovery failed: {e}")
|
|
185
|
-
return None
|
|
186
|
-
|
|
187
|
-
|
|
188
152
|
async def check_if_auth_required(
|
|
189
153
|
mcp_url: str, httpx_kwargs: dict[str, Any] | None = None
|
|
190
154
|
) -> bool:
|
|
@@ -215,70 +179,86 @@ async def check_if_auth_required(
|
|
|
215
179
|
return True
|
|
216
180
|
|
|
217
181
|
|
|
218
|
-
|
|
219
|
-
mcp_url: str,
|
|
220
|
-
scopes: str | list[str] | None = None,
|
|
221
|
-
client_name: str = "FastMCP Client",
|
|
222
|
-
token_storage_cache_dir: Path | None = None,
|
|
223
|
-
additional_client_metadata: dict[str, Any] | None = None,
|
|
224
|
-
) -> OAuthClientProvider:
|
|
182
|
+
class OAuth(OAuthClientProvider):
|
|
225
183
|
"""
|
|
226
|
-
|
|
184
|
+
OAuth client provider for MCP servers with browser-based authentication.
|
|
227
185
|
|
|
228
|
-
This
|
|
229
|
-
|
|
186
|
+
This class provides OAuth authentication for FastMCP clients by opening
|
|
187
|
+
a browser for user authorization and running a local callback server.
|
|
188
|
+
"""
|
|
230
189
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
client_name:
|
|
236
|
-
token_storage_cache_dir:
|
|
237
|
-
additional_client_metadata:
|
|
190
|
+
def __init__(
|
|
191
|
+
self,
|
|
192
|
+
mcp_url: str,
|
|
193
|
+
scopes: str | list[str] | None = None,
|
|
194
|
+
client_name: str = "FastMCP Client",
|
|
195
|
+
token_storage_cache_dir: Path | None = None,
|
|
196
|
+
additional_client_metadata: dict[str, Any] | None = None,
|
|
197
|
+
callback_port: int | None = None,
|
|
198
|
+
):
|
|
199
|
+
"""
|
|
200
|
+
Initialize OAuth client provider for an MCP server.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
mcp_url: Full URL to the MCP endpoint (e.g. "http://host/mcp/sse/")
|
|
204
|
+
scopes: OAuth scopes to request. Can be a
|
|
205
|
+
space-separated string or a list of strings.
|
|
206
|
+
client_name: Name for this client during registration
|
|
207
|
+
token_storage_cache_dir: Directory for FileTokenStorage
|
|
208
|
+
additional_client_metadata: Extra fields for OAuthClientMetadata
|
|
209
|
+
callback_port: Fixed port for OAuth callback (default: random available port)
|
|
210
|
+
"""
|
|
211
|
+
parsed_url = urlparse(mcp_url)
|
|
212
|
+
server_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
|
213
|
+
|
|
214
|
+
# Setup OAuth client
|
|
215
|
+
self.redirect_port = callback_port or find_available_port()
|
|
216
|
+
redirect_uri = f"http://localhost:{self.redirect_port}/callback"
|
|
217
|
+
|
|
218
|
+
if isinstance(scopes, list):
|
|
219
|
+
scopes = " ".join(scopes)
|
|
220
|
+
|
|
221
|
+
client_metadata = OAuthClientMetadata(
|
|
222
|
+
client_name=client_name,
|
|
223
|
+
redirect_uris=[AnyHttpUrl(redirect_uri)],
|
|
224
|
+
grant_types=["authorization_code", "refresh_token"],
|
|
225
|
+
response_types=["code"],
|
|
226
|
+
# token_endpoint_auth_method="client_secret_post",
|
|
227
|
+
scope=scopes,
|
|
228
|
+
**(additional_client_metadata or {}),
|
|
229
|
+
)
|
|
238
230
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
token_endpoint_auth_method="client_secret_post",
|
|
258
|
-
scope=scopes,
|
|
259
|
-
**(additional_client_metadata or {}),
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
# Create server-specific token storage
|
|
263
|
-
storage = FileTokenStorage(
|
|
264
|
-
server_url=server_base_url, cache_dir=token_storage_cache_dir
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
# Define OAuth handlers
|
|
268
|
-
async def redirect_handler(authorization_url: str) -> None:
|
|
231
|
+
# Create server-specific token storage
|
|
232
|
+
storage = FileTokenStorage(
|
|
233
|
+
server_url=server_base_url, cache_dir=token_storage_cache_dir
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Store server_base_url for use in callback_handler
|
|
237
|
+
self.server_base_url = server_base_url
|
|
238
|
+
|
|
239
|
+
# Initialize parent class
|
|
240
|
+
super().__init__(
|
|
241
|
+
server_url=server_base_url,
|
|
242
|
+
client_metadata=client_metadata,
|
|
243
|
+
storage=storage,
|
|
244
|
+
redirect_handler=self.redirect_handler,
|
|
245
|
+
callback_handler=self.callback_handler,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
async def redirect_handler(self, authorization_url: str) -> None:
|
|
269
249
|
"""Open browser for authorization."""
|
|
270
250
|
logger.info(f"OAuth authorization URL: {authorization_url}")
|
|
271
251
|
webbrowser.open(authorization_url)
|
|
272
252
|
|
|
273
|
-
async def callback_handler() -> tuple[str, str | None]:
|
|
253
|
+
async def callback_handler(self) -> tuple[str, str | None]:
|
|
274
254
|
"""Handle OAuth callback and return (auth_code, state)."""
|
|
275
255
|
# Create a future to capture the OAuth response
|
|
276
256
|
response_future = asyncio.get_running_loop().create_future()
|
|
277
257
|
|
|
278
258
|
# Create server with the future
|
|
279
259
|
server = create_oauth_callback_server(
|
|
280
|
-
port=redirect_port,
|
|
281
|
-
server_url=server_base_url,
|
|
260
|
+
port=self.redirect_port,
|
|
261
|
+
server_url=self.server_base_url,
|
|
282
262
|
response_future=response_future,
|
|
283
263
|
)
|
|
284
264
|
|
|
@@ -286,7 +266,7 @@ def OAuth(
|
|
|
286
266
|
async with anyio.create_task_group() as tg:
|
|
287
267
|
tg.start_soon(server.serve)
|
|
288
268
|
logger.info(
|
|
289
|
-
f"🎧 OAuth callback server started on http://
|
|
269
|
+
f"🎧 OAuth callback server started on http://localhost:{self.redirect_port}"
|
|
290
270
|
)
|
|
291
271
|
|
|
292
272
|
TIMEOUT = 300.0 # 5 minute timeout
|
|
@@ -300,14 +280,3 @@ def OAuth(
|
|
|
300
280
|
server.should_exit = True
|
|
301
281
|
await asyncio.sleep(0.1) # Allow server to shutdown gracefully
|
|
302
282
|
tg.cancel_scope.cancel()
|
|
303
|
-
|
|
304
|
-
# Create OAuth provider
|
|
305
|
-
oauth_provider = OAuthClientProvider(
|
|
306
|
-
server_url=server_base_url,
|
|
307
|
-
client_metadata=client_metadata,
|
|
308
|
-
storage=storage,
|
|
309
|
-
redirect_handler=redirect_handler,
|
|
310
|
-
callback_handler=callback_handler,
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
return oauth_provider
|
fastmcp/client/oauth_callback.py
CHANGED
|
@@ -252,6 +252,24 @@ def create_oauth_callback_server(
|
|
|
252
252
|
status_code=400,
|
|
253
253
|
)
|
|
254
254
|
|
|
255
|
+
# Check for missing state parameter (indicates OAuth flow issue)
|
|
256
|
+
if callback_response.state is None:
|
|
257
|
+
# Resolve future with exception if provided
|
|
258
|
+
if response_future and not response_future.done():
|
|
259
|
+
response_future.set_exception(
|
|
260
|
+
RuntimeError(
|
|
261
|
+
"OAuth server did not return state parameter - authentication failed"
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return HTMLResponse(
|
|
266
|
+
create_callback_html(
|
|
267
|
+
"FastMCP OAuth Error: Authentication failed<br>The OAuth server did not return the expected state parameter",
|
|
268
|
+
is_success=False,
|
|
269
|
+
),
|
|
270
|
+
status_code=400,
|
|
271
|
+
)
|
|
272
|
+
|
|
255
273
|
# Success case
|
|
256
274
|
if response_future and not response_future.done():
|
|
257
275
|
response_future.set_result(
|
fastmcp/client/transports.py
CHANGED
|
@@ -48,6 +48,7 @@ __all__ = [
|
|
|
48
48
|
"FastMCPStdioTransport",
|
|
49
49
|
"NodeStdioTransport",
|
|
50
50
|
"UvxStdioTransport",
|
|
51
|
+
"UvStdioTransport",
|
|
51
52
|
"NpxStdioTransport",
|
|
52
53
|
"FastMCPTransport",
|
|
53
54
|
"infer_transport",
|
|
@@ -542,6 +543,63 @@ class NodeStdioTransport(StdioTransport):
|
|
|
542
543
|
self.script_path = script_path
|
|
543
544
|
|
|
544
545
|
|
|
546
|
+
class UvStdioTransport(StdioTransport):
|
|
547
|
+
"""Transport for running commands via the uv tool."""
|
|
548
|
+
|
|
549
|
+
def __init__(
|
|
550
|
+
self,
|
|
551
|
+
command: str,
|
|
552
|
+
args: list[str] | None = None,
|
|
553
|
+
module: bool = False,
|
|
554
|
+
project_directory: str | None = None,
|
|
555
|
+
python_version: str | None = None,
|
|
556
|
+
with_packages: list[str] | None = None,
|
|
557
|
+
with_requirements: str | None = None,
|
|
558
|
+
env_vars: dict[str, str] | None = None,
|
|
559
|
+
keep_alive: bool | None = None,
|
|
560
|
+
):
|
|
561
|
+
# Basic validation
|
|
562
|
+
if project_directory and not Path(project_directory).exists():
|
|
563
|
+
raise NotADirectoryError(
|
|
564
|
+
f"Project directory not found: {project_directory}"
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
# Build uv arguments
|
|
568
|
+
uv_args: list[str] = ["run"]
|
|
569
|
+
if project_directory:
|
|
570
|
+
uv_args.extend(["--directory", str(project_directory)])
|
|
571
|
+
if python_version:
|
|
572
|
+
uv_args.extend(["--python", python_version])
|
|
573
|
+
for pkg in with_packages or []:
|
|
574
|
+
uv_args.extend(["--with", pkg])
|
|
575
|
+
if with_requirements:
|
|
576
|
+
uv_args.extend(["--with-requirements", str(with_requirements)])
|
|
577
|
+
if module:
|
|
578
|
+
uv_args.append("--module")
|
|
579
|
+
|
|
580
|
+
if not args:
|
|
581
|
+
args = []
|
|
582
|
+
|
|
583
|
+
uv_args.extend([command, *args])
|
|
584
|
+
|
|
585
|
+
# Get environment with any additional variables
|
|
586
|
+
env: dict[str, str] | None = None
|
|
587
|
+
if env_vars or project_directory:
|
|
588
|
+
env = os.environ.copy()
|
|
589
|
+
if project_directory:
|
|
590
|
+
env["UV_PROJECT_DIR"] = str(project_directory)
|
|
591
|
+
if env_vars:
|
|
592
|
+
env.update(env_vars)
|
|
593
|
+
|
|
594
|
+
super().__init__(
|
|
595
|
+
command="uv",
|
|
596
|
+
args=uv_args,
|
|
597
|
+
env=env,
|
|
598
|
+
cwd=None, # Use --directory flag instead of cwd
|
|
599
|
+
keep_alive=keep_alive,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
|
|
545
603
|
class UvxStdioTransport(StdioTransport):
|
|
546
604
|
"""Transport for running commands via the uvx tool."""
|
|
547
605
|
|
|
@@ -579,7 +637,7 @@ class UvxStdioTransport(StdioTransport):
|
|
|
579
637
|
)
|
|
580
638
|
|
|
581
639
|
# Build uvx arguments
|
|
582
|
-
uvx_args = []
|
|
640
|
+
uvx_args: list[str] = []
|
|
583
641
|
if python_version:
|
|
584
642
|
uvx_args.extend(["--python", python_version])
|
|
585
643
|
if from_package:
|
|
@@ -592,11 +650,9 @@ class UvxStdioTransport(StdioTransport):
|
|
|
592
650
|
if tool_args:
|
|
593
651
|
uvx_args.extend(tool_args)
|
|
594
652
|
|
|
595
|
-
|
|
596
|
-
env = None
|
|
653
|
+
env: dict[str, str] | None = None
|
|
597
654
|
if env_vars:
|
|
598
655
|
env = os.environ.copy()
|
|
599
|
-
env.update(env_vars)
|
|
600
656
|
|
|
601
657
|
super().__init__(
|
|
602
658
|
command="uvx",
|
|
@@ -605,7 +661,7 @@ class UvxStdioTransport(StdioTransport):
|
|
|
605
661
|
cwd=project_directory,
|
|
606
662
|
keep_alive=keep_alive,
|
|
607
663
|
)
|
|
608
|
-
self.tool_name = tool_name
|
|
664
|
+
self.tool_name: str = tool_name
|
|
609
665
|
|
|
610
666
|
|
|
611
667
|
class NpxStdioTransport(StdioTransport):
|
|
@@ -732,7 +788,7 @@ class MCPConfigTransport(ClientTransport):
|
|
|
732
788
|
|
|
733
789
|
1. If the MCPConfig contains exactly one server, it creates a direct transport to that server.
|
|
734
790
|
2. If the MCPConfig contains multiple servers, it creates a composite client by mounting
|
|
735
|
-
all servers on a single FastMCP instance, with each server's name used as its mounting prefix.
|
|
791
|
+
all servers on a single FastMCP instance, with each server's name, by default, used as its mounting prefix.
|
|
736
792
|
|
|
737
793
|
In the multi-server case, tools are accessible with the prefix pattern `{server_name}_{tool_name}`
|
|
738
794
|
and resources with the pattern `protocol://{server_name}/path/to/resource`.
|
|
@@ -772,7 +828,9 @@ class MCPConfigTransport(ClientTransport):
|
|
|
772
828
|
```
|
|
773
829
|
"""
|
|
774
830
|
|
|
775
|
-
def __init__(self, config: MCPConfig | dict):
|
|
831
|
+
def __init__(self, config: MCPConfig | dict, name_as_prefix: bool = True):
|
|
832
|
+
from fastmcp.utilities.mcp_config import composite_server_from_mcp_config
|
|
833
|
+
|
|
776
834
|
if isinstance(config, dict):
|
|
777
835
|
config = MCPConfig.from_dict(config)
|
|
778
836
|
self.config = config
|
|
@@ -787,15 +845,11 @@ class MCPConfigTransport(ClientTransport):
|
|
|
787
845
|
|
|
788
846
|
# otherwise create a composite client
|
|
789
847
|
else:
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
composite_server.mount(
|
|
794
|
-
prefix=name,
|
|
795
|
-
server=FastMCP.as_proxy(backend=server.to_transport()),
|
|
848
|
+
self.transport = FastMCPTransport(
|
|
849
|
+
mcp=composite_server_from_mcp_config(
|
|
850
|
+
self.config, name_as_prefix=name_as_prefix
|
|
796
851
|
)
|
|
797
|
-
|
|
798
|
-
self.transport = FastMCPTransport(mcp=composite_server)
|
|
852
|
+
)
|
|
799
853
|
|
|
800
854
|
@contextlib.asynccontextmanager
|
|
801
855
|
async def connect_session(
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from fastmcp import FastMCP
|
|
2
2
|
from fastmcp.contrib.component_manager import set_up_component_manager
|
|
3
|
-
from fastmcp.server.auth.providers.
|
|
3
|
+
from fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair
|
|
4
4
|
|
|
5
5
|
key_pair = RSAKeyPair.generate()
|
|
6
6
|
|
|
7
|
-
auth =
|
|
7
|
+
auth = JWTVerifier(
|
|
8
8
|
public_key=key_pair.public_key,
|
|
9
9
|
issuer="https://dev.example.com",
|
|
10
10
|
audience="my-dev-server",
|