Implement SEP-1577 - Sampling With Tools (#1594)
Co-authored-by: Felix Weinberger <fweinberger@anthropic.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c51936f61f
commit
71c475588f
3 changed files with 674 additions and 19 deletions
|
|
@ -41,7 +41,11 @@ from .types import (
|
|||
ResourcesCapability,
|
||||
ResourceUpdatedNotification,
|
||||
RootsCapability,
|
||||
SamplingCapability,
|
||||
SamplingContextCapability,
|
||||
SamplingMessage,
|
||||
SamplingMessageContentBlock,
|
||||
SamplingToolsCapability,
|
||||
ServerCapabilities,
|
||||
ServerNotification,
|
||||
ServerRequest,
|
||||
|
|
@ -50,7 +54,10 @@ from .types import (
|
|||
StopReason,
|
||||
SubscribeRequest,
|
||||
Tool,
|
||||
ToolChoice,
|
||||
ToolResultContent,
|
||||
ToolsCapability,
|
||||
ToolUseContent,
|
||||
UnsubscribeRequest,
|
||||
)
|
||||
from .types import (
|
||||
|
|
@ -65,6 +72,7 @@ __all__ = [
|
|||
"ClientResult",
|
||||
"ClientSession",
|
||||
"ClientSessionGroup",
|
||||
"CompleteRequest",
|
||||
"CreateMessageRequest",
|
||||
"CreateMessageResult",
|
||||
"ErrorData",
|
||||
|
|
@ -77,6 +85,7 @@ __all__ = [
|
|||
"InitializedNotification",
|
||||
"JSONRPCError",
|
||||
"JSONRPCRequest",
|
||||
"JSONRPCResponse",
|
||||
"ListPromptsRequest",
|
||||
"ListPromptsResult",
|
||||
"ListResourcesRequest",
|
||||
|
|
@ -91,12 +100,16 @@ __all__ = [
|
|||
"PromptsCapability",
|
||||
"ReadResourceRequest",
|
||||
"ReadResourceResult",
|
||||
"Resource",
|
||||
"ResourcesCapability",
|
||||
"ResourceUpdatedNotification",
|
||||
"Resource",
|
||||
"RootsCapability",
|
||||
"SamplingCapability",
|
||||
"SamplingContextCapability",
|
||||
"SamplingMessage",
|
||||
"SamplingMessageContentBlock",
|
||||
"SamplingRole",
|
||||
"SamplingToolsCapability",
|
||||
"ServerCapabilities",
|
||||
"ServerNotification",
|
||||
"ServerRequest",
|
||||
|
|
@ -107,10 +120,11 @@ __all__ = [
|
|||
"StopReason",
|
||||
"SubscribeRequest",
|
||||
"Tool",
|
||||
"ToolChoice",
|
||||
"ToolResultContent",
|
||||
"ToolsCapability",
|
||||
"ToolUseContent",
|
||||
"UnsubscribeRequest",
|
||||
"stdio_client",
|
||||
"stdio_server",
|
||||
"CompleteRequest",
|
||||
"JSONRPCResponse",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ from pydantic import AnyUrl
|
|||
|
||||
import mcp.types as types
|
||||
from mcp.server.models import InitializationOptions
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.message import ServerMessageMetadata, SessionMessage
|
||||
from mcp.shared.session import (
|
||||
BaseSession,
|
||||
|
|
@ -120,6 +121,12 @@ class ServerSession(
|
|||
if capability.sampling is not None:
|
||||
if client_caps.sampling is None:
|
||||
return False
|
||||
if capability.sampling.context is not None:
|
||||
if client_caps.sampling.context is None:
|
||||
return False
|
||||
if capability.sampling.tools is not None:
|
||||
if client_caps.sampling.tools is None:
|
||||
return False
|
||||
|
||||
if capability.elicitation is not None:
|
||||
if client_caps.elicitation is None:
|
||||
|
|
@ -223,9 +230,75 @@ class ServerSession(
|
|||
stop_sequences: list[str] | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
model_preferences: types.ModelPreferences | None = None,
|
||||
tools: list[types.Tool] | None = None,
|
||||
tool_choice: types.ToolChoice | None = None,
|
||||
related_request_id: types.RequestId | None = None,
|
||||
) -> types.CreateMessageResult:
|
||||
"""Send a sampling/create_message request."""
|
||||
"""Send a sampling/create_message request.
|
||||
|
||||
Args:
|
||||
messages: The conversation messages to send.
|
||||
max_tokens: Maximum number of tokens to generate.
|
||||
system_prompt: Optional system prompt.
|
||||
include_context: Optional context inclusion setting.
|
||||
Should only be set to "thisServer" or "allServers"
|
||||
if the client has sampling.context capability.
|
||||
temperature: Optional sampling temperature.
|
||||
stop_sequences: Optional stop sequences.
|
||||
metadata: Optional metadata to pass through to the LLM provider.
|
||||
model_preferences: Optional model selection preferences.
|
||||
tools: Optional list of tools the LLM can use during sampling.
|
||||
Requires client to have sampling.tools capability.
|
||||
tool_choice: Optional control over tool usage behavior.
|
||||
Requires client to have sampling.tools capability.
|
||||
related_request_id: Optional ID of a related request.
|
||||
|
||||
Returns:
|
||||
The sampling result from the client.
|
||||
|
||||
Raises:
|
||||
McpError: If tool_use or tool_result blocks are misused when tools are provided.
|
||||
"""
|
||||
|
||||
if tools is not None or tool_choice is not None:
|
||||
has_tools_cap = self.check_client_capability(
|
||||
types.ClientCapabilities(sampling=types.SamplingCapability(tools=types.SamplingToolsCapability()))
|
||||
)
|
||||
if not has_tools_cap:
|
||||
raise McpError(
|
||||
types.ErrorData(
|
||||
code=types.INVALID_PARAMS,
|
||||
message="Client does not support sampling tools capability",
|
||||
)
|
||||
)
|
||||
|
||||
# Validate tool_use/tool_result message structure per SEP-1577:
|
||||
# https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1577
|
||||
# This validation runs regardless of whether `tools` is in this request,
|
||||
# since a tool loop continuation may omit `tools` while still containing
|
||||
# tool_result content that must match previous tool_use.
|
||||
if messages:
|
||||
last_content = messages[-1].content_as_list
|
||||
has_tool_results = any(c.type == "tool_result" for c in last_content)
|
||||
|
||||
previous_content = messages[-2].content_as_list if len(messages) >= 2 else None
|
||||
has_previous_tool_use = previous_content and any(c.type == "tool_use" for c in previous_content)
|
||||
|
||||
if has_tool_results:
|
||||
# Per spec: "SamplingMessage with tool result content blocks
|
||||
# MUST NOT contain other content types."
|
||||
if any(c.type != "tool_result" for c in last_content):
|
||||
raise ValueError("The last message must contain only tool_result content if any is present")
|
||||
if previous_content is None:
|
||||
raise ValueError("tool_result requires a previous message containing tool_use")
|
||||
if not has_previous_tool_use:
|
||||
raise ValueError("tool_result blocks do not match any tool_use in the previous message")
|
||||
if has_previous_tool_use and previous_content:
|
||||
tool_use_ids = {c.id for c in previous_content if c.type == "tool_use"}
|
||||
tool_result_ids = {c.toolUseId for c in last_content if c.type == "tool_result"}
|
||||
if tool_use_ids != tool_result_ids:
|
||||
raise ValueError("ids of tool_result blocks and tool_use blocks from previous message do not match")
|
||||
|
||||
return await self.send_request(
|
||||
request=types.ServerRequest(
|
||||
types.CreateMessageRequest(
|
||||
|
|
@ -238,6 +311,8 @@ class ServerSession(
|
|||
stopSequences=stop_sequences,
|
||||
metadata=metadata,
|
||||
modelPreferences=model_preferences,
|
||||
tools=tools,
|
||||
toolChoice=tool_choice,
|
||||
),
|
||||
)
|
||||
),
|
||||
|
|
|
|||
178
src/mcp/types.py
178
src/mcp/types.py
|
|
@ -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")
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -742,13 +779,97 @@ 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["ContentBlock"] = []
|
||||
"""
|
||||
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")
|
||||
|
||||
@property
|
||||
def content_as_list(self) -> list[SamplingMessageContentBlock]:
|
||||
"""Returns the content as a list of content blocks, regardless of whether
|
||||
it was originally a single block or a list."""
|
||||
return self.content if isinstance(self.content, list) else [self.content]
|
||||
|
||||
|
||||
class EmbeddedResource(BaseModel):
|
||||
"""
|
||||
|
|
@ -1035,6 +1156,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."""
|
||||
|
||||
|
|
@ -1057,6 +1197,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")
|
||||
|
||||
|
||||
|
|
@ -1067,18 +1217,32 @@ 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.
|
||||
"""
|
||||
|
||||
@property
|
||||
def content_as_list(self) -> list[SamplingMessageContentBlock]:
|
||||
"""Returns the content as a list of content blocks, regardless of whether
|
||||
it was originally a single block or a list."""
|
||||
return self.content if isinstance(self.content, list) else [self.content]
|
||||
|
||||
|
||||
class ResourceTemplateReference(BaseModel):
|
||||
|
|
|
|||
Loading…
Reference in a new issue