Unpack settings in FastMCP (#1198)
Some checks failed
Main branch checks / checks (push) Has been cancelled
Some checks failed
Main branch checks / checks (push) Has been cancelled
This commit is contained in:
parent
b34e7207ad
commit
959d4e39ae
3 changed files with 75 additions and 51 deletions
|
|
@ -4,16 +4,13 @@ from __future__ import annotations as _annotations
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence
|
from collections.abc import AsyncIterator, Awaitable, Callable, Collection, Iterable, Sequence
|
||||||
from contextlib import (
|
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
||||||
AbstractAsyncContextManager,
|
|
||||||
asynccontextmanager,
|
|
||||||
)
|
|
||||||
from typing import Any, Generic, Literal
|
from typing import Any, Generic, Literal
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
import pydantic_core
|
import pydantic_core
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel
|
||||||
from pydantic.networks import AnyUrl
|
from pydantic.networks import AnyUrl
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from starlette.applications import Starlette
|
from starlette.applications import Starlette
|
||||||
|
|
@ -25,10 +22,7 @@ from starlette.routing import Mount, Route
|
||||||
from starlette.types import Receive, Scope, Send
|
from starlette.types import Receive, Scope, Send
|
||||||
|
|
||||||
from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
|
from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
|
||||||
from mcp.server.auth.middleware.bearer_auth import (
|
from mcp.server.auth.middleware.bearer_auth import BearerAuthBackend, RequireAuthMiddleware
|
||||||
BearerAuthBackend,
|
|
||||||
RequireAuthMiddleware,
|
|
||||||
)
|
|
||||||
from mcp.server.auth.provider import OAuthAuthorizationServerProvider, ProviderTokenVerifier, TokenVerifier
|
from mcp.server.auth.provider import OAuthAuthorizationServerProvider, ProviderTokenVerifier, TokenVerifier
|
||||||
from mcp.server.auth.settings import AuthSettings
|
from mcp.server.auth.settings import AuthSettings
|
||||||
from mcp.server.elicitation import ElicitationResult, ElicitSchemaModelT, elicit_with_validation
|
from mcp.server.elicitation import ElicitationResult, ElicitSchemaModelT, elicit_with_validation
|
||||||
|
|
@ -48,12 +42,7 @@ from mcp.server.streamable_http import EventStore
|
||||||
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
||||||
from mcp.server.transport_security import TransportSecuritySettings
|
from mcp.server.transport_security import TransportSecuritySettings
|
||||||
from mcp.shared.context import LifespanContextT, RequestContext, RequestT
|
from mcp.shared.context import LifespanContextT, RequestContext, RequestT
|
||||||
from mcp.types import (
|
from mcp.types import AnyFunction, ContentBlock, GetPromptResult, ToolAnnotations
|
||||||
AnyFunction,
|
|
||||||
ContentBlock,
|
|
||||||
GetPromptResult,
|
|
||||||
ToolAnnotations,
|
|
||||||
)
|
|
||||||
from mcp.types import Prompt as MCPPrompt
|
from mcp.types import Prompt as MCPPrompt
|
||||||
from mcp.types import PromptArgument as MCPPromptArgument
|
from mcp.types import PromptArgument as MCPPromptArgument
|
||||||
from mcp.types import Resource as MCPResource
|
from mcp.types import Resource as MCPResource
|
||||||
|
|
@ -79,58 +68,57 @@ class Settings(BaseSettings, Generic[LifespanResultT]):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Server settings
|
# Server settings
|
||||||
debug: bool = False
|
debug: bool
|
||||||
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
||||||
|
|
||||||
# HTTP settings
|
# HTTP settings
|
||||||
host: str = "127.0.0.1"
|
host: str
|
||||||
port: int = 8000
|
port: int
|
||||||
mount_path: str = "/" # Mount path (e.g. "/github", defaults to root path)
|
mount_path: str
|
||||||
sse_path: str = "/sse"
|
sse_path: str
|
||||||
message_path: str = "/messages/"
|
message_path: str
|
||||||
streamable_http_path: str = "/mcp"
|
streamable_http_path: str
|
||||||
|
|
||||||
# StreamableHTTP settings
|
# StreamableHTTP settings
|
||||||
json_response: bool = False
|
json_response: bool
|
||||||
stateless_http: bool = False # If True, uses true stateless mode (new transport per request)
|
stateless_http: bool
|
||||||
|
"""Define if the server should create a new transport per request."""
|
||||||
|
|
||||||
# resource settings
|
# resource settings
|
||||||
warn_on_duplicate_resources: bool = True
|
warn_on_duplicate_resources: bool
|
||||||
|
|
||||||
# tool settings
|
# tool settings
|
||||||
warn_on_duplicate_tools: bool = True
|
warn_on_duplicate_tools: bool
|
||||||
|
|
||||||
# prompt settings
|
# prompt settings
|
||||||
warn_on_duplicate_prompts: bool = True
|
warn_on_duplicate_prompts: bool
|
||||||
|
|
||||||
dependencies: list[str] = Field(
|
# TODO(Marcelo): Investigate if this is used. If it is, it's probably a good idea to remove it.
|
||||||
default_factory=list,
|
dependencies: list[str]
|
||||||
description="List of dependencies to install in the server environment",
|
"""A list of dependencies to install in the server environment."""
|
||||||
)
|
|
||||||
|
|
||||||
lifespan: Callable[[FastMCP], AbstractAsyncContextManager[LifespanResultT]] | None = Field(
|
lifespan: Callable[[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]] | None
|
||||||
None, description="Lifespan context manager"
|
"""A async context manager that will be called when the server is started."""
|
||||||
)
|
|
||||||
|
|
||||||
auth: AuthSettings | None = None
|
auth: AuthSettings | None
|
||||||
|
|
||||||
# Transport security settings (DNS rebinding protection)
|
# Transport security settings (DNS rebinding protection)
|
||||||
transport_security: TransportSecuritySettings | None = None
|
transport_security: TransportSecuritySettings | None
|
||||||
|
|
||||||
|
|
||||||
def lifespan_wrapper(
|
def lifespan_wrapper(
|
||||||
app: FastMCP,
|
app: FastMCP[LifespanResultT],
|
||||||
lifespan: Callable[[FastMCP], AbstractAsyncContextManager[LifespanResultT]],
|
lifespan: Callable[[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]],
|
||||||
) -> Callable[[MCPServer[LifespanResultT, Request]], AbstractAsyncContextManager[object]]:
|
) -> Callable[[MCPServer[LifespanResultT, Request]], AbstractAsyncContextManager[LifespanResultT]]:
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def wrap(s: MCPServer[LifespanResultT, Request]) -> AsyncIterator[object]:
|
async def wrap(_: MCPServer[LifespanResultT, Request]) -> AsyncIterator[LifespanResultT]:
|
||||||
async with lifespan(app) as context:
|
async with lifespan(app) as context:
|
||||||
yield context
|
yield context
|
||||||
|
|
||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
class FastMCP:
|
class FastMCP(Generic[LifespanResultT]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
|
|
@ -140,14 +128,50 @@ class FastMCP:
|
||||||
event_store: EventStore | None = None,
|
event_store: EventStore | None = None,
|
||||||
*,
|
*,
|
||||||
tools: list[Tool] | None = None,
|
tools: list[Tool] | None = None,
|
||||||
**settings: Any,
|
debug: bool = False,
|
||||||
|
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
|
||||||
|
host: str = "127.0.0.1",
|
||||||
|
port: int = 8000,
|
||||||
|
mount_path: str = "/",
|
||||||
|
sse_path: str = "/sse",
|
||||||
|
message_path: str = "/messages/",
|
||||||
|
streamable_http_path: str = "/mcp",
|
||||||
|
json_response: bool = False,
|
||||||
|
stateless_http: bool = False,
|
||||||
|
warn_on_duplicate_resources: bool = True,
|
||||||
|
warn_on_duplicate_tools: bool = True,
|
||||||
|
warn_on_duplicate_prompts: bool = True,
|
||||||
|
dependencies: Collection[str] = (),
|
||||||
|
lifespan: Callable[[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]] | None = None,
|
||||||
|
auth: AuthSettings | None = None,
|
||||||
|
transport_security: TransportSecuritySettings | None = None,
|
||||||
):
|
):
|
||||||
self.settings = Settings(**settings)
|
self.settings = Settings(
|
||||||
|
debug=debug,
|
||||||
|
log_level=log_level,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
mount_path=mount_path,
|
||||||
|
sse_path=sse_path,
|
||||||
|
message_path=message_path,
|
||||||
|
streamable_http_path=streamable_http_path,
|
||||||
|
json_response=json_response,
|
||||||
|
stateless_http=stateless_http,
|
||||||
|
warn_on_duplicate_resources=warn_on_duplicate_resources,
|
||||||
|
warn_on_duplicate_tools=warn_on_duplicate_tools,
|
||||||
|
warn_on_duplicate_prompts=warn_on_duplicate_prompts,
|
||||||
|
dependencies=list(dependencies),
|
||||||
|
lifespan=lifespan,
|
||||||
|
auth=auth,
|
||||||
|
transport_security=transport_security,
|
||||||
|
)
|
||||||
|
|
||||||
self._mcp_server = MCPServer(
|
self._mcp_server = MCPServer(
|
||||||
name=name or "FastMCP",
|
name=name or "FastMCP",
|
||||||
instructions=instructions,
|
instructions=instructions,
|
||||||
lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan),
|
# TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an FastMCP and Server.
|
||||||
|
# We need to create a Lifespan type that is a generic on the server type, like Starlette does.
|
||||||
|
lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore
|
||||||
)
|
)
|
||||||
self._tool_manager = ToolManager(tools=tools, warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools)
|
self._tool_manager = ToolManager(tools=tools, warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools)
|
||||||
self._resource_manager = ResourceManager(warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources)
|
self._resource_manager = ResourceManager(warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources)
|
||||||
|
|
@ -257,7 +281,7 @@ class FastMCP:
|
||||||
for info in tools
|
for info in tools
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_context(self) -> Context[ServerSession, object, Request]:
|
def get_context(self) -> Context[ServerSession, LifespanResultT, Request]:
|
||||||
"""
|
"""
|
||||||
Returns a Context object. Note that the context will only be valid
|
Returns a Context object. Note that the context will only be valid
|
||||||
during a request; outside a request, most methods will error.
|
during a request; outside a request, most methods will error.
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ from mcp.shared.session import RequestResponder
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
LifespanResultT = TypeVar("LifespanResultT")
|
LifespanResultT = TypeVar("LifespanResultT", default=Any)
|
||||||
RequestT = TypeVar("RequestT", default=Any)
|
RequestT = TypeVar("RequestT", default=Any)
|
||||||
|
|
||||||
# type aliases for tool call results
|
# type aliases for tool call results
|
||||||
|
|
@ -118,7 +118,7 @@ class NotificationOptions:
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(server: Server[LifespanResultT, RequestT]) -> AsyncIterator[object]:
|
async def lifespan(_: Server[LifespanResultT, RequestT]) -> AsyncIterator[dict[str, Any]]:
|
||||||
"""Default lifespan context manager that does nothing.
|
"""Default lifespan context manager that does nothing.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ async def test_bidirectional_progress_notifications():
|
||||||
serv_sesh = server_session
|
serv_sesh = server_session
|
||||||
async for message in server_session.incoming_messages:
|
async for message in server_session.incoming_messages:
|
||||||
try:
|
try:
|
||||||
await server._handle_message(message, server_session, ())
|
await server._handle_message(message, server_session, {})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
@ -252,7 +252,7 @@ async def test_progress_context_manager():
|
||||||
) as server_session:
|
) as server_session:
|
||||||
async for message in server_session.incoming_messages:
|
async for message in server_session.incoming_messages:
|
||||||
try:
|
try:
|
||||||
await server._handle_message(message, server_session, ())
|
await server._handle_message(message, server_session, {})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue