mcp: implement sampling with tools (#699)
Summary Add support for tool use within sampling requests, as described in the MCP 2025-11-25 spec's sampling.tools capability. New types Content types: - ToolUseContent — represents a tool invocation request from the assistant - ToolResultContent — represents the result of a tool invocation, with nested []Content for the result blocks Capability types: - SamplingCapabilities gains Tools and Context sub-fields - SamplingToolsCapabilities, SamplingContextCapabilities Tool-enabled sampling (parallel tool calls): - CreateMessageWithToolsParams with SamplingMessageV2 (supports array content) - CreateMessageWithToolsResult (supports array content) - ToolChoice — controls tool invocation mode (auto, required, none) - ServerSession.CreateMessageWithTools - ClientOptions.CreateMessageWithToolsHandler Design Following the TypeScript SDK's pattern, tool-enabled sampling uses separate types from basic sampling to avoid breaking the existing API. The basic CreateMessage/CreateMessageResult path is unchanged. Both paths share the same wire method (sampling/createMessage) and go through the method info table. The table uses the broader CreateMessageWithToolsResult type internally; CreateMessage downconverts (erroring if multiple content blocks are returned). This is documented as a rough edge to unify in v2. Setting CreateMessageWithToolsHandler automatically infers the sampling.tools capability. It is a panic to set both CreateMessageHandler and CreateMessageWithToolsHandler. References - https://modelcontextprotocol.io/specification/2025-11-25/client/sampling - TypeScript SDK: CreateMessageResultWithToolsSchema / CreateMessageRequestParamsWithTools --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
14869a46ee
commit
3cf9f9908d
1 changed files with 1641 additions and 38 deletions
185
mcp/content.go
185
mcp/content.go
|
|
@ -13,7 +13,10 @@ import (
|
|||
)
|
||||
|
||||
// A Content is a [TextContent], [ImageContent], [AudioContent],
|
||||
// [ResourceLink], or [EmbeddedResource].
|
||||
// [ResourceLink], [EmbeddedResource], [ToolUseContent], or [ToolResultContent].
|
||||
//
|
||||
// Note: [ToolUseContent] and [ToolResultContent] are only valid in sampling
|
||||
// message contexts (CreateMessageParams/CreateMessageResult).
|
||||
type Content interface {
|
||||
MarshalJSON() ([]byte, error)
|
||||
fromWire(*wireContent)
|
||||
|
|
@ -182,6 +185,104 @@ func (c *EmbeddedResource) fromWire(wire *wireContent) {
|
|||
c.Annotations = wire.Annotations
|
||||
}
|
||||
|
||||
// ToolUseContent represents a request from the assistant to invoke a tool.
|
||||
// This content type is only valid in sampling messages.
|
||||
type ToolUseContent struct {
|
||||
// ID is a unique identifier for this tool use, used to match with ToolResultContent.
|
||||
ID string
|
||||
// Name is the name of the tool to invoke.
|
||||
Name string
|
||||
// Input contains the tool arguments as a JSON object.
|
||||
Input map[string]any
|
||||
Meta Meta
|
||||
}
|
||||
|
||||
func (c *ToolUseContent) MarshalJSON() ([]byte, error) {
|
||||
input := c.Input
|
||||
if input == nil {
|
||||
input = map[string]any{}
|
||||
}
|
||||
wire := struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]any `json:"input"`
|
||||
Meta Meta `json:"_meta,omitempty"`
|
||||
}{
|
||||
Type: "tool_use",
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
Input: input,
|
||||
Meta: c.Meta,
|
||||
}
|
||||
return json.Marshal(wire)
|
||||
}
|
||||
|
||||
func (c *ToolUseContent) fromWire(wire *wireContent) {
|
||||
c.ID = wire.ID
|
||||
c.Name = wire.Name
|
||||
c.Input = wire.Input
|
||||
c.Meta = wire.Meta
|
||||
}
|
||||
|
||||
// ToolResultContent represents the result of a tool invocation.
|
||||
// This content type is only valid in sampling messages with role "user".
|
||||
type ToolResultContent struct {
|
||||
// ToolUseID references the ID from the corresponding ToolUseContent.
|
||||
ToolUseID string
|
||||
// Content holds the unstructured result of the tool call.
|
||||
Content []Content
|
||||
// StructuredContent holds an optional structured result as a JSON object.
|
||||
StructuredContent any
|
||||
// IsError indicates whether the tool call ended in an error.
|
||||
IsError bool
|
||||
Meta Meta
|
||||
}
|
||||
|
||||
func (c *ToolResultContent) MarshalJSON() ([]byte, error) {
|
||||
// Marshal nested content
|
||||
var contentWire []*wireContent
|
||||
for _, content := range c.Content {
|
||||
data, err := content.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var w wireContent
|
||||
if err := json.Unmarshal(data, &w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contentWire = append(contentWire, &w)
|
||||
}
|
||||
if contentWire == nil {
|
||||
contentWire = []*wireContent{} // avoid JSON null
|
||||
}
|
||||
|
||||
wire := struct {
|
||||
Type string `json:"type"`
|
||||
ToolUseID string `json:"toolUseId"`
|
||||
Content []*wireContent `json:"content"`
|
||||
StructuredContent any `json:"structuredContent,omitempty"`
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
Meta Meta `json:"_meta,omitempty"`
|
||||
}{
|
||||
Type: "tool_result",
|
||||
ToolUseID: c.ToolUseID,
|
||||
Content: contentWire,
|
||||
StructuredContent: c.StructuredContent,
|
||||
IsError: c.IsError,
|
||||
Meta: c.Meta,
|
||||
}
|
||||
return json.Marshal(wire)
|
||||
}
|
||||
|
||||
func (c *ToolResultContent) fromWire(wire *wireContent) {
|
||||
c.ToolUseID = wire.ToolUseID
|
||||
c.StructuredContent = wire.StructuredContent
|
||||
c.IsError = wire.IsError
|
||||
c.Meta = wire.Meta
|
||||
// Content is handled separately in contentFromWire due to nested content
|
||||
}
|
||||
|
||||
// ResourceContents contains the contents of a specific resource or
|
||||
// sub-resource.
|
||||
type ResourceContents struct {
|
||||
|
|
@ -194,28 +295,55 @@ type ResourceContents struct {
|
|||
|
||||
// wireContent is the wire format for content.
|
||||
// It represents the protocol types TextContent, ImageContent, AudioContent,
|
||||
// ResourceLink, and EmbeddedResource.
|
||||
// ResourceLink, EmbeddedResource, ToolUseContent, and ToolResultContent.
|
||||
// The Type field distinguishes them. In the protocol, each type has a constant
|
||||
// value for the field.
|
||||
// At most one of Text, Data, Resource, and URI is non-zero.
|
||||
type wireContent struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
MIMEType string `json:"mimeType,omitempty"`
|
||||
Data []byte `json:"data,omitempty"`
|
||||
Resource *ResourceContents `json:"resource,omitempty"`
|
||||
URI string `json:"uri,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Size *int64 `json:"size,omitempty"`
|
||||
Meta Meta `json:"_meta,omitempty"`
|
||||
Annotations *Annotations `json:"annotations,omitempty"`
|
||||
Icons []Icon `json:"icons,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"` // TextContent
|
||||
MIMEType string `json:"mimeType,omitempty"` // ImageContent, AudioContent, ResourceLink
|
||||
Data []byte `json:"data,omitempty"` // ImageContent, AudioContent
|
||||
Resource *ResourceContents `json:"resource,omitempty"` // EmbeddedResource
|
||||
URI string `json:"uri,omitempty"` // ResourceLink
|
||||
Name string `json:"name,omitempty"` // ResourceLink, ToolUseContent
|
||||
Title string `json:"title,omitempty"` // ResourceLink
|
||||
Description string `json:"description,omitempty"` // ResourceLink
|
||||
Size *int64 `json:"size,omitempty"` // ResourceLink
|
||||
Meta Meta `json:"_meta,omitempty"` // all types
|
||||
Annotations *Annotations `json:"annotations,omitempty"` // all types except ToolUseContent, ToolResultContent
|
||||
Icons []Icon `json:"icons,omitempty"` // ResourceLink
|
||||
ID string `json:"id,omitempty"` // ToolUseContent
|
||||
Input map[string]any `json:"input,omitempty"` // ToolUseContent
|
||||
ToolUseID string `json:"toolUseId,omitempty"` // ToolResultContent
|
||||
NestedContent []*wireContent `json:"content,omitempty"` // ToolResultContent
|
||||
StructuredContent any `json:"structuredContent,omitempty"` // ToolResultContent
|
||||
IsError bool `json:"isError,omitempty"` // ToolResultContent
|
||||
}
|
||||
|
||||
// unmarshalContent unmarshals JSON that is either a single content object or
|
||||
// an array of content objects. A single object is wrapped in a one-element slice.
|
||||
func unmarshalContent(raw json.RawMessage, allow map[string]bool) ([]Content, error) {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return nil, fmt.Errorf("nil content")
|
||||
}
|
||||
// Try array first, then fall back to single object.
|
||||
var wires []*wireContent
|
||||
if err := json.Unmarshal(raw, &wires); err == nil {
|
||||
return contentsFromWire(wires, allow)
|
||||
}
|
||||
var wire wireContent
|
||||
if err := json.Unmarshal(raw, &wire); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := contentFromWire(&wire, allow)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []Content{c}, nil
|
||||
}
|
||||
|
||||
func contentsFromWire(wires []*wireContent, allow map[string]bool) ([]Content, error) {
|
||||
var blocks []Content
|
||||
blocks := make([]Content, 0, len(wires))
|
||||
for _, wire := range wires {
|
||||
block, err := contentFromWire(wire, allow)
|
||||
if err != nil {
|
||||
|
|
@ -254,6 +382,27 @@ func contentFromWire(wire *wireContent, allow map[string]bool) (Content, error)
|
|||
v := new(EmbeddedResource)
|
||||
v.fromWire(wire)
|
||||
return v, nil
|
||||
case "tool_use":
|
||||
v := new(ToolUseContent)
|
||||
v.fromWire(wire)
|
||||
return v, nil
|
||||
case "tool_result":
|
||||
v := new(ToolResultContent)
|
||||
v.fromWire(wire)
|
||||
// Handle nested content - tool_result content can contain text, image, audio,
|
||||
// resource_link, and resource (same as CallToolResult.content)
|
||||
if wire.NestedContent != nil {
|
||||
toolResultContentAllow := map[string]bool{
|
||||
"text": true, "image": true, "audio": true,
|
||||
"resource_link": true, "resource": true,
|
||||
}
|
||||
nestedContent, err := contentsFromWire(wire.NestedContent, toolResultContentAllow)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tool_result nested content: %w", err)
|
||||
}
|
||||
v.Content = nestedContent
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("internal error: unrecognized content type %s", wire.Type)
|
||||
return nil, fmt.Errorf("unrecognized content type %q", wire.Type)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue