Skip to content
Open
66 changes: 66 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1426,6 +1426,72 @@ app = server.streamable_http_app(

The lowlevel `Server` also now exposes a `session_manager` property to access the `StreamableHTTPSessionManager` after calling `streamable_http_app()`.

### Resolver dependency injection for tools (`Resolve` / `Elicit`)

A tool parameter annotated `Annotated[T, Resolve(fn)]` is filled by running the resolver `fn` before the tool body, instead of by the calling LLM. Resolvers form a dependency graph: a resolver may declare its own `Resolve(...)` dependencies, read the `Context` (including `ctx.headers`), and receive the tool's own arguments by name. A resolver may return `Elicit[T]` to ask the client; the SDK runs the elicitation and injects the answer. Each resolver runs at most once per `tools/call`.

```python
from typing import Annotated

from pydantic import BaseModel

from mcp.server.mcpserver import (
AcceptedElicitation,
CancelledElicitation,
Context,
DeclinedElicitation,
Elicit,
MCPServer,
Resolve,
)

mcp = MCPServer(name="github")


class Login(BaseModel):
username: str


class Confirm(BaseModel):
ok: bool


async def login(ctx: Context) -> Login | Elicit[Login]:
if username := (ctx.headers or {}).get("x-github-user"):
return Login(username=username) # resolved from context, no question
return Elicit("GitHub username?", Login) # must ask


async def confirm(repo: str, login: Annotated[Login, Resolve(login)]) -> Elicit[Confirm]:
return Elicit(f"Star {repo} as {login.username}?", Confirm)


@mcp.tool()
async def star_repo(
repo: str,
login: Annotated[Login, Resolve(login)],
confirm: Annotated[Confirm, Resolve(confirm)],
) -> str:
"""Star a GitHub repo."""
return f"starred {repo} as {login.username}" if confirm.ok else "cancelled"
```

The injected type follows the consumer's annotation. Annotating the unwrapped model (`Annotated[Login, Resolve(login)]`) injects the model on accept and aborts the call with an error result on decline or cancel. To branch on the outcome instead, annotate the elicitation result union:

```python
@mcp.tool()
async def whoami(
login: Annotated[AcceptedElicitation[Login] | DeclinedElicitation | CancelledElicitation, Resolve(login)],
) -> str:
match login:
case AcceptedElicitation(data=data):
return f"hi {data.username}"
case _:
return "no username provided"
```

Resolved parameters are omitted from the tool's input schema, so the client never supplies them. Resolver parameters that cannot be classified, and cyclic resolver dependencies, raise at registration time.

## Need Help?

If you encounter issues during migration:
Expand Down
22 changes: 21 additions & 1 deletion src/mcp/server/mcpserver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,27 @@
from mcp.types import Icon

from .context import Context
from .resolve import (
AcceptedElicitation,
CancelledElicitation,
DeclinedElicitation,
Elicit,
ElicitationResult,
Resolve,
)
from .server import MCPServer
from .utilities.types import Audio, Image

__all__ = ["MCPServer", "Context", "Image", "Audio", "Icon"]
__all__ = [
"MCPServer",
"Context",
"Image",
"Audio",
"Icon",
"Resolve",
"Elicit",
"ElicitationResult",
"AcceptedElicitation",
"DeclinedElicitation",
"CancelledElicitation",
]
20 changes: 18 additions & 2 deletions src/mcp/server/mcpserver/context.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, Generic
from collections.abc import Iterable, Mapping
from typing import TYPE_CHECKING, Any, Generic, Protocol, cast

from pydantic import AnyUrl, BaseModel
from typing_extensions import deprecated
Expand All @@ -22,6 +22,11 @@
from mcp.server.mcpserver.server import MCPServer


class _HasHeaders(Protocol):
@property
def headers(self) -> Mapping[str, str]: ...


class Context(BaseModel, Generic[LifespanContextT, RequestT]):
"""Context object providing access to MCP capabilities.

Expand Down Expand Up @@ -214,6 +219,17 @@ def client_id(self) -> str | None:
"""
return self.request_context.meta.get("client_id") if self.request_context.meta else None # pragma: no cover

@property
def headers(self) -> Mapping[str, str] | None:
"""Request headers carried by this message, when the transport has them.

Populated by HTTP-based transports; `None` on stdio.
"""
request = self.request_context.request
if request is None:
return None
return cast("_HasHeaders", request).headers

@property
def request_id(self) -> str:
"""Get the unique ID for this request."""
Expand Down
Loading
Loading