Skip to content
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -886,9 +886,10 @@ async def generate_poem(topic: str, ctx: Context[ServerSession, None]) -> str:
max_tokens=100,
)

if result.content.type == "text":
return result.content.text
return str(result.content)
content = result.content[0] if isinstance(result.content, list) else result.content
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want this as the example of how to use the new multiple content blocks :)

(same comment on other places where this pattern shows up)

if content.type == "text":
return content.text
return str(content)
```

_Full example: [examples/snippets/servers/sampling.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/sampling.py)_
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,9 @@ async def test_sampling(prompt: str, ctx: Context[ServerSession, None]) -> str:
max_tokens=100,
)

if result.content.type == "text":
model_response = result.content.text
content = result.content[0] if isinstance(result.content, list) else result.content
if content.type == "text":
model_response = content.text
else:
model_response = "No response"

Expand Down
7 changes: 4 additions & 3 deletions examples/snippets/servers/sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ async def generate_poem(topic: str, ctx: Context[ServerSession, None]) -> str:
max_tokens=100,
)

if result.content.type == "text":
return result.content.text
return str(result.content)
content = result.content[0] if isinstance(result.content, list) else result.content
if content.type == "text":
return content.text
return str(content)
20 changes: 17 additions & 3 deletions src/mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@
ResourcesCapability,
ResourceUpdatedNotification,
RootsCapability,
SamplingCapability,
SamplingContextCapability,
SamplingMessage,
SamplingMessageContentBlock,
SamplingToolsCapability,
ServerCapabilities,
ServerNotification,
ServerRequest,
Expand All @@ -50,7 +54,10 @@
StopReason,
SubscribeRequest,
Tool,
ToolChoice,
ToolResultContent,
ToolsCapability,
ToolUseContent,
UnsubscribeRequest,
)
from .types import (
Expand All @@ -65,6 +72,7 @@
"ClientResult",
"ClientSession",
"ClientSessionGroup",
"CompleteRequest",
"CreateMessageRequest",
"CreateMessageResult",
"ErrorData",
Expand All @@ -77,6 +85,7 @@
"InitializedNotification",
"JSONRPCError",
"JSONRPCRequest",
"JSONRPCResponse",
"ListPromptsRequest",
"ListPromptsResult",
"ListResourcesRequest",
Expand All @@ -91,12 +100,16 @@
"PromptsCapability",
"ReadResourceRequest",
"ReadResourceResult",
"Resource",
"ResourcesCapability",
"ResourceUpdatedNotification",
"Resource",
"RootsCapability",
"SamplingCapability",
"SamplingContextCapability",
"SamplingMessage",
"SamplingMessageContentBlock",
"SamplingRole",
"SamplingToolsCapability",
"ServerCapabilities",
"ServerNotification",
"ServerRequest",
Expand All @@ -107,10 +120,11 @@
"StopReason",
"SubscribeRequest",
"Tool",
"ToolChoice",
"ToolResultContent",
"ToolsCapability",
"ToolUseContent",
"UnsubscribeRequest",
"stdio_client",
"stdio_server",
"CompleteRequest",
"JSONRPCResponse",
]
168 changes: 160 additions & 8 deletions src/mcp/types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections.abc import Callable
from typing import Annotated, Any, Generic, Literal, TypeAlias, TypeVar
from typing import Annotated, Any, Generic, Literal, TypeAlias, TypeVar, Union

from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel
from pydantic.networks import AnyUrl, UrlConstraints
Expand Down Expand Up @@ -250,8 +250,24 @@ class RootsCapability(BaseModel):
model_config = ConfigDict(extra="allow")


class SamplingCapability(BaseModel):
"""Capability for sampling operations."""
class SamplingContextCapability(BaseModel):
"""
Capability for context inclusion during sampling.

Indicates support for non-'none' values in the includeContext parameter.
SOFT-DEPRECATED: New implementations should use tools parameter instead.
"""

model_config = ConfigDict(extra="allow")


class SamplingToolsCapability(BaseModel):
"""
Capability indicating support for tool calling during sampling.

When present in ClientCapabilities.sampling, indicates that the client
supports the tools and toolChoice parameters in sampling requests.
"""

model_config = ConfigDict(extra="allow")

Expand All @@ -262,13 +278,34 @@ class ElicitationCapability(BaseModel):
model_config = ConfigDict(extra="allow")


class SamplingCapability(BaseModel):
"""
Sampling capability structure, allowing fine-grained capability advertisement.
"""

context: SamplingContextCapability | None = None
"""
Present if the client supports non-'none' values for includeContext parameter.
SOFT-DEPRECATED: New implementations should use tools parameter instead.
"""
tools: SamplingToolsCapability | None = None
"""
Present if the client supports tools and toolChoice parameters in sampling requests.
Presence indicates full tool calling support during sampling.
"""
model_config = ConfigDict(extra="allow")


class ClientCapabilities(BaseModel):
"""Capabilities a client may support."""

experimental: dict[str, dict[str, Any]] | None = None
"""Experimental, non-standard capabilities that the client supports."""
sampling: SamplingCapability | None = None
"""Present if the client supports sampling from an LLM."""
"""
Present if the client supports sampling from an LLM.
Can contain fine-grained capabilities like context and tools support.
"""
elicitation: ElicitationCapability | None = None
"""Present if the client supports elicitation from the user."""
roots: RootsCapability | None = None
Expand Down Expand Up @@ -742,11 +779,89 @@ class AudioContent(BaseModel):
model_config = ConfigDict(extra="allow")


class ToolUseContent(BaseModel):
"""
Content representing an assistant's request to invoke a tool.

This content type appears in assistant messages when the LLM wants to call a tool
during sampling. The server should execute the tool and return a ToolResultContent
in the next user message.
"""

type: Literal["tool_use"]
"""Discriminator for tool use content."""

name: str
"""The name of the tool to invoke. Must match a tool name from the request's tools array."""

id: str
"""Unique identifier for this tool call, used to correlate with ToolResultContent."""

input: dict[str, Any]
"""Arguments to pass to the tool. Must conform to the tool's inputSchema."""

meta: dict[str, Any] | None = Field(alias="_meta", default=None)
"""
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage.
"""
model_config = ConfigDict(extra="allow")


class ToolResultContent(BaseModel):
"""
Content representing the result of a tool execution.

This content type appears in user messages as a response to a ToolUseContent
from the assistant. It contains the output of executing the requested tool.
"""

type: Literal["tool_result"]
"""Discriminator for tool result content."""

toolUseId: str
"""The unique identifier that corresponds to the tool call's id field."""

content: list[Union[TextContent, ImageContent, AudioContent, "ResourceLink", "EmbeddedResource"]] = []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can just use ContentBlock here?

"""
A list of content objects representing the tool result.
Defaults to empty list if not provided.
"""

structuredContent: dict[str, Any] | None = None
"""
Optional structured tool output that matches the tool's outputSchema (if defined).
"""

isError: bool | None = None
"""Whether the tool execution resulted in an error."""

meta: dict[str, Any] | None = Field(alias="_meta", default=None)
"""
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage.
"""
model_config = ConfigDict(extra="allow")


SamplingMessageContentBlock: TypeAlias = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent
"""Content block types allowed in sampling messages."""


class SamplingMessage(BaseModel):
"""Describes a message issued to or received from an LLM API."""

role: Role
content: TextContent | ImageContent | AudioContent
content: SamplingMessageContentBlock | list[SamplingMessageContentBlock]
"""
Message content. Can be a single content block or an array of content blocks
for multi-modal messages and tool interactions.
"""
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
"""
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
for notes on _meta usage.
"""
model_config = ConfigDict(extra="allow")


Expand Down Expand Up @@ -1035,6 +1150,25 @@ class ModelPreferences(BaseModel):
model_config = ConfigDict(extra="allow")


class ToolChoice(BaseModel):
"""
Controls tool usage behavior during sampling.

Allows the server to specify whether and how the LLM should use tools
in its response.
"""

mode: Literal["auto", "required", "none"] | None = None
"""
Controls when tools are used:
- "auto": Model decides whether to use tools (default)
- "required": Model MUST use at least one tool before completing
- "none": Model should not use tools
"""

model_config = ConfigDict(extra="allow")


class CreateMessageRequestParams(RequestParams):
"""Parameters for creating a message."""

Expand All @@ -1057,6 +1191,16 @@ class CreateMessageRequestParams(RequestParams):
stopSequences: list[str] | None = None
metadata: dict[str, Any] | None = None
"""Optional metadata to pass through to the LLM provider."""
tools: list["Tool"] | None = None
"""
Tool definitions for the LLM to use during sampling.
Requires clientCapabilities.sampling.tools to be present.
"""
toolChoice: ToolChoice | None = None
"""
Controls tool usage behavior.
Requires clientCapabilities.sampling.tools and the tools parameter to be present.
"""
model_config = ConfigDict(extra="allow")


Expand All @@ -1067,18 +1211,26 @@ class CreateMessageRequest(Request[CreateMessageRequestParams, Literal["sampling
params: CreateMessageRequestParams


StopReason = Literal["endTurn", "stopSequence", "maxTokens"] | str
StopReason = Literal["endTurn", "stopSequence", "maxTokens", "toolUse"] | str


class CreateMessageResult(Result):
"""The client's response to a sampling/create_message request from the server."""

role: Role
content: TextContent | ImageContent | AudioContent
"""The role of the message sender (typically 'assistant' for LLM responses)."""
content: SamplingMessageContentBlock | list[SamplingMessageContentBlock]
"""
Response content. May be a single content block or an array.
May include ToolUseContent if stopReason is 'toolUse'.
"""
model: str
"""The name of the model that generated the message."""
stopReason: StopReason | None = None
"""The reason why sampling stopped, if known."""
"""
The reason why sampling stopped, if known.
'toolUse' indicates the model wants to use a tool.
"""


class ResourceTemplateReference(BaseModel):
Expand Down
12 changes: 10 additions & 2 deletions tests/shared/test_streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,10 @@ async def handle_call_tool(name: str, args: dict[str, Any]) -> list[TextContent]
)

# Return the sampling result in the tool response
response = sampling_result.content.text if sampling_result.content.type == "text" else None
content = (
sampling_result.content[0] if isinstance(sampling_result.content, list) else sampling_result.content
)
response = content.text if content.type == "text" else None
return [
TextContent(
type="text",
Expand Down Expand Up @@ -1239,7 +1242,12 @@ async def sampling_callback(
nonlocal sampling_callback_invoked, captured_message_params
sampling_callback_invoked = True
captured_message_params = params
message_received = params.messages[0].content.text if params.messages[0].content.type == "text" else None
msg_content = (
params.messages[0].content[0]
if isinstance(params.messages[0].content, list)
else params.messages[0].content
)
message_received = msg_content.text if msg_content.type == "text" else None

return types.CreateMessageResult(
role="assistant",
Expand Down
Loading