refactor: remove request_ctx ContextVar, thread Context explicitly (#2203)

Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
This commit is contained in:
Max Isbey 2026-03-04 13:23:02 +00:00 • committed by GitHub
commit cc22bf5464
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 484 additions and 413 deletions

View file

@ -288,6 +288,37 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app(json_response=Tru
**Note:** DNS rebinding protection is automatically enabled when `host` is `127.0.0.1`, `localhost`, or `::1`. This now happens in `sse_app()` and `streamable_http_app()` instead of the constructor. **Note:** DNS rebinding protection is automatically enabled when `host` is `127.0.0.1`, `localhost`, or `::1`. This now happens in `sse_app()` and `streamable_http_app()` instead of the constructor.
### `MCPServer.get_context()` removed
`MCPServer.get_context()` has been removed. Context is now injected by the framework and passed explicitly — there is no ambient ContextVar to read from.
**If you were calling `get_context()` from inside a tool/resource/prompt:** use the `ctx: Context` parameter injection instead.
**Before (v1):**
```python
@mcp.tool()
async def my_tool(x: int) -> str:
ctx = mcp.get_context()
await ctx.info("Processing...")
return str(x)
```
**After (v2):**
```python
@mcp.tool()
async def my_tool(x: int, ctx: Context) -> str:
await ctx.info("Processing...")
return str(x)
```
### `MCPServer.call_tool()`, `read_resource()`, `get_prompt()` now accept a `context` parameter
`MCPServer.call_tool()`, `MCPServer.read_resource()`, and `MCPServer.get_prompt()` now accept an optional `context: Context | None = None` parameter. The framework passes this automatically during normal request handling. If you call these methods directly and omit `context`, a Context with no active request is constructed for you — tools that don't use `ctx` work normally, but any attempt to use `ctx.session`, `ctx.request_id`, etc. will raise.
The internal layers (`ToolManager.call_tool`, `Tool.run`, `Prompt.render`, `ResourceTemplate.create_resource`, etc.) now require `context` as a positional argument.
### Replace `RootModel` by union types with `TypeAdapter` validation ### Replace `RootModel` by union types with `TypeAdapter` validation
The following union types are no longer `RootModel` subclasses: The following union types are no longer `RootModel` subclasses:
@ -694,7 +725,7 @@ If you prefer the convenience of automatic wrapping, use `MCPServer` which still
### Lowlevel `Server`: `request_context` property removed ### Lowlevel `Server`: `request_context` property removed
The `server.request_context` property has been removed. Request context is now passed directly to handlers as the first argument (`ctx`). The `request_ctx` module-level contextvar is now an internal implementation detail and should not be relied upon. The `server.request_context` property has been removed. Request context is now passed directly to handlers as the first argument (`ctx`). The `request_ctx` module-level contextvar has been removed entirely.
**Before (v1):** **Before (v1):**

View file

@ -36,7 +36,6 @@ handler callables by method string.
from __future__ import annotations from __future__ import annotations
import contextvars
import logging import logging
import warnings import warnings
from collections.abc import AsyncIterator, Awaitable, Callable from collections.abc import AsyncIterator, Awaitable, Callable
@ -74,8 +73,6 @@ logger = logging.getLogger(__name__)
LifespanResultT = TypeVar("LifespanResultT", default=Any) LifespanResultT = TypeVar("LifespanResultT", default=Any)
request_ctx: contextvars.ContextVar[ServerRequestContext[Any]] = contextvars.ContextVar("request_ctx")
class NotificationOptions: class NotificationOptions:
def __init__(self, prompts_changed: bool = False, resources_changed: bool = False, tools_changed: bool = False): def __init__(self, prompts_changed: bool = False, resources_changed: bool = False, tools_changed: bool = False):
@ -474,11 +471,7 @@ class Server(Generic[LifespanResultT]):
close_sse_stream=close_sse_stream_cb, close_sse_stream=close_sse_stream_cb,
close_standalone_sse_stream=close_standalone_sse_stream_cb, close_standalone_sse_stream=close_standalone_sse_stream_cb,
) )
token = request_ctx.set(ctx) response = await handler(ctx, req.params)
try:
response = await handler(ctx, req.params)
finally:
request_ctx.reset(token)
except MCPError as err: except MCPError as err:
response = err.error response = err.error
except anyio.get_cancelled_exc_class(): except anyio.get_cancelled_exc_class():

View file

@ -2,7 +2,8 @@
from mcp.types import Icon from mcp.types import Icon
from .server import Context, MCPServer from .context import Context
from .server import MCPServer
from .utilities.types import Audio, Image from .utilities.types import Audio, Image
__all__ = ["MCPServer", "Context", "Image", "Audio", "Icon"] __all__ = ["MCPServer", "Context", "Image", "Audio", "Icon"]

View file

@ -0,0 +1,280 @@
from __future__ import annotations
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, Generic, Literal
from pydantic import AnyUrl, BaseModel
from mcp.server.context import LifespanContextT, RequestT, ServerRequestContext
from mcp.server.elicitation import (
ElicitationResult,
ElicitSchemaModelT,
UrlElicitationResult,
elicit_url,
elicit_with_validation,
)
from mcp.server.lowlevel.helper_types import ReadResourceContents
if TYPE_CHECKING:
from mcp.server.mcpserver.server import MCPServer
class Context(BaseModel, Generic[LifespanContextT, RequestT]):
"""Context object providing access to MCP capabilities.
This provides a cleaner interface to MCP's RequestContext functionality.
It gets injected into tool and resource functions that request it via type hints.
To use context in a tool function, add a parameter with the Context type annotation:
```python
@server.tool()
async def my_tool(x: int, ctx: Context) -> str:
# Log messages to the client
await ctx.info(f"Processing {x}")
await ctx.debug("Debug info")
await ctx.warning("Warning message")
await ctx.error("Error message")
# Report progress
await ctx.report_progress(50, 100)
# Access resources
data = await ctx.read_resource("resource://data")
# Get request info
request_id = ctx.request_id
client_id = ctx.client_id
return str(x)
```
The context parameter name can be anything as long as it's annotated with Context.
The context is optional - tools that don't need it can omit the parameter.
"""
_request_context: ServerRequestContext[LifespanContextT, RequestT] | None
_mcp_server: MCPServer | None
# TODO(maxisbey): Consider making request_context/mcp_server required, or refactor Context entirely.
def __init__(
self,
*,
request_context: ServerRequestContext[LifespanContextT, RequestT] | None = None,
mcp_server: MCPServer | None = None,
# TODO(Marcelo): We should drop this kwargs parameter.
**kwargs: Any,
):
super().__init__(**kwargs)
self._request_context = request_context
self._mcp_server = mcp_server
@property
def mcp_server(self) -> MCPServer:
"""Access to the MCPServer instance."""
if self._mcp_server is None: # pragma: no cover
raise ValueError("Context is not available outside of a request")
return self._mcp_server # pragma: no cover
@property
def request_context(self) -> ServerRequestContext[LifespanContextT, RequestT]:
"""Access to the underlying request context."""
if self._request_context is None: # pragma: no cover
raise ValueError("Context is not available outside of a request")
return self._request_context
async def report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None:
"""Report progress for the current operation.
Args:
progress: Current progress value (e.g., 24)
total: Optional total value (e.g., 100)
message: Optional message (e.g., "Starting render...")
"""
progress_token = self.request_context.meta.get("progress_token") if self.request_context.meta else None
if progress_token is None: # pragma: no cover
return
await self.request_context.session.send_progress_notification(
progress_token=progress_token,
progress=progress,
total=total,
message=message,
related_request_id=self.request_id,
)
async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]:
"""Read a resource by URI.
Args:
uri: Resource URI to read
Returns:
The resource content as either text or bytes
"""
assert self._mcp_server is not None, "Context is not available outside of a request"
return await self._mcp_server.read_resource(uri, self)
async def elicit(
self,
message: str,
schema: type[ElicitSchemaModelT],
) -> ElicitationResult[ElicitSchemaModelT]:
"""Elicit information from the client/user.
This method can be used to interactively ask for additional information from the
client within a tool's execution. The client might display the message to the
user and collect a response according to the provided schema. If the client
is an agent, it might decide how to handle the elicitation -- either by asking
the user or automatically generating a response.
Args:
message: Message to present to the user
schema: A Pydantic model class defining the expected response structure.
According to the specification, only primitive types are allowed.
Returns:
An ElicitationResult containing the action taken and the data if accepted
Note:
Check the result.action to determine if the user accepted, declined, or cancelled.
The result.data will only be populated if action is "accept" and validation succeeded.
"""
return await elicit_with_validation(
session=self.request_context.session,
message=message,
schema=schema,
related_request_id=self.request_id,
)
async def elicit_url(
self,
message: str,
url: str,
elicitation_id: str,
) -> UrlElicitationResult:
"""Request URL mode elicitation from the client.
This directs the user to an external URL for out-of-band interactions
that must not pass through the MCP client. Use this for:
- Collecting sensitive credentials (API keys, passwords)
- OAuth authorization flows with third-party services
- Payment and subscription flows
- Any interaction where data should not pass through the LLM context
The response indicates whether the user consented to navigate to the URL.
The actual interaction happens out-of-band. When the elicitation completes,
call `ctx.session.send_elicit_complete(elicitation_id)` to notify the client.
Args:
message: Human-readable explanation of why the interaction is needed
url: The URL the user should navigate to
elicitation_id: Unique identifier for tracking this elicitation
Returns:
UrlElicitationResult indicating accept, decline, or cancel
"""
return await elicit_url(
session=self.request_context.session,
message=message,
url=url,
elicitation_id=elicitation_id,
related_request_id=self.request_id,
)
async def log(
self,
level: Literal["debug", "info", "warning", "error"],
message: str,
*,
logger_name: str | None = None,
extra: dict[str, Any] | None = None,
) -> None:
"""Send a log message to the client.
Args:
level: Log level (debug, info, warning, error)
message: Log message
logger_name: Optional logger name
extra: Optional dictionary with additional structured data to include
"""
if extra:
log_data = {"message": message, **extra}
else:
log_data = message
await self.request_context.session.send_log_message(
level=level,
data=log_data,
logger=logger_name,
related_request_id=self.request_id,
)
@property
def client_id(self) -> str | None:
"""Get the client ID if available."""
return self.request_context.meta.get("client_id") if self.request_context.meta else None # pragma: no cover
@property
def request_id(self) -> str:
"""Get the unique ID for this request."""
return str(self.request_context.request_id)
@property
def session(self):
"""Access to the underlying session for advanced usage."""
return self.request_context.session
async def close_sse_stream(self) -> None:
"""Close the SSE stream to trigger client reconnection.
This method closes the HTTP connection for the current request, triggering
client reconnection. Events continue to be stored in the event store and will
be replayed when the client reconnects with Last-Event-ID.
Use this to implement polling behavior during long-running operations -
the client will reconnect after the retry interval specified in the priming event.
Note:
This is a no-op if not using StreamableHTTP transport with event_store.
The callback is only available when event_store is configured.
"""
if self._request_context and self._request_context.close_sse_stream: # pragma: no cover
await self._request_context.close_sse_stream()
async def close_standalone_sse_stream(self) -> None:
"""Close the standalone GET SSE stream to trigger client reconnection.
This method closes the HTTP connection for the standalone GET stream used
for unsolicited server-to-client notifications. The client SHOULD reconnect
with Last-Event-ID to resume receiving notifications.
Note:
This is a no-op if not using StreamableHTTP transport with event_store.
Currently, client reconnection for standalone GET streams is NOT
implemented - this is a known gap.
"""
if self._request_context and self._request_context.close_standalone_sse_stream: # pragma: no cover
await self._request_context.close_standalone_sse_stream()
# Convenience methods for common log levels
async def debug(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
"""Send a debug log message."""
await self.log("debug", message, logger_name=logger_name, extra=extra)
async def info(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
"""Send an info log message."""
await self.log("info", message, logger_name=logger_name, extra=extra)
async def warning(
self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None
) -> None:
"""Send a warning log message."""
await self.log("warning", message, logger_name=logger_name, extra=extra)
async def error(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
"""Send an error log message."""
await self.log("error", message, logger_name=logger_name, extra=extra)

View file

@ -15,7 +15,7 @@ from mcp.types import ContentBlock, Icon, TextContent
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.server.context import LifespanContextT, RequestT from mcp.server.context import LifespanContextT, RequestT
from mcp.server.mcpserver.server import Context from mcp.server.mcpserver.context import Context
class Message(BaseModel): class Message(BaseModel):
@ -135,10 +135,14 @@ class Prompt(BaseModel):
async def render( async def render(
self, self,
arguments: dict[str, Any] | None = None, arguments: dict[str, Any] | None,
context: Context[LifespanContextT, RequestT] | None = None, context: Context[LifespanContextT, RequestT],
) -> list[Message]: ) -> list[Message]:
"""Render the prompt with arguments.""" """Render the prompt with arguments.
Raises:
ValueError: If required arguments are missing, or if rendering fails.
"""
# Validate required arguments # Validate required arguments
if self.arguments: if self.arguments:
required = {arg.name for arg in self.arguments if arg.required} required = {arg.name for arg in self.arguments if arg.required}

View file

@ -9,7 +9,7 @@ from mcp.server.mcpserver.utilities.logging import get_logger
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.server.context import LifespanContextT, RequestT from mcp.server.context import LifespanContextT, RequestT
from mcp.server.mcpserver.server import Context from mcp.server.mcpserver.context import Context
logger = get_logger(__name__) logger = get_logger(__name__)
@ -48,12 +48,12 @@ class PromptManager:
async def render_prompt( async def render_prompt(
self, self,
name: str, name: str,
arguments: dict[str, Any] | None = None, arguments: dict[str, Any] | None,
context: Context[LifespanContextT, RequestT] | None = None, context: Context[LifespanContextT, RequestT],
) -> list[Message]: ) -> list[Message]:
"""Render a prompt by name with arguments.""" """Render a prompt by name with arguments."""
prompt = self.get_prompt(name) prompt = self.get_prompt(name)
if not prompt: if not prompt:
raise ValueError(f"Unknown prompt: {name}") raise ValueError(f"Unknown prompt: {name}")
return await prompt.render(arguments, context=context) return await prompt.render(arguments, context)

View file

@ -14,7 +14,7 @@ from mcp.types import Annotations, Icon
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.server.context import LifespanContextT, RequestT from mcp.server.context import LifespanContextT, RequestT
from mcp.server.mcpserver.server import Context from mcp.server.mcpserver.context import Context
logger = get_logger(__name__) logger = get_logger(__name__)
@ -80,9 +80,7 @@ class ResourceManager:
self._templates[template.uri_template] = template self._templates[template.uri_template] = template
return template return template
async def get_resource( async def get_resource(self, uri: AnyUrl | str, context: Context[LifespanContextT, RequestT]) -> Resource:
self, uri: AnyUrl | str, context: Context[LifespanContextT, RequestT] | None = None
) -> Resource:
"""Get resource by URI, checking concrete resources first, then templates.""" """Get resource by URI, checking concrete resources first, then templates."""
uri_str = str(uri) uri_str = str(uri)
logger.debug("Getting resource", extra={"uri": uri_str}) logger.debug("Getting resource", extra={"uri": uri_str})

View file

@ -17,7 +17,7 @@ from mcp.types import Annotations, Icon
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.server.context import LifespanContextT, RequestT from mcp.server.context import LifespanContextT, RequestT
from mcp.server.mcpserver.server import Context from mcp.server.mcpserver.context import Context
class ResourceTemplate(BaseModel): class ResourceTemplate(BaseModel):
@ -99,9 +99,13 @@ class ResourceTemplate(BaseModel):
self, self,
uri: str, uri: str,
params: dict[str, Any], params: dict[str, Any],
context: Context[LifespanContextT, RequestT] | None = None, context: Context[LifespanContextT, RequestT],
) -> Resource: ) -> Resource:
"""Create a resource from the template with the given parameters.""" """Create a resource from the template with the given parameters.
Raises:
ValueError: If creating the resource fails.
"""
try: try:
# Add context to params if needed # Add context to params if needed
params = inject_context(self.fn, params, context, self.context_kwarg) params = inject_context(self.fn, params, context, self.context_kwarg)

View file

@ -12,7 +12,6 @@ from typing import Any, Generic, Literal, TypeVar, overload
import anyio import anyio
import pydantic_core import pydantic_core
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
@ -27,12 +26,11 @@ from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
from mcp.server.auth.middleware.bearer_auth import BearerAuthBackend, RequireAuthMiddleware from mcp.server.auth.middleware.bearer_auth import 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.context import LifespanContextT, RequestT, ServerRequestContext from mcp.server.context import ServerRequestContext
from mcp.server.elicitation import ElicitationResult, ElicitSchemaModelT, UrlElicitationResult, elicit_with_validation
from mcp.server.elicitation import elicit_url as _elicit_url
from mcp.server.lowlevel.helper_types import ReadResourceContents from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.server.lowlevel.server import LifespanResultT, Server, request_ctx from mcp.server.lowlevel.server import LifespanResultT, Server
from mcp.server.lowlevel.server import lifespan as default_lifespan from mcp.server.lowlevel.server import lifespan as default_lifespan
from mcp.server.mcpserver.context import Context
from mcp.server.mcpserver.exceptions import ResourceError from mcp.server.mcpserver.exceptions import ResourceError
from mcp.server.mcpserver.prompts import Prompt, PromptManager from mcp.server.mcpserver.prompts import Prompt, PromptManager
from mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager from mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager
@ -300,8 +298,9 @@ class MCPServer(Generic[LifespanResultT]):
async def _handle_call_tool( async def _handle_call_tool(
self, ctx: ServerRequestContext[LifespanResultT], params: CallToolRequestParams self, ctx: ServerRequestContext[LifespanResultT], params: CallToolRequestParams
) -> CallToolResult: ) -> CallToolResult:
context = Context(request_context=ctx, mcp_server=self)
try: try:
result = await self.call_tool(params.name, params.arguments or {}) result = await self.call_tool(params.name, params.arguments or {}, context)
except MCPError: except MCPError:
raise raise
except Exception as e: except Exception as e:
@ -332,7 +331,8 @@ class MCPServer(Generic[LifespanResultT]):
async def _handle_read_resource( async def _handle_read_resource(
self, ctx: ServerRequestContext[LifespanResultT], params: ReadResourceRequestParams self, ctx: ServerRequestContext[LifespanResultT], params: ReadResourceRequestParams
) -> ReadResourceResult: ) -> ReadResourceResult:
results = await self.read_resource(params.uri) context = Context(request_context=ctx, mcp_server=self)
results = await self.read_resource(params.uri, context)
contents: list[TextResourceContents | BlobResourceContents] = [] contents: list[TextResourceContents | BlobResourceContents] = []
for item in results: for item in results:
if isinstance(item.content, bytes): if isinstance(item.content, bytes):
@ -368,7 +368,8 @@ class MCPServer(Generic[LifespanResultT]):
async def _handle_get_prompt( async def _handle_get_prompt(
self, ctx: ServerRequestContext[LifespanResultT], params: GetPromptRequestParams self, ctx: ServerRequestContext[LifespanResultT], params: GetPromptRequestParams
) -> GetPromptResult: ) -> GetPromptResult:
return await self.get_prompt(params.name, params.arguments) context = Context(request_context=ctx, mcp_server=self)
return await self.get_prompt(params.name, params.arguments, context)
async def list_tools(self) -> list[MCPTool]: async def list_tools(self) -> list[MCPTool]:
"""List all available tools.""" """List all available tools."""
@ -387,22 +388,13 @@ class MCPServer(Generic[LifespanResultT]):
for info in tools for info in tools
] ]
def get_context(self) -> Context[LifespanResultT, Request]: async def call_tool(
"""Return a Context object. self, name: str, arguments: dict[str, Any], context: Context[LifespanResultT, Any] | None = None
) -> Sequence[ContentBlock] | dict[str, Any]:
Note that the context will only be valid during a request; outside a
request, most methods will error.
"""
try:
request_context = request_ctx.get()
except LookupError:
request_context = None
return Context(request_context=request_context, mcp_server=self)
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Sequence[ContentBlock] | dict[str, Any]:
"""Call a tool by name with arguments.""" """Call a tool by name with arguments."""
context = self.get_context() if context is None:
return await self._tool_manager.call_tool(name, arguments, context=context, convert_result=True) context = Context(mcp_server=self)
return await self._tool_manager.call_tool(name, arguments, context, convert_result=True)
async def list_resources(self) -> list[MCPResource]: async def list_resources(self) -> list[MCPResource]:
"""List all available resources.""" """List all available resources."""
@ -438,12 +430,14 @@ class MCPServer(Generic[LifespanResultT]):
for template in templates for template in templates
] ]
async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]: async def read_resource(
self, uri: AnyUrl | str, context: Context[LifespanResultT, Any] | None = None
) -> Iterable[ReadResourceContents]:
"""Read a resource by URI.""" """Read a resource by URI."""
if context is None:
context = self.get_context() context = Context(mcp_server=self)
try: try:
resource = await self._resource_manager.get_resource(uri, context=context) resource = await self._resource_manager.get_resource(uri, context)
except ValueError: except ValueError:
raise ResourceError(f"Unknown resource: {uri}") raise ResourceError(f"Unknown resource: {uri}")
@ -1087,14 +1081,18 @@ class MCPServer(Generic[LifespanResultT]):
for prompt in prompts for prompt in prompts
] ]
async def get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult: async def get_prompt(
self, name: str, arguments: dict[str, Any] | None = None, context: Context[LifespanResultT, Any] | None = None
) -> GetPromptResult:
"""Get a prompt by name with arguments.""" """Get a prompt by name with arguments."""
if context is None:
context = Context(mcp_server=self)
try: try:
prompt = self._prompt_manager.get_prompt(name) prompt = self._prompt_manager.get_prompt(name)
if not prompt: if not prompt:
raise ValueError(f"Unknown prompt: {name}") raise ValueError(f"Unknown prompt: {name}")
messages = await prompt.render(arguments, context=self.get_context()) messages = await prompt.render(arguments, context)
return GetPromptResult( return GetPromptResult(
description=prompt.description, description=prompt.description,
@ -1103,263 +1101,3 @@ class MCPServer(Generic[LifespanResultT]):
except Exception as e: except Exception as e:
logger.exception(f"Error getting prompt {name}") logger.exception(f"Error getting prompt {name}")
raise ValueError(str(e)) raise ValueError(str(e))
class Context(BaseModel, Generic[LifespanContextT, RequestT]):
"""Context object providing access to MCP capabilities.
This provides a cleaner interface to MCP's RequestContext functionality.
It gets injected into tool and resource functions that request it via type hints.
To use context in a tool function, add a parameter with the Context type annotation:
```python
@server.tool()
async def my_tool(x: int, ctx: Context) -> str:
# Log messages to the client
await ctx.info(f"Processing {x}")
await ctx.debug("Debug info")
await ctx.warning("Warning message")
await ctx.error("Error message")
# Report progress
await ctx.report_progress(50, 100)
# Access resources
data = await ctx.read_resource("resource://data")
# Get request info
request_id = ctx.request_id
client_id = ctx.client_id
return str(x)
```
The context parameter name can be anything as long as it's annotated with Context.
The context is optional - tools that don't need it can omit the parameter.
"""
_request_context: ServerRequestContext[LifespanContextT, RequestT] | None
_mcp_server: MCPServer | None
def __init__(
self,
*,
request_context: ServerRequestContext[LifespanContextT, RequestT] | None = None,
mcp_server: MCPServer | None = None,
# TODO(Marcelo): We should drop this kwargs parameter.
**kwargs: Any,
):
super().__init__(**kwargs)
self._request_context = request_context
self._mcp_server = mcp_server
@property
def mcp_server(self) -> MCPServer:
"""Access to the MCPServer instance."""
if self._mcp_server is None: # pragma: no cover
raise ValueError("Context is not available outside of a request")
return self._mcp_server # pragma: no cover
@property
def request_context(self) -> ServerRequestContext[LifespanContextT, RequestT]:
"""Access to the underlying request context."""
if self._request_context is None: # pragma: no cover
raise ValueError("Context is not available outside of a request")
return self._request_context
async def report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None:
"""Report progress for the current operation.
Args:
progress: Current progress value (e.g., 24)
total: Optional total value (e.g., 100)
message: Optional message (e.g., "Starting render...")
"""
progress_token = self.request_context.meta.get("progress_token") if self.request_context.meta else None
if progress_token is None: # pragma: no cover
return
await self.request_context.session.send_progress_notification(
progress_token=progress_token,
progress=progress,
total=total,
message=message,
related_request_id=self.request_id,
)
async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]:
"""Read a resource by URI.
Args:
uri: Resource URI to read
Returns:
The resource content as either text or bytes
"""
assert self._mcp_server is not None, "Context is not available outside of a request"
return await self._mcp_server.read_resource(uri)
async def elicit(
self,
message: str,
schema: type[ElicitSchemaModelT],
) -> ElicitationResult[ElicitSchemaModelT]:
"""Elicit information from the client/user.
This method can be used to interactively ask for additional information from the
client within a tool's execution. The client might display the message to the
user and collect a response according to the provided schema. If the client
is an agent, it might decide how to handle the elicitation -- either by asking
the user or automatically generating a response.
Args:
message: Message to present to the user
schema: A Pydantic model class defining the expected response structure.
According to the specification, only primitive types are allowed.
Returns:
An ElicitationResult containing the action taken and the data if accepted
Note:
Check the result.action to determine if the user accepted, declined, or cancelled.
The result.data will only be populated if action is "accept" and validation succeeded.
"""
return await elicit_with_validation(
session=self.request_context.session,
message=message,
schema=schema,
related_request_id=self.request_id,
)
async def elicit_url(
self,
message: str,
url: str,
elicitation_id: str,
) -> UrlElicitationResult:
"""Request URL mode elicitation from the client.
This directs the user to an external URL for out-of-band interactions
that must not pass through the MCP client. Use this for:
- Collecting sensitive credentials (API keys, passwords)
- OAuth authorization flows with third-party services
- Payment and subscription flows
- Any interaction where data should not pass through the LLM context
The response indicates whether the user consented to navigate to the URL.
The actual interaction happens out-of-band. When the elicitation completes,
call `ctx.session.send_elicit_complete(elicitation_id)` to notify the client.
Args:
message: Human-readable explanation of why the interaction is needed
url: The URL the user should navigate to
elicitation_id: Unique identifier for tracking this elicitation
Returns:
UrlElicitationResult indicating accept, decline, or cancel
"""
return await _elicit_url(
session=self.request_context.session,
message=message,
url=url,
elicitation_id=elicitation_id,
related_request_id=self.request_id,
)
async def log(
self,
level: Literal["debug", "info", "warning", "error"],
message: str,
*,
logger_name: str | None = None,
extra: dict[str, Any] | None = None,
) -> None:
"""Send a log message to the client.
Args:
level: Log level (debug, info, warning, error)
message: Log message
logger_name: Optional logger name
extra: Optional dictionary with additional structured data to include
"""
if extra:
log_data = {"message": message, **extra}
else:
log_data = message
await self.request_context.session.send_log_message(
level=level,
data=log_data,
logger=logger_name,
related_request_id=self.request_id,
)
@property
def client_id(self) -> str | None:
"""Get the client ID if available."""
return self.request_context.meta.get("client_id") if self.request_context.meta else None # pragma: no cover
@property
def request_id(self) -> str:
"""Get the unique ID for this request."""
return str(self.request_context.request_id)
@property
def session(self):
"""Access to the underlying session for advanced usage."""
return self.request_context.session
async def close_sse_stream(self) -> None:
"""Close the SSE stream to trigger client reconnection.
This method closes the HTTP connection for the current request, triggering
client reconnection. Events continue to be stored in the event store and will
be replayed when the client reconnects with Last-Event-ID.
Use this to implement polling behavior during long-running operations -
the client will reconnect after the retry interval specified in the priming event.
Note:
This is a no-op if not using StreamableHTTP transport with event_store.
The callback is only available when event_store is configured.
"""
if self._request_context and self._request_context.close_sse_stream: # pragma: no cover
await self._request_context.close_sse_stream()
async def close_standalone_sse_stream(self) -> None:
"""Close the standalone GET SSE stream to trigger client reconnection.
This method closes the HTTP connection for the standalone GET stream used
for unsolicited server-to-client notifications. The client SHOULD reconnect
with Last-Event-ID to resume receiving notifications.
Note:
This is a no-op if not using StreamableHTTP transport with event_store.
Currently, client reconnection for standalone GET streams is NOT
implemented - this is a known gap.
"""
if self._request_context and self._request_context.close_standalone_sse_stream: # pragma: no cover
await self._request_context.close_standalone_sse_stream()
# Convenience methods for common log levels
async def debug(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
"""Send a debug log message."""
await self.log("debug", message, logger_name=logger_name, extra=extra)
async def info(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
"""Send an info log message."""
await self.log("info", message, logger_name=logger_name, extra=extra)
async def warning(
self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None
) -> None:
"""Send a warning log message."""
await self.log("warning", message, logger_name=logger_name, extra=extra)
async def error(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
"""Send an error log message."""
await self.log("error", message, logger_name=logger_name, extra=extra)

View file

@ -17,7 +17,7 @@ from mcp.types import Icon, ToolAnnotations
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.server.context import LifespanContextT, RequestT from mcp.server.context import LifespanContextT, RequestT
from mcp.server.mcpserver.server import Context from mcp.server.mcpserver.context import Context
class Tool(BaseModel): class Tool(BaseModel):
@ -92,10 +92,14 @@ class Tool(BaseModel):
async def run( async def run(
self, self,
arguments: dict[str, Any], arguments: dict[str, Any],
context: Context[LifespanContextT, RequestT] | None = None, context: Context[LifespanContextT, RequestT],
convert_result: bool = False, convert_result: bool = False,
) -> Any: ) -> Any:
"""Run the tool with arguments.""" """Run the tool with arguments.
Raises:
ToolError: If the tool function raises during execution.
"""
try: try:
result = await self.fn_metadata.call_fn_with_arg_validation( result = await self.fn_metadata.call_fn_with_arg_validation(
self.fn, self.fn,

View file

@ -10,7 +10,7 @@ from mcp.types import Icon, ToolAnnotations
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.server.context import LifespanContextT, RequestT from mcp.server.context import LifespanContextT, RequestT
from mcp.server.mcpserver.server import Context from mcp.server.mcpserver.context import Context
logger = get_logger(__name__) logger = get_logger(__name__)
@ -81,7 +81,7 @@ class ToolManager:
self, self,
name: str, name: str,
arguments: dict[str, Any], arguments: dict[str, Any],
context: Context[LifespanContextT, RequestT] | None = None, context: Context[LifespanContextT, RequestT],
convert_result: bool = False, convert_result: bool = False,
) -> Any: ) -> Any:
"""Call a tool by name with arguments.""" """Call a tool by name with arguments."""
@ -89,4 +89,4 @@ class ToolManager:
if not tool: if not tool:
raise ToolError(f"Unknown tool: {name}") raise ToolError(f"Unknown tool: {name}")
return await tool.run(arguments, context=context, convert_result=convert_result) return await tool.run(arguments, context, convert_result=convert_result)

View file

@ -7,6 +7,8 @@ import typing
from collections.abc import Callable from collections.abc import Callable
from typing import Any from typing import Any
from mcp.server.mcpserver.context import Context
def find_context_parameter(fn: Callable[..., Any]) -> str | None: def find_context_parameter(fn: Callable[..., Any]) -> str | None:
"""Find the parameter that should receive the Context object. """Find the parameter that should receive the Context object.
@ -20,8 +22,6 @@ def find_context_parameter(fn: Callable[..., Any]) -> str | None:
Returns: Returns:
The name of the context parameter, or None if not found The name of the context parameter, or None if not found
""" """
from mcp.server.mcpserver.server import Context
# Get type hints to properly resolve string annotations # Get type hints to properly resolve string annotations
try: try:
hints = typing.get_type_hints(fn) hints = typing.get_type_hints(fn)

View file

@ -3,8 +3,7 @@ from pydantic import FileUrl
from mcp import Client from mcp import Client
from mcp.client.session import ClientSession from mcp.client.session import ClientSession
from mcp.server.mcpserver import MCPServer from mcp.server.mcpserver import Context, MCPServer
from mcp.server.mcpserver.server import Context
from mcp.shared._context import RequestContext from mcp.shared._context import RequestContext
from mcp.types import ListRootsResult, Root, TextContent from mcp.types import ListRootsResult, Root, TextContent

View file

@ -3,7 +3,7 @@ from typing import Any, Literal
import pytest import pytest
from mcp import Client, types from mcp import Client, types
from mcp.server.mcpserver import MCPServer from mcp.server.mcpserver import Context, MCPServer
from mcp.shared.session import RequestResponder from mcp.shared.session import RequestResponder
from mcp.types import ( from mcp.types import (
LoggingMessageNotificationParams, LoggingMessageNotificationParams,
@ -33,14 +33,10 @@ async def test_logging_callback():
# Create a function that can send a log notification # Create a function that can send a log notification
@server.tool("test_tool_with_log") @server.tool("test_tool_with_log")
async def test_tool_with_log( async def test_tool_with_log(
message: str, level: Literal["debug", "info", "warning", "error"], logger: str message: str, level: Literal["debug", "info", "warning", "error"], logger: str, ctx: Context
) -> bool: ) -> bool:
"""Send a log notification to the client.""" """Send a log notification to the client."""
await server.get_context().log( await ctx.log(level=level, message=message, logger_name=logger)
level=level,
message=message,
logger_name=logger,
)
return True return True
@server.tool("test_tool_with_log_extra") @server.tool("test_tool_with_log_extra")
@ -50,9 +46,10 @@ async def test_logging_callback():
logger: str, logger: str,
extra_string: str, extra_string: str,
extra_dict: dict[str, Any], extra_dict: dict[str, Any],
ctx: Context,
) -> bool: ) -> bool:
"""Send a log notification to the client with extra fields.""" """Send a log notification to the client with extra fields."""
await server.get_context().log( await ctx.log(
level=level, level=level,
message=message, message=message,
logger_name=logger, logger_name=logger,

View file

@ -2,7 +2,7 @@ import pytest
from mcp import Client from mcp import Client
from mcp.client.session import ClientSession from mcp.client.session import ClientSession
from mcp.server.mcpserver import MCPServer from mcp.server.mcpserver import Context, MCPServer
from mcp.shared._context import RequestContext from mcp.shared._context import RequestContext
from mcp.types import ( from mcp.types import (
CreateMessageRequestParams, CreateMessageRequestParams,
@ -32,8 +32,8 @@ async def test_sampling_callback():
return callback_return return callback_return
@server.tool("test_sampling") @server.tool("test_sampling")
async def test_sampling_tool(message: str): async def test_sampling_tool(message: str, ctx: Context) -> bool:
value = await server.get_context().session.create_message( value = await ctx.session.create_message(
messages=[SamplingMessage(role="user", content=TextContent(type="text", text=message))], messages=[SamplingMessage(role="user", content=TextContent(type="text", text=message))],
max_tokens=100, max_tokens=100,
) )
@ -77,9 +77,9 @@ async def test_create_message_backwards_compat_single_content():
return callback_return return callback_return
@server.tool("test_backwards_compat") @server.tool("test_backwards_compat")
async def test_tool(message: str): async def test_tool(message: str, ctx: Context) -> bool:
# Call create_message WITHOUT tools # Call create_message WITHOUT tools
result = await server.get_context().session.create_message( result = await ctx.session.create_message(
messages=[SamplingMessage(role="user", content=TextContent(type="text", text=message))], messages=[SamplingMessage(role="user", content=TextContent(type="text", text=message))],
max_tokens=100, max_tokens=100,
) )

View file

@ -2,6 +2,7 @@ from typing import Any
import pytest import pytest
from mcp.server.mcpserver import Context
from mcp.server.mcpserver.prompts.base import AssistantMessage, Message, Prompt, UserMessage from mcp.server.mcpserver.prompts.base import AssistantMessage, Message, Prompt, UserMessage
from mcp.types import EmbeddedResource, TextContent, TextResourceContents from mcp.types import EmbeddedResource, TextContent, TextResourceContents
@ -13,7 +14,9 @@ class TestRenderPrompt:
return "Hello, world!" return "Hello, world!"
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
assert await prompt.render() == [UserMessage(content=TextContent(type="text", text="Hello, world!"))] assert await prompt.render(None, Context()) == [
UserMessage(content=TextContent(type="text", text="Hello, world!"))
]
@pytest.mark.anyio @pytest.mark.anyio
async def test_async_fn(self): async def test_async_fn(self):
@ -21,7 +24,9 @@ class TestRenderPrompt:
return "Hello, world!" return "Hello, world!"
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
assert await prompt.render() == [UserMessage(content=TextContent(type="text", text="Hello, world!"))] assert await prompt.render(None, Context()) == [
UserMessage(content=TextContent(type="text", text="Hello, world!"))
]
@pytest.mark.anyio @pytest.mark.anyio
async def test_fn_with_args(self): async def test_fn_with_args(self):
@ -29,7 +34,7 @@ class TestRenderPrompt:
return f"Hello, {name}! You're {age} years old." return f"Hello, {name}! You're {age} years old."
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
assert await prompt.render(arguments={"name": "World"}) == [ assert await prompt.render({"name": "World"}, Context()) == [
UserMessage(content=TextContent(type="text", text="Hello, World! You're 30 years old.")) UserMessage(content=TextContent(type="text", text="Hello, World! You're 30 years old."))
] ]
@ -40,7 +45,7 @@ class TestRenderPrompt:
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
with pytest.raises(ValueError): with pytest.raises(ValueError):
await prompt.render(arguments={"age": 40}) await prompt.render({"age": 40}, Context())
@pytest.mark.anyio @pytest.mark.anyio
async def test_fn_returns_message(self): async def test_fn_returns_message(self):
@ -48,7 +53,9 @@ class TestRenderPrompt:
return UserMessage(content="Hello, world!") return UserMessage(content="Hello, world!")
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
assert await prompt.render() == [UserMessage(content=TextContent(type="text", text="Hello, world!"))] assert await prompt.render(None, Context()) == [
UserMessage(content=TextContent(type="text", text="Hello, world!"))
]
@pytest.mark.anyio @pytest.mark.anyio
async def test_fn_returns_assistant_message(self): async def test_fn_returns_assistant_message(self):
@ -56,7 +63,9 @@ class TestRenderPrompt:
return AssistantMessage(content=TextContent(type="text", text="Hello, world!")) return AssistantMessage(content=TextContent(type="text", text="Hello, world!"))
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
assert await prompt.render() == [AssistantMessage(content=TextContent(type="text", text="Hello, world!"))] assert await prompt.render(None, Context()) == [
AssistantMessage(content=TextContent(type="text", text="Hello, world!"))
]
@pytest.mark.anyio @pytest.mark.anyio
async def test_fn_returns_multiple_messages(self): async def test_fn_returns_multiple_messages(self):
@ -70,7 +79,7 @@ class TestRenderPrompt:
return expected return expected
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
assert await prompt.render() == expected assert await prompt.render(None, Context()) == expected
@pytest.mark.anyio @pytest.mark.anyio
async def test_fn_returns_list_of_strings(self): async def test_fn_returns_list_of_strings(self):
@ -83,7 +92,7 @@ class TestRenderPrompt:
return expected return expected
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
assert await prompt.render() == [UserMessage(t) for t in expected] assert await prompt.render(None, Context()) == [UserMessage(t) for t in expected]
@pytest.mark.anyio @pytest.mark.anyio
async def test_fn_returns_resource_content(self): async def test_fn_returns_resource_content(self):
@ -102,7 +111,7 @@ class TestRenderPrompt:
) )
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
assert await prompt.render() == [ assert await prompt.render(None, Context()) == [
UserMessage( UserMessage(
content=EmbeddedResource( content=EmbeddedResource(
type="resource", type="resource",
@ -136,7 +145,7 @@ class TestRenderPrompt:
] ]
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
assert await prompt.render() == [ assert await prompt.render(None, Context()) == [
UserMessage(content=TextContent(type="text", text="Please analyze this file:")), UserMessage(content=TextContent(type="text", text="Please analyze this file:")),
UserMessage( UserMessage(
content=EmbeddedResource( content=EmbeddedResource(
@ -169,7 +178,7 @@ class TestRenderPrompt:
} }
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
assert await prompt.render() == [ assert await prompt.render(None, Context()) == [
UserMessage( UserMessage(
content=EmbeddedResource( content=EmbeddedResource(
type="resource", type="resource",

View file

@ -1,5 +1,6 @@
import pytest import pytest
from mcp.server.mcpserver import Context
from mcp.server.mcpserver.prompts.base import Prompt, UserMessage from mcp.server.mcpserver.prompts.base import Prompt, UserMessage
from mcp.server.mcpserver.prompts.manager import PromptManager from mcp.server.mcpserver.prompts.manager import PromptManager
from mcp.types import TextContent from mcp.types import TextContent
@ -72,7 +73,7 @@ class TestPromptManager:
manager = PromptManager() manager = PromptManager()
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
manager.add_prompt(prompt) manager.add_prompt(prompt)
messages = await manager.render_prompt("fn") messages = await manager.render_prompt("fn", None, Context())
assert messages == [UserMessage(content=TextContent(type="text", text="Hello, world!"))] assert messages == [UserMessage(content=TextContent(type="text", text="Hello, world!"))]
@pytest.mark.anyio @pytest.mark.anyio
@ -85,7 +86,7 @@ class TestPromptManager:
manager = PromptManager() manager = PromptManager()
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
manager.add_prompt(prompt) manager.add_prompt(prompt)
messages = await manager.render_prompt("fn", arguments={"name": "World"}) messages = await manager.render_prompt("fn", {"name": "World"}, Context())
assert messages == [UserMessage(content=TextContent(type="text", text="Hello, World!"))] assert messages == [UserMessage(content=TextContent(type="text", text="Hello, World!"))]
@pytest.mark.anyio @pytest.mark.anyio
@ -93,7 +94,7 @@ class TestPromptManager:
"""Test rendering a non-existent prompt.""" """Test rendering a non-existent prompt."""
manager = PromptManager() manager = PromptManager()
with pytest.raises(ValueError, match="Unknown prompt: unknown"): with pytest.raises(ValueError, match="Unknown prompt: unknown"):
await manager.render_prompt("unknown") await manager.render_prompt("unknown", None, Context())
@pytest.mark.anyio @pytest.mark.anyio
async def test_render_prompt_with_missing_args(self): async def test_render_prompt_with_missing_args(self):
@ -106,4 +107,4 @@ class TestPromptManager:
prompt = Prompt.from_function(fn) prompt = Prompt.from_function(fn)
manager.add_prompt(prompt) manager.add_prompt(prompt)
with pytest.raises(ValueError, match="Missing required arguments"): with pytest.raises(ValueError, match="Missing required arguments"):
await manager.render_prompt("fn") await manager.render_prompt("fn", None, Context())

View file

@ -4,6 +4,7 @@ from tempfile import NamedTemporaryFile
import pytest import pytest
from pydantic import AnyUrl from pydantic import AnyUrl
from mcp.server.mcpserver import Context
from mcp.server.mcpserver.resources import FileResource, FunctionResource, ResourceManager, ResourceTemplate from mcp.server.mcpserver.resources import FileResource, FunctionResource, ResourceManager, ResourceTemplate
@ -86,7 +87,7 @@ class TestResourceManager:
path=temp_file, path=temp_file,
) )
manager.add_resource(resource) manager.add_resource(resource)
retrieved = await manager.get_resource(resource.uri) retrieved = await manager.get_resource(resource.uri, Context())
assert retrieved == resource assert retrieved == resource
@pytest.mark.anyio @pytest.mark.anyio
@ -104,7 +105,7 @@ class TestResourceManager:
) )
manager._templates[template.uri_template] = template manager._templates[template.uri_template] = template
resource = await manager.get_resource(AnyUrl("greet://world")) resource = await manager.get_resource(AnyUrl("greet://world"), Context())
assert isinstance(resource, FunctionResource) assert isinstance(resource, FunctionResource)
content = await resource.read() content = await resource.read()
assert content == "Hello, world!" assert content == "Hello, world!"
@ -114,7 +115,7 @@ class TestResourceManager:
"""Test getting a non-existent resource.""" """Test getting a non-existent resource."""
manager = ResourceManager() manager = ResourceManager()
with pytest.raises(ValueError, match="Unknown resource"): with pytest.raises(ValueError, match="Unknown resource"):
await manager.get_resource(AnyUrl("unknown://test")) await manager.get_resource(AnyUrl("unknown://test"), Context())
def test_list_resources(self, temp_file: Path): def test_list_resources(self, temp_file: Path):
"""Test listing all resources.""" """Test listing all resources."""

View file

@ -4,7 +4,7 @@ from typing import Any
import pytest import pytest
from pydantic import BaseModel from pydantic import BaseModel
from mcp.server.mcpserver import MCPServer from mcp.server.mcpserver import Context, MCPServer
from mcp.server.mcpserver.resources import FunctionResource, ResourceTemplate from mcp.server.mcpserver.resources import FunctionResource, ResourceTemplate
from mcp.types import Annotations from mcp.types import Annotations
@ -64,6 +64,7 @@ class TestResourceTemplate:
resource = await template.create_resource( resource = await template.create_resource(
"test://foo/123", "test://foo/123",
{"key": "foo", "value": 123}, {"key": "foo", "value": 123},
Context(),
) )
assert isinstance(resource, FunctionResource) assert isinstance(resource, FunctionResource)
@ -86,7 +87,7 @@ class TestResourceTemplate:
) )
with pytest.raises(ValueError, match="Error creating resource from template"): with pytest.raises(ValueError, match="Error creating resource from template"):
await template.create_resource("fail://test", {"x": "test"}) await template.create_resource("fail://test", {"x": "test"}, Context())
@pytest.mark.anyio @pytest.mark.anyio
async def test_async_text_resource(self): async def test_async_text_resource(self):
@ -104,6 +105,7 @@ class TestResourceTemplate:
resource = await template.create_resource( resource = await template.create_resource(
"greet://world", "greet://world",
{"name": "world"}, {"name": "world"},
Context(),
) )
assert isinstance(resource, FunctionResource) assert isinstance(resource, FunctionResource)
@ -126,6 +128,7 @@ class TestResourceTemplate:
resource = await template.create_resource( resource = await template.create_resource(
"bytes://test", "bytes://test",
{"value": "test"}, {"value": "test"},
Context(),
) )
assert isinstance(resource, FunctionResource) assert isinstance(resource, FunctionResource)
@ -152,6 +155,7 @@ class TestResourceTemplate:
resource = await template.create_resource( resource = await template.create_resource(
"test://foo/123", "test://foo/123",
{"key": "foo", "value": 123}, {"key": "foo", "value": 123},
Context(),
) )
assert isinstance(resource, FunctionResource) assert isinstance(resource, FunctionResource)
@ -183,6 +187,7 @@ class TestResourceTemplate:
resource = await template.create_resource( resource = await template.create_resource(
"test://hello", "test://hello",
{"value": "hello"}, {"value": "hello"},
Context(),
) )
assert isinstance(resource, FunctionResource) assert isinstance(resource, FunctionResource)
@ -249,7 +254,7 @@ class TestResourceTemplateAnnotations:
) )
# Create a resource from the template # Create a resource from the template
resource = await template.create_resource("resource://items/123", {"item_id": "123"}) resource = await template.create_resource("resource://items/123", {"item_id": "123"}, Context())
# The resource should inherit the template's annotations # The resource should inherit the template's annotations
assert resource.annotations is not None assert resource.annotations is not None
@ -298,7 +303,7 @@ class TestResourceTemplateMetadata:
) )
# Create a resource from the template # Create a resource from the template
resource = await template.create_resource("resource://items/123", {"item_id": "123"}) resource = await template.create_resource("resource://items/123", {"item_id": "123"}, Context())
# The resource should inherit the template's metadata # The resource should inherit the template's metadata
assert resource.meta is not None assert resource.meta is not None

View file

@ -891,7 +891,7 @@ class TestServerResourceTemplates:
assert len(await mcp.list_resources()) == 0 assert len(await mcp.list_resources()) == 0
# When accessed, should create a concrete resource # When accessed, should create a concrete resource
resource = await mcp._resource_manager.get_resource("resource://test/data") resource = await mcp._resource_manager.get_resource("resource://test/data", Context())
assert isinstance(resource, FunctionResource) assert isinstance(resource, FunctionResource)
result = await resource.read() result = await resource.read()
assert result == "Data for test" assert result == "Data for test"
@ -1231,6 +1231,19 @@ class TestContextInjection:
class TestServerPrompts: class TestServerPrompts:
"""Test prompt functionality in MCPServer server.""" """Test prompt functionality in MCPServer server."""
async def test_get_prompt_direct_call_without_context(self):
"""Test calling mcp.get_prompt() directly without passing context."""
mcp = MCPServer()
@mcp.prompt()
def fn() -> str:
return "Hello, world!"
result = await mcp.get_prompt("fn")
content = result.messages[0].content
assert isinstance(content, TextContent)
assert content.text == "Hello, world!"
async def test_prompt_decorator(self): async def test_prompt_decorator(self):
"""Test that the prompt decorator registers prompts correctly.""" """Test that the prompt decorator registers prompts correctly."""
mcp = MCPServer() mcp = MCPServer()
@ -1243,7 +1256,7 @@ class TestServerPrompts:
assert len(prompts) == 1 assert len(prompts) == 1
assert prompts[0].name == "fn" assert prompts[0].name == "fn"
# Don't compare functions directly since validate_call wraps them # Don't compare functions directly since validate_call wraps them
content = await prompts[0].render() content = await prompts[0].render(None, Context())
assert isinstance(content[0].content, TextContent) assert isinstance(content[0].content, TextContent)
assert content[0].content.text == "Hello, world!" assert content[0].content.text == "Hello, world!"
@ -1258,7 +1271,7 @@ class TestServerPrompts:
prompts = mcp._prompt_manager.list_prompts() prompts = mcp._prompt_manager.list_prompts()
assert len(prompts) == 1 assert len(prompts) == 1
assert prompts[0].name == "custom_name" assert prompts[0].name == "custom_name"
content = await prompts[0].render() content = await prompts[0].render(None, Context())
assert isinstance(content[0].content, TextContent) assert isinstance(content[0].content, TextContent)
assert content[0].content.text == "Hello, world!" assert content[0].content.text == "Hello, world!"
@ -1273,7 +1286,7 @@ class TestServerPrompts:
prompts = mcp._prompt_manager.list_prompts() prompts = mcp._prompt_manager.list_prompts()
assert len(prompts) == 1 assert len(prompts) == 1
assert prompts[0].description == "A custom description" assert prompts[0].description == "A custom description"
content = await prompts[0].render() content = await prompts[0].render(None, Context())
assert isinstance(content[0].content, TextContent) assert isinstance(content[0].content, TextContent)
assert content[0].content.text == "Hello, world!" assert content[0].content.text == "Hello, world!"

Some files were not shown because too many files have changed in this diff Show more