Add ServerSessionT type var to Context (#271)

* Add ServerSessionT type var to Context

* Passing locally

* Try now
This commit is contained in:
Marcelo Trylesinski
2025-03-12 16:35:15 +01:00
committed by GitHub
parent 78fc5c12c0
commit e756315dea
5 changed files with 44 additions and 38 deletions

View File

@@ -1,5 +1,7 @@
"""FastMCP - A more ergonomic interface for MCP servers.""" """FastMCP - A more ergonomic interface for MCP servers."""
from __future__ import annotations as _annotations
import inspect import inspect
import json import json
import re import re
@@ -25,16 +27,10 @@ from mcp.server.fastmcp.tools import ToolManager
from mcp.server.fastmcp.utilities.logging import configure_logging, get_logger from mcp.server.fastmcp.utilities.logging import configure_logging, get_logger
from mcp.server.fastmcp.utilities.types import Image from mcp.server.fastmcp.utilities.types import Image
from mcp.server.lowlevel.helper_types import ReadResourceContents from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.server.lowlevel.server import ( from mcp.server.lowlevel.server import LifespanResultT
LifespanResultT, from mcp.server.lowlevel.server import Server as MCPServer
) from mcp.server.lowlevel.server import lifespan as default_lifespan
from mcp.server.lowlevel.server import ( from mcp.server.session import ServerSession, ServerSessionT
Server as MCPServer,
)
from mcp.server.lowlevel.server import (
lifespan as default_lifespan,
)
from mcp.server.session import ServerSession
from mcp.server.sse import SseServerTransport from mcp.server.sse import SseServerTransport
from mcp.server.stdio import stdio_server from mcp.server.stdio import stdio_server
from mcp.shared.context import LifespanContextT, RequestContext from mcp.shared.context import LifespanContextT, RequestContext
@@ -45,21 +41,11 @@ from mcp.types import (
ImageContent, ImageContent,
TextContent, TextContent,
) )
from mcp.types import ( from mcp.types import Prompt as MCPPrompt
Prompt as MCPPrompt, from mcp.types import PromptArgument as MCPPromptArgument
) from mcp.types import Resource as MCPResource
from mcp.types import ( from mcp.types import ResourceTemplate as MCPResourceTemplate
PromptArgument as MCPPromptArgument, from mcp.types import Tool as MCPTool
)
from mcp.types import (
Resource as MCPResource,
)
from mcp.types import (
ResourceTemplate as MCPResourceTemplate,
)
from mcp.types import (
Tool as MCPTool,
)
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -105,11 +91,11 @@ class Settings(BaseSettings, Generic[LifespanResultT]):
def lifespan_wrapper( def lifespan_wrapper(
app: "FastMCP", app: FastMCP,
lifespan: Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]], lifespan: Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]],
) -> Callable[[MCPServer], AbstractAsyncContextManager[object]]: ) -> Callable[[MCPServer[LifespanResultT]], AbstractAsyncContextManager[object]]:
@asynccontextmanager @asynccontextmanager
async def wrap(s: MCPServer) -> AsyncIterator[object]: async def wrap(s: MCPServer[LifespanResultT]) -> AsyncIterator[object]:
async with lifespan(app) as context: async with lifespan(app) as context:
yield context yield context
@@ -191,7 +177,7 @@ class FastMCP:
for info in tools for info in tools
] ]
def get_context(self) -> "Context": def get_context(self) -> "Context[ServerSession, object]":
""" """
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.
@@ -564,7 +550,7 @@ def _convert_to_content(
return [TextContent(type="text", text=result)] return [TextContent(type="text", text=result)]
class Context(BaseModel, Generic[LifespanContextT]): class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
"""Context object providing access to MCP capabilities. """Context object providing access to MCP capabilities.
This provides a cleaner interface to MCP's RequestContext functionality. This provides a cleaner interface to MCP's RequestContext functionality.
@@ -598,13 +584,13 @@ class Context(BaseModel, Generic[LifespanContextT]):
The context is optional - tools that don't need it can omit the parameter. The context is optional - tools that don't need it can omit the parameter.
""" """
_request_context: RequestContext[ServerSession, LifespanContextT] | None _request_context: RequestContext[ServerSessionT, LifespanContextT] | None
_fastmcp: FastMCP | None _fastmcp: FastMCP | None
def __init__( def __init__(
self, self,
*, *,
request_context: RequestContext[ServerSession, LifespanContextT] | None = None, request_context: RequestContext[ServerSessionT, LifespanContextT] | None = None,
fastmcp: FastMCP | None = None, fastmcp: FastMCP | None = None,
**kwargs: Any, **kwargs: Any,
): ):
@@ -620,7 +606,7 @@ class Context(BaseModel, Generic[LifespanContextT]):
return self._fastmcp return self._fastmcp
@property @property
def request_context(self) -> RequestContext[ServerSession, LifespanContextT]: def request_context(self) -> RequestContext[ServerSessionT, LifespanContextT]:
"""Access to the underlying request context.""" """Access to the underlying request context."""
if self._request_context is None: if self._request_context is None:
raise ValueError("Context is not available outside of a request") raise ValueError("Context is not available outside of a request")

View File

@@ -1,3 +1,5 @@
from __future__ import annotations as _annotations
import inspect import inspect
from typing import TYPE_CHECKING, Any, Callable from typing import TYPE_CHECKING, Any, Callable
@@ -9,6 +11,8 @@ from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metada
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.server.fastmcp.server import Context from mcp.server.fastmcp.server import Context
from mcp.server.session import ServerSessionT
from mcp.shared.context import LifespanContextT
class Tool(BaseModel): class Tool(BaseModel):
@@ -68,7 +72,11 @@ class Tool(BaseModel):
context_kwarg=context_kwarg, context_kwarg=context_kwarg,
) )
async def run(self, arguments: dict, context: "Context | None" = None) -> Any: async def run(
self,
arguments: dict[str, Any],
context: Context[ServerSessionT, LifespanContextT] | None = None,
) -> Any:
"""Run the tool with arguments.""" """Run the tool with arguments."""
try: try:
return await self.fn_metadata.call_fn_with_arg_validation( return await self.fn_metadata.call_fn_with_arg_validation(

View File

@@ -1,12 +1,16 @@
from __future__ import annotations as _annotations
from collections.abc import Callable from collections.abc import Callable
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from mcp.server.fastmcp.exceptions import ToolError from mcp.server.fastmcp.exceptions import ToolError
from mcp.server.fastmcp.tools.base import Tool from mcp.server.fastmcp.tools.base import Tool
from mcp.server.fastmcp.utilities.logging import get_logger from mcp.server.fastmcp.utilities.logging import get_logger
from mcp.shared.context import LifespanContextT
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.server.fastmcp.server import Context from mcp.server.fastmcp.server import Context
from mcp.server.session import ServerSessionT
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -43,7 +47,10 @@ class ToolManager:
return tool return tool
async def call_tool( async def call_tool(
self, name: str, arguments: dict, context: "Context | None" = None self,
name: str,
arguments: dict[str, Any],
context: Context[ServerSessionT, LifespanContextT] | None = None,
) -> Any: ) -> Any:
"""Call a tool by name with arguments.""" """Call a tool by name with arguments."""
tool = self.get_tool(name) tool = self.get_tool(name)

View File

@@ -64,6 +64,8 @@ notifications. It automatically manages the request context and handles incoming
messages from the client. messages from the client.
""" """
from __future__ import annotations as _annotations
import contextvars import contextvars
import logging import logging
import warnings import warnings
@@ -107,7 +109,7 @@ class NotificationOptions:
@asynccontextmanager @asynccontextmanager
async def lifespan(server: "Server") -> AsyncIterator[object]: async def lifespan(server: Server[LifespanResultT]) -> AsyncIterator[object]:
"""Default lifespan context manager that does nothing. """Default lifespan context manager that does nothing.
Args: Args:
@@ -126,7 +128,7 @@ class Server(Generic[LifespanResultT]):
version: str | None = None, version: str | None = None,
instructions: str | None = None, instructions: str | None = None,
lifespan: Callable[ lifespan: Callable[
["Server"], AbstractAsyncContextManager[LifespanResultT] [Server[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
] = lifespan, ] = lifespan,
): ):
self.name = name self.name = name

View File

@@ -38,7 +38,7 @@ be instantiated directly by users of the MCP framework.
""" """
from enum import Enum from enum import Enum
from typing import Any from typing import Any, TypeVar
import anyio import anyio
import anyio.lowlevel import anyio.lowlevel
@@ -59,6 +59,9 @@ class InitializationState(Enum):
Initialized = 3 Initialized = 3
ServerSessionT = TypeVar("ServerSessionT", bound="ServerSession")
class ServerSession( class ServerSession(
BaseSession[ BaseSession[
types.ServerRequest, types.ServerRequest,