fast-agent-mcp 0.3.4__py3-none-any.whl → 0.3.5__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.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

fast_agent/config.py CHANGED
@@ -9,14 +9,29 @@ from pathlib import Path
9
9
  from typing import Any, Dict, List, Literal, Optional, Tuple
10
10
 
11
11
  from mcp import Implementation
12
- from pydantic import BaseModel, ConfigDict, field_validator
12
+ from pydantic import BaseModel, ConfigDict, field_validator, model_validator
13
13
  from pydantic_settings import BaseSettings, SettingsConfigDict
14
14
 
15
15
 
16
16
  class MCPServerAuthSettings(BaseModel):
17
- """Represents authentication configuration for a server."""
17
+ """Represents authentication configuration for a server.
18
18
 
19
- api_key: str | None = None
19
+ Minimal OAuth v2.1 support with sensible defaults.
20
+ """
21
+
22
+ # Enable OAuth for SSE/HTTP transports. If None is provided for the auth block,
23
+ # the system will assume OAuth is enabled by default.
24
+ oauth: bool = True
25
+
26
+ # Local callback server configuration
27
+ redirect_port: int = 3030
28
+ redirect_path: str = "/callback"
29
+
30
+ # Optional scope override. If set to a list, values are space-joined.
31
+ scope: str | list[str] | None = None
32
+
33
+ # Token persistence: use OS keychain via 'keyring' by default; fallback to 'memory'.
34
+ persist: Literal["keyring", "memory"] = "keyring"
20
35
 
21
36
  model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
22
37
 
@@ -109,6 +124,42 @@ class MCPServerSettings(BaseModel):
109
124
 
110
125
  implementation: Implementation | None = None
111
126
 
127
+ @model_validator(mode="before")
128
+ @classmethod
129
+ def validate_transport_inference(cls, values):
130
+ """Automatically infer transport type based on url/command presence."""
131
+ import warnings
132
+
133
+ if isinstance(values, dict):
134
+ # Check if transport was explicitly provided in the input
135
+ transport_explicit = "transport" in values
136
+ url = values.get("url")
137
+ command = values.get("command")
138
+
139
+ # Only infer if transport was not explicitly set
140
+ if not transport_explicit:
141
+ # Check if we have both url and command specified
142
+ has_url = url is not None and str(url).strip()
143
+ has_command = command is not None and str(command).strip()
144
+
145
+ if has_url and has_command:
146
+ warnings.warn(
147
+ f"MCP Server config has both 'url' ({url}) and 'command' ({command}) specified. "
148
+ "Preferring HTTP transport and ignoring command.",
149
+ UserWarning,
150
+ stacklevel=4,
151
+ )
152
+ values["transport"] = "http"
153
+ values["command"] = None # Clear command to avoid confusion
154
+ elif has_url and not has_command:
155
+ values["transport"] = "http"
156
+ elif has_command and not has_url:
157
+ # Keep default "stdio" for command-based servers
158
+ values["transport"] = "stdio"
159
+ # If neither url nor command is specified, keep default "stdio"
160
+
161
+ return values
162
+
112
163
 
113
164
  class MCPSettings(BaseModel):
114
165
  """Configuration for all MCP servers."""
@@ -260,8 +311,8 @@ class TensorZeroSettings(BaseModel):
260
311
  Settings for using TensorZero via its OpenAI-compatible API.
261
312
  """
262
313
 
263
- base_url: Optional[str] = None
264
- api_key: Optional[str] = None
314
+ base_url: str | None = None
315
+ api_key: str | None = None
265
316
  model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
266
317
 
267
318
 
@@ -287,7 +338,7 @@ class HuggingFaceSettings(BaseModel):
287
338
  Settings for HuggingFace authentication (used for MCP connections).
288
339
  """
289
340
 
290
- api_key: Optional[str] = None
341
+ api_key: str | None = None
291
342
  model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
292
343
 
293
344
 
@@ -408,7 +459,7 @@ class Settings(BaseSettings):
408
459
  execution_engine: Literal["asyncio"] = "asyncio"
409
460
  """Execution engine for the fast-agent application"""
410
461
 
411
- default_model: str | None = "haiku"
462
+ default_model: str | None = "gpt-5-mini.low"
412
463
  """
413
464
  Default model for agents. Format is provider.model_name.<reasoning_effort>, for example openai.o3-mini.low
414
465
  Aliases are provided for common models e.g. sonnet, haiku, gpt-4.1, o3-mini etc.
@@ -459,7 +510,7 @@ class Settings(BaseSettings):
459
510
  groq: GroqSettings | None = None
460
511
  """Settings for using the Groq provider in the fast-agent application"""
461
512
 
462
- logger: LoggerSettings | None = LoggerSettings()
513
+ logger: LoggerSettings = LoggerSettings()
463
514
  """Logger settings for the fast-agent application"""
464
515
 
465
516
  # MCP UI integration mode for handling ui:// embedded resources from MCP tool results
@@ -33,6 +33,7 @@ from fast_agent.core.logging.logger import get_logger
33
33
  from fast_agent.event_progress import ProgressAction
34
34
  from fast_agent.mcp.logger_textio import get_stderr_handler
35
35
  from fast_agent.mcp.mcp_agent_client_session import MCPAgentClientSession
36
+ from fast_agent.mcp.oauth_client import build_oauth_provider
36
37
 
37
38
  if TYPE_CHECKING:
38
39
  from fast_agent.context import Context
@@ -341,6 +342,8 @@ class MCPConnectionManager(ContextDependent):
341
342
 
342
343
  def transport_context_factory():
343
344
  if config.transport == "stdio":
345
+ if not config.command:
346
+ raise ValueError(f"Server '{server_name}' uses stdio transport but no command is specified")
344
347
  server_params = StdioServerParameters(
345
348
  command=config.command,
346
349
  args=config.args if config.args is not None else [],
@@ -353,18 +356,33 @@ class MCPConnectionManager(ContextDependent):
353
356
  logger.debug(f"{server_name}: Creating stdio client with custom error handler")
354
357
  return _add_none_to_context(stdio_client(server_params, errlog=error_handler))
355
358
  elif config.transport == "sse":
359
+ if not config.url:
360
+ raise ValueError(f"Server '{server_name}' uses sse transport but no url is specified")
356
361
  # Suppress MCP library error spam
357
362
  self._suppress_mcp_sse_errors()
358
-
363
+ oauth_auth = build_oauth_provider(config)
364
+ # If using OAuth, strip any pre-existing Authorization headers to avoid conflicts
365
+ headers = dict(config.headers or {})
366
+ if oauth_auth is not None:
367
+ headers.pop("Authorization", None)
368
+ headers.pop("X-HF-Authorization", None)
359
369
  return _add_none_to_context(
360
370
  sse_client(
361
371
  config.url,
362
- config.headers,
372
+ headers,
363
373
  sse_read_timeout=config.read_transport_sse_timeout_seconds,
374
+ auth=oauth_auth,
364
375
  )
365
376
  )
366
377
  elif config.transport == "http":
367
- return streamablehttp_client(config.url, config.headers)
378
+ if not config.url:
379
+ raise ValueError(f"Server '{server_name}' uses http transport but no url is specified")
380
+ oauth_auth = build_oauth_provider(config)
381
+ headers = dict(config.headers or {})
382
+ if oauth_auth is not None:
383
+ headers.pop("Authorization", None)
384
+ headers.pop("X-HF-Authorization", None)
385
+ return streamablehttp_client(config.url, headers, auth=oauth_auth)
368
386
  else:
369
387
  raise ValueError(f"Unsupported transport: {config.transport}")
370
388
 
@@ -0,0 +1,481 @@
1
+ """
2
+ OAuth v2.1 integration helpers for MCP client transports.
3
+
4
+ Provides token storage (in-memory and OS keyring), a local callback server
5
+ with paste-URL fallback, and a builder for OAuthClientProvider that can be
6
+ passed to SSE/HTTP transports as the `auth` parameter.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import threading
12
+ import time
13
+ from dataclasses import dataclass
14
+ from http.server import BaseHTTPRequestHandler, HTTPServer
15
+ from typing import TYPE_CHECKING, Any, Callable
16
+ from urllib.parse import parse_qs, urlparse
17
+
18
+ from mcp.client.auth import OAuthClientProvider, TokenStorage
19
+ from mcp.shared.auth import (
20
+ OAuthClientInformationFull,
21
+ OAuthClientMetadata,
22
+ OAuthToken,
23
+ )
24
+ from pydantic import AnyUrl
25
+
26
+ from fast_agent.core.logging.logger import get_logger
27
+ from fast_agent.ui import console
28
+
29
+ if TYPE_CHECKING:
30
+ from fast_agent.config import MCPServerSettings
31
+
32
+ logger = get_logger(__name__)
33
+
34
+
35
+ class InMemoryTokenStorage(TokenStorage):
36
+ """Non-persistent token storage (process memory only)."""
37
+
38
+ def __init__(self) -> None:
39
+ self._tokens: OAuthToken | None = None
40
+ self._client_info: OAuthClientInformationFull | None = None
41
+
42
+ async def get_tokens(self) -> OAuthToken | None:
43
+ return self._tokens
44
+
45
+ async def set_tokens(self, tokens: OAuthToken) -> None:
46
+ self._tokens = tokens
47
+
48
+ async def get_client_info(self) -> OAuthClientInformationFull | None:
49
+ return self._client_info
50
+
51
+ async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
52
+ self._client_info = client_info
53
+
54
+
55
+ @dataclass
56
+ class _CallbackResult:
57
+ authorization_code: str | None = None
58
+ state: str | None = None
59
+ error: str | None = None
60
+
61
+
62
+ class _CallbackHandler(BaseHTTPRequestHandler):
63
+ """HTTP handler to capture OAuth callback query params."""
64
+
65
+ def __init__(self, *args, result: _CallbackResult, expected_path: str, **kwargs):
66
+ self._result = result
67
+ self._expected_path = expected_path.rstrip("/") or "/callback"
68
+ super().__init__(*args, **kwargs)
69
+
70
+ def do_GET(self) -> None: # noqa: N802 - http.server signature
71
+ parsed = urlparse(self.path)
72
+
73
+ # Only accept the configured callback path
74
+ if (parsed.path.rstrip("/") or "/callback") != self._expected_path:
75
+ self.send_response(404)
76
+ self.end_headers()
77
+ return
78
+
79
+ params = parse_qs(parsed.query)
80
+ if "code" in params:
81
+ self._result.authorization_code = params["code"][0]
82
+ self._result.state = params.get("state", [None])[0]
83
+ self.send_response(200)
84
+ self.send_header("Content-Type", "text/html")
85
+ self.end_headers()
86
+ self.wfile.write(
87
+ b"""
88
+ <html><body>
89
+ <h1>Authorization Successful</h1>
90
+ <p>You can close this window.</p>
91
+ <script>setTimeout(() => window.close(), 1000);</script>
92
+ </body></html>
93
+ """
94
+ )
95
+ elif "error" in params:
96
+ self._result.error = params["error"][0]
97
+ self.send_response(400)
98
+ self.send_header("Content-Type", "text/html")
99
+ self.end_headers()
100
+ self.wfile.write(
101
+ f"""
102
+ <html><body>
103
+ <h1>Authorization Failed</h1>
104
+ <p>Error: {self._result.error}</p>
105
+ </body></html>
106
+ """.encode()
107
+ )
108
+ else:
109
+ self.send_response(404)
110
+ self.end_headers()
111
+
112
+ def log_message(self, fmt: str, *args: Any) -> None: # silence default logging
113
+ return
114
+
115
+
116
+ class _CallbackServer:
117
+ """Simple background HTTP server to receive a single OAuth callback."""
118
+
119
+ def __init__(self, port: int, path: str) -> None:
120
+ self._port = port
121
+ self._path = path.rstrip("/") or "/callback"
122
+ self._result = _CallbackResult()
123
+ self._server: HTTPServer | None = None
124
+ self._thread: threading.Thread | None = None
125
+
126
+ def _make_handler(self) -> Callable[..., BaseHTTPRequestHandler]:
127
+ result = self._result
128
+ expected_path = self._path
129
+
130
+ def handler(*args, **kwargs):
131
+ return _CallbackHandler(*args, result=result, expected_path=expected_path, **kwargs)
132
+
133
+ return handler
134
+
135
+ def start(self) -> None:
136
+ self._server = HTTPServer(("localhost", self._port), self._make_handler())
137
+ self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
138
+ self._thread.start()
139
+ logger.info(f"OAuth callback server listening on http://localhost:{self._port}{self._path}")
140
+
141
+ def stop(self) -> None:
142
+ if self._server:
143
+ try:
144
+ self._server.shutdown()
145
+ self._server.server_close()
146
+ except Exception:
147
+ pass
148
+ if self._thread:
149
+ self._thread.join(timeout=1)
150
+
151
+ def wait(self, timeout_seconds: int = 300) -> tuple[str, str | None]:
152
+ start = time.time()
153
+ while time.time() - start < timeout_seconds:
154
+ if self._result.authorization_code:
155
+ return self._result.authorization_code, self._result.state
156
+ if self._result.error:
157
+ raise RuntimeError(f"OAuth error: {self._result.error}")
158
+ time.sleep(0.1)
159
+ raise TimeoutError("Timeout waiting for OAuth callback")
160
+
161
+
162
+ def _derive_base_server_url(url: str | None) -> str | None:
163
+ """Derive the base server URL for OAuth discovery from an MCP endpoint URL.
164
+
165
+ - Strips a trailing "/mcp" or "/sse" path segment
166
+ - Ignores query and fragment parts entirely
167
+ """
168
+ if not url:
169
+ return None
170
+ try:
171
+ from urllib.parse import urlparse, urlunparse
172
+
173
+ parsed = urlparse(url)
174
+ # Normalize path without trailing slash
175
+ path = parsed.path or ""
176
+ path = path[:-1] if path.endswith("/") else path
177
+ # Remove one trailing segment if it is mcp or sse
178
+ for suffix in ("/mcp", "/sse"):
179
+ if path.endswith(suffix):
180
+ path = path[: -len(suffix)]
181
+ break
182
+ # Ensure path is at least '/'
183
+ if not path:
184
+ path = "/"
185
+ # Rebuild URL without query/fragment
186
+ clean = parsed._replace(path=path, params="", query="", fragment="")
187
+ base = urlunparse(clean)
188
+ # Drop trailing slash except for root
189
+ if base.endswith("/") and base.count("/") > 2:
190
+ base = base[:-1]
191
+ return base
192
+ except Exception:
193
+ return url
194
+
195
+
196
+ def compute_server_identity(server_config: MCPServerSettings) -> str:
197
+ """Compute a stable identity for token storage.
198
+
199
+ Prefer the normalized base server URL; fall back to configured name, then 'default'.
200
+ """
201
+ base = _derive_base_server_url(server_config.url)
202
+ if base:
203
+ return base
204
+ if server_config.name:
205
+ return server_config.name
206
+ return "default"
207
+
208
+
209
+ def keyring_has_token(server_config: MCPServerSettings) -> bool:
210
+ """Check if keyring has a token stored for this server."""
211
+ try:
212
+ import keyring
213
+
214
+ identity = compute_server_identity(server_config)
215
+ token_key = f"oauth:tokens:{identity}"
216
+ return keyring.get_password("fast-agent-mcp", token_key) is not None
217
+ except Exception:
218
+ return False
219
+
220
+
221
+ async def _print_authorization_link(auth_url: str) -> None:
222
+ """Emit a clickable authorization link using rich console markup."""
223
+ console.console.print("[bold]Open this link to authorize:[/bold]", markup=True)
224
+ console.console.print(f"[link={auth_url}]{auth_url}[/link]")
225
+ logger.info("OAuth authorization URL emitted to console")
226
+
227
+
228
+ class KeyringTokenStorage(TokenStorage):
229
+ """Token storage backed by the OS keychain using 'keyring'."""
230
+
231
+ def __init__(self, service_name: str, server_identity: str) -> None:
232
+ self._service = service_name
233
+ self._identity = server_identity
234
+
235
+ @property
236
+ def _token_key(self) -> str:
237
+ return f"oauth:tokens:{self._identity}"
238
+
239
+ @property
240
+ def _client_key(self) -> str:
241
+ return f"oauth:client_info:{self._identity}"
242
+
243
+ async def get_tokens(self) -> OAuthToken | None:
244
+ try:
245
+ import keyring
246
+
247
+ payload = keyring.get_password(self._service, self._token_key)
248
+ if not payload:
249
+ return None
250
+ return OAuthToken.model_validate_json(payload)
251
+ except Exception:
252
+ return None
253
+
254
+ async def set_tokens(self, tokens: OAuthToken) -> None:
255
+ try:
256
+ import keyring
257
+
258
+ keyring.set_password(self._service, self._token_key, tokens.model_dump_json())
259
+ # Update index
260
+ add_identity_to_index(self._service, self._identity)
261
+ except Exception:
262
+ pass
263
+
264
+ async def get_client_info(self) -> OAuthClientInformationFull | None:
265
+ try:
266
+ import keyring
267
+
268
+ payload = keyring.get_password(self._service, self._client_key)
269
+ if not payload:
270
+ return None
271
+ return OAuthClientInformationFull.model_validate_json(payload)
272
+ except Exception:
273
+ return None
274
+
275
+ async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
276
+ try:
277
+ import keyring
278
+
279
+ keyring.set_password(self._service, self._client_key, client_info.model_dump_json())
280
+ except Exception:
281
+ pass
282
+
283
+
284
+ # --- Keyring index helpers (to enable cross-platform token enumeration) ---
285
+
286
+ def _index_username() -> str:
287
+ return "oauth:index"
288
+
289
+
290
+ def _read_index(service: str) -> set[str]:
291
+ try:
292
+ import json
293
+
294
+ import keyring
295
+
296
+ raw = keyring.get_password(service, _index_username())
297
+ if not raw:
298
+ return set()
299
+ data = json.loads(raw)
300
+ if isinstance(data, list):
301
+ return set([str(x) for x in data])
302
+ return set()
303
+ except Exception:
304
+ return set()
305
+
306
+
307
+ def _write_index(service: str, identities: set[str]) -> None:
308
+ try:
309
+ import json
310
+
311
+ import keyring
312
+
313
+ payload = json.dumps(sorted(list(identities)))
314
+ keyring.set_password(service, _index_username(), payload)
315
+ except Exception:
316
+ pass
317
+
318
+
319
+ def add_identity_to_index(service: str, identity: str) -> None:
320
+ identities = _read_index(service)
321
+ if identity not in identities:
322
+ identities.add(identity)
323
+ _write_index(service, identities)
324
+
325
+
326
+ def remove_identity_from_index(service: str, identity: str) -> None:
327
+ identities = _read_index(service)
328
+ if identity in identities:
329
+ identities.remove(identity)
330
+ _write_index(service, identities)
331
+
332
+
333
+ def list_keyring_tokens(service: str = "fast-agent-mcp") -> list[str]:
334
+ """List identities with stored tokens in keyring (using our index).
335
+
336
+ Returns only identities that currently have a corresponding token entry.
337
+ """
338
+ try:
339
+ import keyring
340
+
341
+ identities = _read_index(service)
342
+ present: list[str] = []
343
+ for ident in sorted(identities):
344
+ tok_key = f"oauth:tokens:{ident}"
345
+ if keyring.get_password(service, tok_key):
346
+ present.append(ident)
347
+ return present
348
+ except Exception:
349
+ return []
350
+
351
+
352
+ def clear_keyring_token(identity: str, service: str = "fast-agent-mcp") -> bool:
353
+ """Remove token+client info for identity and update the index.
354
+
355
+ Returns True if anything was removed.
356
+ """
357
+ removed = False
358
+ try:
359
+ import keyring
360
+
361
+ tok_key = f"oauth:tokens:{identity}"
362
+ cli_key = f"oauth:client_info:{identity}"
363
+ try:
364
+ keyring.delete_password(service, tok_key)
365
+ removed = True
366
+ except Exception:
367
+ pass
368
+ try:
369
+ keyring.delete_password(service, cli_key)
370
+ removed = True or removed
371
+ except Exception:
372
+ pass
373
+ if removed:
374
+ remove_identity_from_index(service, identity)
375
+ except Exception:
376
+ return False
377
+ return removed
378
+
379
+
380
+ def build_oauth_provider(server_config: MCPServerSettings) -> OAuthClientProvider | None:
381
+ """
382
+ Build an OAuthClientProvider for the given server config if applicable.
383
+
384
+ Returns None for unsupported transports, or when disabled via config.
385
+ """
386
+ # Only for SSE/HTTP transports
387
+ if server_config.transport not in ("sse", "http"):
388
+ return None
389
+
390
+ # Determine if OAuth should be enabled. Default to True if no auth block provided
391
+ enable_oauth = True
392
+ redirect_port = 3030
393
+ redirect_path = "/callback"
394
+ scope_value: str | None = None
395
+ persist_mode: str = "keyring"
396
+
397
+ if server_config.auth is not None:
398
+ try:
399
+ enable_oauth = getattr(server_config.auth, "oauth", True)
400
+ redirect_port = getattr(server_config.auth, "redirect_port", 3030)
401
+ redirect_path = getattr(server_config.auth, "redirect_path", "/callback")
402
+ scope_field = getattr(server_config.auth, "scope", None)
403
+ persist_mode = getattr(server_config.auth, "persist", "keyring")
404
+ if isinstance(scope_field, list):
405
+ scope_value = " ".join(scope_field)
406
+ elif isinstance(scope_field, str):
407
+ scope_value = scope_field
408
+ except Exception:
409
+ logger.debug("Malformed auth configuration; using defaults.")
410
+
411
+ if not enable_oauth:
412
+ return None
413
+
414
+ base_url = _derive_base_server_url(server_config.url)
415
+ if not base_url:
416
+ # No usable URL -> cannot build provider
417
+ return None
418
+
419
+ # Construct client metadata with minimal defaults
420
+ redirect_uri = f"http://localhost:{redirect_port}{redirect_path}"
421
+ metadata_kwargs: dict[str, Any] = {
422
+ "client_name": "fast-agent",
423
+ "redirect_uris": [AnyUrl(redirect_uri)],
424
+ "grant_types": ["authorization_code", "refresh_token"],
425
+ "response_types": ["code"],
426
+ }
427
+ if scope_value:
428
+ metadata_kwargs["scope"] = scope_value
429
+
430
+ client_metadata = OAuthClientMetadata.model_validate(metadata_kwargs)
431
+
432
+ # Local callback server handler
433
+ async def _redirect_handler(authorization_url: str) -> None:
434
+ await _print_authorization_link(authorization_url)
435
+
436
+ async def _callback_handler() -> tuple[str, str | None]:
437
+ # Try local HTTP capture first
438
+ try:
439
+ server = _CallbackServer(port=redirect_port, path=redirect_path)
440
+ server.start()
441
+ try:
442
+ code, state = server.wait(timeout_seconds=300)
443
+ return code, state
444
+ finally:
445
+ server.stop()
446
+ except Exception as e:
447
+ # Fallback to paste-URL flow
448
+ logger.info(f"OAuth local callback server unavailable, fallback to paste flow: {e}")
449
+ try:
450
+ import sys
451
+
452
+ print("Paste the full callback URL after authorization:", file=sys.stderr)
453
+ callback_url = input("Callback URL: ").strip()
454
+ except Exception as ee:
455
+ raise RuntimeError(f"Failed to read callback URL from user: {ee}")
456
+
457
+ params = parse_qs(urlparse(callback_url).query)
458
+ code = params.get("code", [None])[0]
459
+ state = params.get("state", [None])[0]
460
+ if not code:
461
+ raise RuntimeError("Callback URL missing authorization code")
462
+ return code, state
463
+
464
+ # Choose storage
465
+ storage: TokenStorage
466
+ if persist_mode == "keyring":
467
+ identity = compute_server_identity(server_config)
468
+ # Update index on write via storage methods; creation here doesn't modify index yet.
469
+ storage = KeyringTokenStorage(service_name="fast-agent-mcp", server_identity=identity)
470
+ else:
471
+ storage = InMemoryTokenStorage()
472
+
473
+ provider = OAuthClientProvider(
474
+ server_url=base_url,
475
+ client_metadata=client_metadata,
476
+ storage=storage,
477
+ redirect_handler=_redirect_handler,
478
+ callback_handler=_callback_handler,
479
+ )
480
+
481
+ return provider
@@ -41,5 +41,4 @@ mcp:
41
41
  command: "npx"
42
42
  args: ["-y", "@modelcontextprotocol/server-filesystem", "."]
43
43
  huggingface:
44
- transport: "http"
45
- url: "https://huggingface.co/mcp"
44
+ url: "https://huggingface.co/mcp?login"