mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 21:44:18 +02:00
Merge branch 'main' into switch-kvstore
This commit is contained in:
commit
93c41e815c
11 changed files with 388 additions and 28 deletions
|
|
@ -41,6 +41,8 @@ FastMCP supports [MCP proxying](/servers/proxy), which allows you to mirror a lo
|
|||
|
||||
You can also create proxies from configuration dictionaries that follow the MCPConfig schema, which is useful for quickly connecting to one or more remote servers. See the [Proxy Servers documentation](/servers/proxy#configuration-based-proxies) for details on configuration-based proxying. Note that MCPConfig follows an emerging standard and its format may evolve over time.
|
||||
|
||||
Prefixing rules for tools, prompts, resources, and templates are identical across importing, mounting, and proxies.
|
||||
|
||||
## Importing (Static Composition)
|
||||
|
||||
The `import_server()` method copies all components (tools, resources, templates, prompts) from one `FastMCP` instance (the *subserver*) into another (the *main server*). An optional `prefix` can be provided to avoid naming conflicts. If no prefix is provided, components are imported without modification. When multiple servers are imported with the same prefix (or no prefix), the most recently imported server's components take precedence.
|
||||
|
|
|
|||
|
|
@ -89,11 +89,12 @@ Note that the MCP SDK may perform additional operations like listing tools for c
|
|||
This hierarchy allows you to target your middleware logic with the right level of specificity. Use `on_message` for broad concerns like logging, `on_request` for authentication, and `on_call_tool` for tool-specific logic like performance monitoring.
|
||||
|
||||
### Available Hooks
|
||||
<VersionBadge version="2.9.0" />
|
||||
|
||||
- `on_message`: Called for all MCP messages (requests and notifications)
|
||||
- `on_request`: Called specifically for MCP requests (that expect responses)
|
||||
- `on_notification`: Called specifically for MCP notifications (fire-and-forget)
|
||||
- `on_initialize`: Called when a client connects and initializes the session (returns `None`)
|
||||
|
||||
- `on_call_tool`: Called when tools are being executed
|
||||
- `on_read_resource`: Called when resources are being read
|
||||
- `on_get_prompt`: Called when prompts are being retrieved
|
||||
|
|
@ -101,7 +102,8 @@ This hierarchy allows you to target your middleware logic with the right level o
|
|||
- `on_list_resources`: Called when listing available resources
|
||||
- `on_list_resource_templates`: Called when listing resource templates
|
||||
- `on_list_prompts`: Called when listing available prompts
|
||||
|
||||
<VersionBadge version="2.13.0" />
|
||||
- `on_initialize`: Called when a client connects and initializes the session (returns `None`)
|
||||
<Note>
|
||||
The `on_initialize` hook receives the client's initialization request but **returns `None`** rather than a result. The initialization response is handled internally by the MCP protocol and cannot be modified by middleware. This hook is useful for client detection, logging connections, or initializing session state, but not for modifying the initialization handshake itself.
|
||||
</Note>
|
||||
|
|
|
|||
|
|
@ -245,11 +245,29 @@ config = {
|
|||
# Create a unified proxy to multiple servers
|
||||
composite_proxy = FastMCP.as_proxy(config, name="Composite Proxy")
|
||||
|
||||
# Tools and resources are accessible with prefixes:
|
||||
# - weather_get_forecast, calendar_add_event
|
||||
# - weather://weather/icons/sunny, calendar://calendar/events/today
|
||||
# Tools, resources, prompts, and templates are accessible with prefixes:
|
||||
# - Tools: weather_get_forecast, calendar_add_event
|
||||
# - Prompts: weather_daily_summary, calendar_quick_add
|
||||
# - Resources: weather://weather/icons/sunny, calendar://calendar/events/today
|
||||
# - Templates: weather://weather/locations/{id}, calendar://calendar/events/{date}
|
||||
```
|
||||
|
||||
## Component Prefixing
|
||||
|
||||
When proxying one or more servers, component names are prefixed the same way as with mounting and importing:
|
||||
|
||||
- Tools: `{prefix}_{tool_name}`
|
||||
- Prompts: `{prefix}_{prompt_name}`
|
||||
- Resources: `protocol://{prefix}/path/to/resource` (default path format)
|
||||
- Resource templates: `protocol://{prefix}/...` and template names are also prefixed
|
||||
|
||||
These rules apply uniformly whether you:
|
||||
- Mount a proxy on another server
|
||||
- Create a multi-server proxy from an `MCPConfig`
|
||||
- Use `FastMCP.as_proxy()` directly
|
||||
|
||||
For resource URI prefix formats (path vs legacy protocol style) and configuration options, see Server Composition → Resource Prefix Formats.
|
||||
|
||||
## Mirrored Components
|
||||
|
||||
<VersionBadge version="2.10.5" />
|
||||
|
|
@ -332,4 +350,3 @@ def custom_client_factory():
|
|||
|
||||
proxy = FastMCPProxy(client_factory=custom_client_factory)
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from __future__ import annotations as _annotations
|
|||
|
||||
import inspect
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -62,7 +61,7 @@ class PromptArgument(FastMCPBaseModel):
|
|||
)
|
||||
|
||||
|
||||
class Prompt(FastMCPComponent, ABC):
|
||||
class Prompt(FastMCPComponent):
|
||||
"""A prompt template that can be rendered with parameters."""
|
||||
|
||||
arguments: list[PromptArgument] | None = Field(
|
||||
|
|
@ -139,13 +138,16 @@ class Prompt(FastMCPComponent, ABC):
|
|||
meta=meta,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
async def render(
|
||||
self,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
) -> list[PromptMessage]:
|
||||
"""Render the prompt with arguments."""
|
||||
raise NotImplementedError("Prompt.render() must be implemented by subclasses")
|
||||
"""Render the prompt with arguments.
|
||||
|
||||
This method is not implemented in the base Prompt class and must be
|
||||
implemented by subclasses.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement render()")
|
||||
|
||||
|
||||
class FunctionPrompt(Prompt):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Annotated, Any
|
||||
|
|
@ -31,7 +30,7 @@ if TYPE_CHECKING:
|
|||
pass
|
||||
|
||||
|
||||
class Resource(FastMCPComponent, abc.ABC):
|
||||
class Resource(FastMCPComponent):
|
||||
"""Base class for all resources."""
|
||||
|
||||
model_config = ConfigDict(validate_default=True)
|
||||
|
|
@ -111,10 +110,13 @@ class Resource(FastMCPComponent, abc.ABC):
|
|||
raise ValueError("Either name or uri must be provided")
|
||||
return self
|
||||
|
||||
@abc.abstractmethod
|
||||
async def read(self) -> str | bytes:
|
||||
"""Read the resource content."""
|
||||
pass
|
||||
"""Read the resource content.
|
||||
|
||||
This method is not implemented in the base Resource class and must be
|
||||
implemented by subclasses.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement read()")
|
||||
|
||||
def to_mcp_resource(
|
||||
self,
|
||||
|
|
|
|||
171
src/fastmcp/server/auth/providers/supabase.py
Normal file
171
src/fastmcp/server/auth/providers/supabase.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
"""Supabase authentication provider for FastMCP.
|
||||
|
||||
This module provides SupabaseProvider - a complete authentication solution that integrates
|
||||
with Supabase Auth's JWT verification, supporting Dynamic Client Registration (DCR)
|
||||
for seamless MCP client authentication.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
from pydantic import AnyHttpUrl, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.routing import Route
|
||||
|
||||
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
|
||||
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
||||
from fastmcp.utilities.auth import parse_scopes
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import NotSet, NotSetT
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SupabaseProviderSettings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="FASTMCP_SERVER_AUTH_SUPABASE_",
|
||||
env_file=".env",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
project_url: AnyHttpUrl
|
||||
base_url: AnyHttpUrl
|
||||
required_scopes: list[str] | None = None
|
||||
|
||||
@field_validator("required_scopes", mode="before")
|
||||
@classmethod
|
||||
def _parse_scopes(cls, v):
|
||||
return parse_scopes(v)
|
||||
|
||||
|
||||
class SupabaseProvider(RemoteAuthProvider):
|
||||
"""Supabase metadata provider for DCR (Dynamic Client Registration).
|
||||
|
||||
This provider implements Supabase Auth integration using metadata forwarding.
|
||||
This approach allows Supabase to handle the OAuth flow directly while FastMCP acts
|
||||
as a resource server, verifying JWTs issued by Supabase Auth.
|
||||
|
||||
IMPORTANT SETUP REQUIREMENTS:
|
||||
|
||||
1. Supabase Project Setup:
|
||||
- Create a Supabase project at https://supabase.com
|
||||
- Note your project URL (e.g., "https://abc123.supabase.co")
|
||||
- For projects created after May 1st, 2025, asymmetric RS256 keys are used by default
|
||||
- For older projects, consider migrating to asymmetric keys for better security
|
||||
|
||||
2. JWT Verification:
|
||||
- FastMCP verifies JWTs using the JWKS endpoint at {project_url}/auth/v1/.well-known/jwks.json
|
||||
- JWTs are issued by {project_url}/auth/v1
|
||||
- Tokens are cached for up to 10 minutes by Supabase's edge servers
|
||||
|
||||
For detailed setup instructions, see:
|
||||
https://supabase.com/docs/guides/auth/jwts
|
||||
|
||||
Example:
|
||||
```python
|
||||
from fastmcp.server.auth.providers.supabase import SupabaseProvider
|
||||
|
||||
# Create Supabase metadata provider (JWT verifier created automatically)
|
||||
supabase_auth = SupabaseProvider(
|
||||
project_url="https://abc123.supabase.co",
|
||||
base_url="https://your-fastmcp-server.com",
|
||||
)
|
||||
|
||||
# Use with FastMCP
|
||||
mcp = FastMCP("My App", auth=supabase_auth)
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
project_url: AnyHttpUrl | str | NotSetT = NotSet,
|
||||
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
||||
required_scopes: list[str] | None | NotSetT = NotSet,
|
||||
token_verifier: TokenVerifier | None = None,
|
||||
):
|
||||
"""Initialize Supabase metadata provider.
|
||||
|
||||
Args:
|
||||
project_url: Your Supabase project URL (e.g., "https://abc123.supabase.co")
|
||||
base_url: Public URL of this FastMCP server
|
||||
required_scopes: Optional list of scopes to require for all requests
|
||||
token_verifier: Optional token verifier. If None, creates JWT verifier for Supabase
|
||||
"""
|
||||
settings = SupabaseProviderSettings.model_validate(
|
||||
{
|
||||
k: v
|
||||
for k, v in {
|
||||
"project_url": project_url,
|
||||
"base_url": base_url,
|
||||
"required_scopes": required_scopes,
|
||||
}.items()
|
||||
if v is not NotSet
|
||||
}
|
||||
)
|
||||
|
||||
self.project_url = str(settings.project_url).rstrip("/")
|
||||
self.base_url = str(settings.base_url).rstrip("/")
|
||||
|
||||
# Create default JWT verifier if none provided
|
||||
if token_verifier is None:
|
||||
token_verifier = JWTVerifier(
|
||||
jwks_uri=f"{self.project_url}/auth/v1/.well-known/jwks.json",
|
||||
issuer=f"{self.project_url}/auth/v1",
|
||||
algorithm="ES256", # Supabase uses ES256 for asymmetric keys
|
||||
required_scopes=settings.required_scopes,
|
||||
)
|
||||
|
||||
# Initialize RemoteAuthProvider with Supabase as the authorization server
|
||||
super().__init__(
|
||||
token_verifier=token_verifier,
|
||||
authorization_servers=[AnyHttpUrl(f"{self.project_url}/auth/v1")],
|
||||
base_url=self.base_url,
|
||||
)
|
||||
|
||||
def get_routes(
|
||||
self,
|
||||
mcp_path: str | None = None,
|
||||
) -> list[Route]:
|
||||
"""Get OAuth routes including Supabase authorization server metadata forwarding.
|
||||
|
||||
This returns the standard protected resource routes plus an authorization server
|
||||
metadata endpoint that forwards Supabase's OAuth metadata to clients.
|
||||
|
||||
Args:
|
||||
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
|
||||
This is used to advertise the resource URL in metadata.
|
||||
"""
|
||||
# Get the standard protected resource routes from RemoteAuthProvider
|
||||
routes = super().get_routes(mcp_path)
|
||||
|
||||
async def oauth_authorization_server_metadata(request):
|
||||
"""Forward Supabase OAuth authorization server metadata with FastMCP customizations."""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.project_url}/auth/v1/.well-known/oauth-authorization-server"
|
||||
)
|
||||
response.raise_for_status()
|
||||
metadata = response.json()
|
||||
return JSONResponse(metadata)
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
{
|
||||
"error": "server_error",
|
||||
"error_description": f"Failed to fetch Supabase metadata: {e}",
|
||||
},
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
# Add Supabase authorization server metadata forwarding
|
||||
routes.append(
|
||||
Route(
|
||||
"/.well-known/oauth-authorization-server",
|
||||
endpoint=oauth_authorization_server_metadata,
|
||||
methods=["GET"],
|
||||
)
|
||||
)
|
||||
|
||||
return routes
|
||||
|
|
@ -197,7 +197,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
self._mcp_server = LowLevelServer[LifespanResultT](
|
||||
fastmcp=self,
|
||||
name=name or self.generate_name(),
|
||||
version=version,
|
||||
version=version or fastmcp.__version__,
|
||||
instructions=instructions,
|
||||
lifespan=_lifespan_wrapper(self, lifespan),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from mcp import McpError
|
|||
from mcp.client.auth import OAuthClientProvider
|
||||
from pydantic import AnyUrl
|
||||
|
||||
import fastmcp
|
||||
from fastmcp.client import Client
|
||||
from fastmcp.client.auth.bearer import BearerAuth
|
||||
from fastmcp.client.transports import (
|
||||
|
|
@ -435,11 +436,8 @@ async def test_server_info_custom_version():
|
|||
async with client:
|
||||
result = client.initialize_result
|
||||
assert result.serverInfo.name == "DefaultVersionServer"
|
||||
# Should fall back to MCP library version
|
||||
assert result.serverInfo.version is not None
|
||||
assert (
|
||||
result.serverInfo.version != "1.2.3"
|
||||
) # Should be different from custom version
|
||||
# Should fall back to FastMCP version
|
||||
assert result.serverInfo.version == fastmcp.__version__
|
||||
|
||||
|
||||
async def test_client_nested_context_manager(fastmcp_server):
|
||||
|
|
|
|||
|
|
@ -85,14 +85,15 @@ class TestResourceValidation:
|
|||
)
|
||||
assert resource.mime_type == "application/json"
|
||||
|
||||
async def test_resource_read_abstract(self):
|
||||
"""Test that Resource.read() is abstract."""
|
||||
async def test_resource_read_not_implemented(self):
|
||||
"""Test that Resource.read() raises NotImplementedError."""
|
||||
|
||||
class ConcreteResource(Resource):
|
||||
pass
|
||||
|
||||
with pytest.raises(TypeError, match="abstract method"):
|
||||
ConcreteResource(uri=AnyUrl("test://test"), name="test") # type: ignore
|
||||
resource = ConcreteResource(uri=AnyUrl("test://test"), name="test") # type: ignore
|
||||
with pytest.raises(NotImplementedError, match="Subclasses must implement read"):
|
||||
await resource.read()
|
||||
|
||||
def test_resource_meta_parameter(self):
|
||||
"""Test that meta parameter is properly handled."""
|
||||
|
|
|
|||
165
tests/server/auth/providers/test_supabase.py
Normal file
165
tests/server/auth/providers/test_supabase.py
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
"""Tests for Supabase Auth provider."""
|
||||
|
||||
import os
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.client.transports import StreamableHttpTransport
|
||||
from fastmcp.server.auth.providers.supabase import SupabaseProvider
|
||||
from fastmcp.utilities.tests import HeadlessOAuth, run_server_in_process
|
||||
|
||||
|
||||
class TestSupabaseProvider:
|
||||
"""Test Supabase Auth provider functionality."""
|
||||
|
||||
def test_init_with_explicit_params(self):
|
||||
"""Test SupabaseProvider initialization with explicit parameters."""
|
||||
provider = SupabaseProvider(
|
||||
project_url="https://abc123.supabase.co",
|
||||
base_url="https://myserver.com",
|
||||
)
|
||||
|
||||
assert provider.project_url == "https://abc123.supabase.co"
|
||||
assert str(provider.base_url) == "https://myserver.com/"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scopes_env",
|
||||
[
|
||||
"openid,email",
|
||||
'["openid", "email"]',
|
||||
],
|
||||
)
|
||||
def test_init_with_env_vars(self, scopes_env):
|
||||
"""Test SupabaseProvider initialization from environment variables."""
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"FASTMCP_SERVER_AUTH_SUPABASE_PROJECT_URL": "https://env123.supabase.co",
|
||||
"FASTMCP_SERVER_AUTH_SUPABASE_BASE_URL": "https://envserver.com",
|
||||
},
|
||||
):
|
||||
provider = SupabaseProvider()
|
||||
|
||||
assert provider.project_url == "https://env123.supabase.co"
|
||||
assert str(provider.base_url) == "https://envserver.com/"
|
||||
|
||||
def test_environment_variable_loading(self):
|
||||
"""Test that environment variables are loaded correctly."""
|
||||
provider = SupabaseProvider(
|
||||
project_url="https://env123.supabase.co",
|
||||
base_url="http://env-server.com",
|
||||
)
|
||||
|
||||
assert provider.project_url == "https://env123.supabase.co"
|
||||
assert str(provider.base_url) == "http://env-server.com/"
|
||||
|
||||
def test_project_url_normalization(self):
|
||||
"""Test that project_url handles trailing slashes correctly."""
|
||||
# Without trailing slash
|
||||
provider1 = SupabaseProvider(
|
||||
project_url="https://abc123.supabase.co",
|
||||
base_url="https://myserver.com",
|
||||
)
|
||||
assert provider1.project_url == "https://abc123.supabase.co"
|
||||
|
||||
# With trailing slash - should be stripped
|
||||
provider2 = SupabaseProvider(
|
||||
project_url="https://abc123.supabase.co/",
|
||||
base_url="https://myserver.com",
|
||||
)
|
||||
assert provider2.project_url == "https://abc123.supabase.co"
|
||||
|
||||
def test_jwt_verifier_configured_correctly(self):
|
||||
"""Test that JWT verifier is configured correctly."""
|
||||
provider = SupabaseProvider(
|
||||
project_url="https://abc123.supabase.co",
|
||||
base_url="https://myserver.com",
|
||||
)
|
||||
|
||||
# Check that JWT verifier uses the correct endpoints
|
||||
assert (
|
||||
provider.token_verifier.jwks_uri # type: ignore[attr-defined]
|
||||
== "https://abc123.supabase.co/auth/v1/.well-known/jwks.json"
|
||||
)
|
||||
assert (
|
||||
provider.token_verifier.issuer == "https://abc123.supabase.co/auth/v1" # type: ignore[attr-defined]
|
||||
)
|
||||
assert provider.token_verifier.algorithm == "ES256" # type: ignore[attr-defined]
|
||||
|
||||
def test_jwt_verifier_with_required_scopes(self):
|
||||
"""Test that JWT verifier respects required_scopes."""
|
||||
provider = SupabaseProvider(
|
||||
project_url="https://abc123.supabase.co",
|
||||
base_url="https://myserver.com",
|
||||
required_scopes=["openid", "email"],
|
||||
)
|
||||
|
||||
assert provider.token_verifier.required_scopes == ["openid", "email"] # type: ignore[attr-defined]
|
||||
|
||||
def test_authorization_servers_configured(self):
|
||||
"""Test that authorization servers list is configured correctly."""
|
||||
provider = SupabaseProvider(
|
||||
project_url="https://abc123.supabase.co",
|
||||
base_url="https://myserver.com",
|
||||
)
|
||||
|
||||
assert len(provider.authorization_servers) == 1
|
||||
assert (
|
||||
str(provider.authorization_servers[0])
|
||||
== "https://abc123.supabase.co/auth/v1"
|
||||
)
|
||||
|
||||
|
||||
def run_mcp_server(host: str, port: int) -> None:
|
||||
mcp = FastMCP(
|
||||
auth=SupabaseProvider(
|
||||
project_url="https://test123.supabase.co",
|
||||
base_url="http://localhost:4321",
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
|
||||
mcp.run(host=host, port=port, transport="http")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mcp_server_url() -> Generator[str]:
|
||||
with run_server_in_process(run_mcp_server) as url:
|
||||
yield f"{url}/mcp"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client_with_headless_oauth(
|
||||
mcp_server_url: str,
|
||||
) -> Generator[Client, None, None]:
|
||||
"""Client with headless OAuth that bypasses browser interaction."""
|
||||
client = Client(
|
||||
transport=StreamableHttpTransport(mcp_server_url),
|
||||
auth=HeadlessOAuth(mcp_url=mcp_server_url),
|
||||
)
|
||||
yield client
|
||||
|
||||
|
||||
class TestSupabaseProviderIntegration:
|
||||
async def test_unauthorized_access(self, mcp_server_url: str):
|
||||
with pytest.raises(httpx.HTTPStatusError) as exc_info:
|
||||
async with Client(mcp_server_url) as client:
|
||||
tools = await client.list_tools() # noqa: F841
|
||||
|
||||
assert isinstance(exc_info.value, httpx.HTTPStatusError)
|
||||
assert exc_info.value.response.status_code == 401
|
||||
assert "tools" not in locals()
|
||||
|
||||
# async def test_authorized_access(self, client_with_headless_oauth: Client):
|
||||
# async with client_with_headless_oauth:
|
||||
# tools = await client_with_headless_oauth.list_tools()
|
||||
# assert tools is not None
|
||||
# assert len(tools) > 0
|
||||
# assert "add" in tools
|
||||
|
|
@ -90,7 +90,7 @@ class TestGetFastMCPInfo:
|
|||
assert info.fastmcp_version == fastmcp.__version__
|
||||
assert info.mcp_version == importlib.metadata.version("mcp")
|
||||
assert info.server_generation == 2 # v2 server
|
||||
assert info.version is None
|
||||
assert info.version == fastmcp.__version__
|
||||
assert info.tools == []
|
||||
assert info.prompts == []
|
||||
assert info.resources == []
|
||||
|
|
@ -405,7 +405,7 @@ class TestFastMCP1xCompatibility:
|
|||
assert info1x.server_generation == 1 # v1
|
||||
assert info2x.server_generation == 2 # v2
|
||||
assert info1x.version is None
|
||||
assert info2x.version is None
|
||||
assert info2x.version == fastmcp.__version__
|
||||
|
||||
# No templates added in these tests
|
||||
assert len(info1x.templates) == 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue