Skip to content

Commit 4ded25a

Browse files
committed
Implement SEP-1577: Sampling With Tools
Add support for tool calling during sampling requests, enabling MCP servers to execute agentic workflows using client LLM capabilities. Key changes: - Add ToolUseContent type for assistant tool invocation requests - Add ToolResultContent type for tool execution results - Add ToolChoice type to control tool usage behavior - Add UserMessage and AssistantMessage types for role-specific messages - Extend SamplingMessage to support tool content (backward compatible) - Add SamplingToolsCapability for capability negotiation - Update CreateMessageRequestParams with tools and toolChoice fields - Update CreateMessageResult to support tool use content - Update StopReason to include "toolUse" value - Add comprehensive unit tests for all new types The implementation maintains backward compatibility by keeping SamplingMessage as a flexible BaseModel while adding more specific UserMessage and AssistantMessage types for type-safe tool interactions. All new types follow existing patterns: - Use Pydantic V2 BaseModel - Allow extra fields with ConfigDict(extra="allow") - Include proper docstrings and field descriptions - Support optional fields where appropriate Github-Issue: #1577
1 parent 9eae96a commit 4ded25a

File tree

3 files changed

+440
-12
lines changed

3 files changed

+440
-12
lines changed

src/mcp/__init__.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .server.stdio import stdio_server
66
from .shared.exceptions import McpError
77
from .types import (
8+
AssistantMessage,
89
CallToolRequest,
910
ClientCapabilities,
1011
ClientNotification,
@@ -42,6 +43,7 @@
4243
ResourceUpdatedNotification,
4344
RootsCapability,
4445
SamplingMessage,
46+
SamplingToolsCapability,
4547
ServerCapabilities,
4648
ServerNotification,
4749
ServerRequest,
@@ -50,21 +52,27 @@
5052
StopReason,
5153
SubscribeRequest,
5254
Tool,
55+
ToolChoice,
56+
ToolResultContent,
5357
ToolsCapability,
58+
ToolUseContent,
5459
UnsubscribeRequest,
60+
UserMessage,
5561
)
5662
from .types import (
5763
Role as SamplingRole,
5864
)
5965

6066
__all__ = [
67+
"AssistantMessage",
6168
"CallToolRequest",
6269
"ClientCapabilities",
6370
"ClientNotification",
6471
"ClientRequest",
6572
"ClientResult",
6673
"ClientSession",
6774
"ClientSessionGroup",
75+
"CompleteRequest",
6876
"CreateMessageRequest",
6977
"CreateMessageResult",
7078
"ErrorData",
@@ -77,6 +85,7 @@
7785
"InitializedNotification",
7886
"JSONRPCError",
7987
"JSONRPCRequest",
88+
"JSONRPCResponse",
8089
"ListPromptsRequest",
8190
"ListPromptsResult",
8291
"ListResourcesRequest",
@@ -91,12 +100,13 @@
91100
"PromptsCapability",
92101
"ReadResourceRequest",
93102
"ReadResourceResult",
103+
"Resource",
94104
"ResourcesCapability",
95105
"ResourceUpdatedNotification",
96-
"Resource",
97106
"RootsCapability",
98107
"SamplingMessage",
99108
"SamplingRole",
109+
"SamplingToolsCapability",
100110
"ServerCapabilities",
101111
"ServerNotification",
102112
"ServerRequest",
@@ -107,10 +117,12 @@
107117
"StopReason",
108118
"SubscribeRequest",
109119
"Tool",
120+
"ToolChoice",
121+
"ToolResultContent",
110122
"ToolsCapability",
123+
"ToolUseContent",
111124
"UnsubscribeRequest",
125+
"UserMessage",
112126
"stdio_client",
113127
"stdio_server",
114-
"CompleteRequest",
115-
"JSONRPCResponse",
116128
]

src/mcp/types.py

Lines changed: 215 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections.abc import Callable
2-
from typing import Annotated, Any, Generic, Literal, TypeAlias, TypeVar
2+
from typing import Annotated, Any, Generic, Literal, TypeAlias, TypeVar, Union
33

44
from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel
55
from pydantic.networks import AnyUrl, UrlConstraints
@@ -256,19 +256,52 @@ class SamplingCapability(BaseModel):
256256
model_config = ConfigDict(extra="allow")
257257

258258

259+
class SamplingToolsCapability(BaseModel):
260+
"""
261+
Capability indicating support for tool calling during sampling.
262+
263+
When present in ClientCapabilities.sampling, indicates that the client
264+
supports the tools and toolChoice parameters in sampling requests.
265+
"""
266+
267+
model_config = ConfigDict(extra="allow")
268+
269+
259270
class ElicitationCapability(BaseModel):
260271
"""Capability for elicitation operations."""
261272

262273
model_config = ConfigDict(extra="allow")
263274

264275

276+
class SamplingCapabilityNested(BaseModel):
277+
"""
278+
Nested structure for sampling capabilities, allowing fine-grained capability advertisement.
279+
"""
280+
281+
context: SamplingCapability | None = None
282+
"""
283+
Present if the client supports non-'none' values for includeContext parameter.
284+
SOFT-DEPRECATED: New implementations should use tools parameter instead.
285+
"""
286+
tools: SamplingToolsCapability | None = None
287+
"""
288+
Present if the client supports tools and toolChoice parameters in sampling requests.
289+
Presence indicates full tool calling support during sampling.
290+
"""
291+
model_config = ConfigDict(extra="allow")
292+
293+
265294
class ClientCapabilities(BaseModel):
266295
"""Capabilities a client may support."""
267296

268297
experimental: dict[str, dict[str, Any]] | None = None
269298
"""Experimental, non-standard capabilities that the client supports."""
270-
sampling: SamplingCapability | None = None
271-
"""Present if the client supports sampling from an LLM."""
299+
sampling: SamplingCapabilityNested | SamplingCapability | None = None
300+
"""
301+
Present if the client supports sampling from an LLM.
302+
Can be a structured object (SamplingCapabilityNested) with fine-grained capabilities,
303+
or a simple marker object (SamplingCapability) for backward compatibility.
304+
"""
272305
elicitation: ElicitationCapability | None = None
273306
"""Present if the client supports elicitation from the user."""
274307
roots: RootsCapability | None = None
@@ -742,11 +775,141 @@ class AudioContent(BaseModel):
742775
model_config = ConfigDict(extra="allow")
743776

744777

778+
class ToolUseContent(BaseModel):
779+
"""
780+
Content representing an assistant's request to invoke a tool.
781+
782+
This content type appears in assistant messages when the LLM wants to call a tool
783+
during sampling. The server should execute the tool and return a ToolResultContent
784+
in the next user message.
785+
"""
786+
787+
type: Literal["tool_use"]
788+
"""Discriminator for tool use content."""
789+
790+
name: str
791+
"""The name of the tool to invoke. Must match a tool name from the request's tools array."""
792+
793+
id: str
794+
"""Unique identifier for this tool call, used to correlate with ToolResultContent."""
795+
796+
input: dict[str, Any]
797+
"""Arguments to pass to the tool. Must conform to the tool's inputSchema."""
798+
799+
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
800+
"""
801+
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
802+
for notes on _meta usage.
803+
"""
804+
model_config = ConfigDict(extra="allow")
805+
806+
807+
class ToolResultContent(BaseModel):
808+
"""
809+
Content representing the result of a tool execution.
810+
811+
This content type appears in user messages as a response to a ToolUseContent
812+
from the assistant. It contains the output of executing the requested tool.
813+
"""
814+
815+
type: Literal["tool_result"]
816+
"""Discriminator for tool result content."""
817+
818+
toolUseId: str
819+
"""The unique identifier that corresponds to the tool call's id field."""
820+
821+
content: list[Union[TextContent, ImageContent, AudioContent, "EmbeddedResource"]] = []
822+
"""
823+
A list of content objects representing the tool result.
824+
Defaults to empty list if not provided.
825+
"""
826+
827+
structuredContent: dict[str, Any] | None = None
828+
"""
829+
Optional structured tool output that matches the tool's outputSchema (if defined).
830+
"""
831+
832+
isError: bool | None = None
833+
"""Whether the tool execution resulted in an error."""
834+
835+
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
836+
"""
837+
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
838+
for notes on _meta usage.
839+
"""
840+
model_config = ConfigDict(extra="allow")
841+
842+
745843
class SamplingMessage(BaseModel):
746-
"""Describes a message issued to or received from an LLM API."""
844+
"""
845+
Describes a message issued to or received from an LLM API.
846+
847+
For backward compatibility, this class accepts any role and any content type.
848+
For type-safe usage with tool calling, use UserMessage or AssistantMessage instead.
849+
"""
747850

748851
role: Role
749-
content: TextContent | ImageContent | AudioContent
852+
content: (
853+
TextContent
854+
| ImageContent
855+
| AudioContent
856+
| ToolUseContent
857+
| ToolResultContent
858+
| list[TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent]
859+
)
860+
"""
861+
Message content. Can be a single content block or an array of content blocks
862+
for multi-modal messages and tool interactions.
863+
"""
864+
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
865+
"""
866+
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
867+
for notes on _meta usage.
868+
"""
869+
model_config = ConfigDict(extra="allow")
870+
871+
872+
# Type aliases for role-specific messages
873+
UserMessageContent: TypeAlias = TextContent | ImageContent | AudioContent | ToolResultContent
874+
"""Content types allowed in user messages during sampling."""
875+
876+
AssistantMessageContent: TypeAlias = TextContent | ImageContent | AudioContent | ToolUseContent
877+
"""Content types allowed in assistant messages during sampling."""
878+
879+
880+
class UserMessage(BaseModel):
881+
"""
882+
A message from the user (server) in a sampling conversation.
883+
884+
User messages can include tool results in response to assistant tool use requests.
885+
"""
886+
887+
role: Literal["user"]
888+
content: UserMessageContent | list[UserMessageContent]
889+
"""Message content. Can be a single content block or an array for multi-modal messages."""
890+
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
891+
"""
892+
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
893+
for notes on _meta usage.
894+
"""
895+
model_config = ConfigDict(extra="allow")
896+
897+
898+
class AssistantMessage(BaseModel):
899+
"""
900+
A message from the assistant (LLM) in a sampling conversation.
901+
902+
Assistant messages can include tool use requests when the LLM wants to call tools.
903+
"""
904+
905+
role: Literal["assistant"]
906+
content: AssistantMessageContent | list[AssistantMessageContent]
907+
"""Message content. Can be a single content block or an array for multi-modal messages."""
908+
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
909+
"""
910+
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
911+
for notes on _meta usage.
912+
"""
750913
model_config = ConfigDict(extra="allow")
751914

752915

@@ -1035,6 +1198,31 @@ class ModelPreferences(BaseModel):
10351198
model_config = ConfigDict(extra="allow")
10361199

10371200

1201+
class ToolChoice(BaseModel):
1202+
"""
1203+
Controls tool usage behavior during sampling.
1204+
1205+
Allows the server to specify whether and how the LLM should use tools
1206+
in its response.
1207+
"""
1208+
1209+
mode: Literal["auto", "required", "none"] | None = None
1210+
"""
1211+
Controls when tools are used:
1212+
- "auto": Model decides whether to use tools (default)
1213+
- "required": Model MUST use at least one tool before completing
1214+
- "none": Model should not use tools
1215+
"""
1216+
1217+
disable_parallel_tool_use: bool | None = None
1218+
"""
1219+
If true, the model should not use multiple tools in parallel.
1220+
Some models may ignore this hint. Default: false (parallel use enabled).
1221+
"""
1222+
1223+
model_config = ConfigDict(extra="allow")
1224+
1225+
10381226
class CreateMessageRequestParams(RequestParams):
10391227
"""Parameters for creating a message."""
10401228

@@ -1057,6 +1245,16 @@ class CreateMessageRequestParams(RequestParams):
10571245
stopSequences: list[str] | None = None
10581246
metadata: dict[str, Any] | None = None
10591247
"""Optional metadata to pass through to the LLM provider."""
1248+
tools: list["Tool"] | None = None
1249+
"""
1250+
Tool definitions for the LLM to use during sampling.
1251+
Requires clientCapabilities.sampling.tools to be present.
1252+
"""
1253+
toolChoice: ToolChoice | None = None
1254+
"""
1255+
Controls tool usage behavior.
1256+
Requires clientCapabilities.sampling.tools and the tools parameter to be present.
1257+
"""
10601258
model_config = ConfigDict(extra="allow")
10611259

10621260

@@ -1067,18 +1265,26 @@ class CreateMessageRequest(Request[CreateMessageRequestParams, Literal["sampling
10671265
params: CreateMessageRequestParams
10681266

10691267

1070-
StopReason = Literal["endTurn", "stopSequence", "maxTokens"] | str
1268+
StopReason = Literal["endTurn", "stopSequence", "maxTokens", "toolUse"] | str
10711269

10721270

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

1076-
role: Role
1077-
content: TextContent | ImageContent | AudioContent
1274+
role: Literal["assistant"]
1275+
"""The role is always 'assistant' in responses from the LLM."""
1276+
content: AssistantMessageContent | list[AssistantMessageContent]
1277+
"""
1278+
Response content from the assistant. May be a single content block or an array.
1279+
May include ToolUseContent if stopReason is 'toolUse'.
1280+
"""
10781281
model: str
10791282
"""The name of the model that generated the message."""
10801283
stopReason: StopReason | None = None
1081-
"""The reason why sampling stopped, if known."""
1284+
"""
1285+
The reason why sampling stopped, if known.
1286+
'toolUse' indicates the model wants to use a tool.
1287+
"""
10821288

10831289

10841290
class ResourceTemplateReference(BaseModel):

0 commit comments

Comments
 (0)