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
|
|
@ -52,6 +52,9 @@ func NewClient(impl *Implementation, options *ClientOptions) *Client {
|
|||
}
|
||||
options = nil // prevent reuse
|
||||
|
||||
if opts.CreateMessageHandler != nil && opts.CreateMessageWithToolsHandler != nil {
|
||||
panic("cannot set both CreateMessageHandler and CreateMessageWithToolsHandler; use CreateMessageWithToolsHandler for tool support, or CreateMessageHandler for basic sampling")
|
||||
}
|
||||
if opts.Logger == nil { // ensure we have a logger
|
||||
opts.Logger = ensureLogger(nil)
|
||||
}
|
||||
|
|
@ -77,6 +80,19 @@ type ClientOptions struct {
|
|||
// non nil value for [ClientCapabilities.Sampling], that value overrides the
|
||||
// inferred capability.
|
||||
CreateMessageHandler func(context.Context, *CreateMessageRequest) (*CreateMessageResult, error)
|
||||
// CreateMessageWithToolsHandler handles incoming sampling/createMessage
|
||||
// requests that may involve tool use. It returns
|
||||
// [CreateMessageWithToolsResult], which supports array content for parallel
|
||||
// tool calls.
|
||||
//
|
||||
// Setting this handler causes the client to advertise the sampling
|
||||
// capability with tools support (sampling.tools). As with
|
||||
// [CreateMessageHandler], [ClientOptions.Capabilities].Sampling overrides
|
||||
// the inferred capability.
|
||||
//
|
||||
// It is a panic to set both CreateMessageHandler and
|
||||
// CreateMessageWithToolsHandler.
|
||||
CreateMessageWithToolsHandler func(context.Context, *CreateMessageWithToolsRequest) (*CreateMessageWithToolsResult, error)
|
||||
// ElicitationHandler handles incoming requests for elicitation/create.
|
||||
//
|
||||
// Setting ElicitationHandler to a non-nil value automatically causes the
|
||||
|
|
@ -109,7 +125,16 @@ type ClientOptions struct {
|
|||
// are set in the Capabilities field, their values override the inferred
|
||||
// value.
|
||||
//
|
||||
// For example, to to configure elicitation modes:
|
||||
// For example, to advertise sampling with tools and context support:
|
||||
//
|
||||
// Capabilities: &ClientCapabilities{
|
||||
// Sampling: &SamplingCapabilities{
|
||||
// Tools: &SamplingToolsCapabilities{},
|
||||
// Context: &SamplingContextCapabilities{},
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// Or to configure elicitation modes:
|
||||
//
|
||||
// Capabilities: &ClientCapabilities{
|
||||
// Elicitation: &ElicitationCapabilities{
|
||||
|
|
@ -119,8 +144,7 @@ type ClientOptions struct {
|
|||
// }
|
||||
//
|
||||
// Conversely, if Capabilities does not set a field (for example, if the
|
||||
// Elicitation field is nil), the inferred elicitation capability will be
|
||||
// used.
|
||||
// Elicitation field is nil), the inferred capability will be used.
|
||||
Capabilities *ClientCapabilities
|
||||
// ElicitationCompleteHandler handles incoming notifications for notifications/elicitation/complete.
|
||||
ElicitationCompleteHandler func(context.Context, *ElicitationCompleteNotificationRequest)
|
||||
|
|
@ -198,10 +222,13 @@ func (c *Client) capabilities(protocolVersion string) *ClientCapabilities {
|
|||
caps.Roots = *caps.RootsV2
|
||||
}
|
||||
|
||||
// Augment with sampling capability if handler is set.
|
||||
if c.opts.CreateMessageHandler != nil {
|
||||
// Augment with sampling capability if a handler is set.
|
||||
if c.opts.CreateMessageHandler != nil || c.opts.CreateMessageWithToolsHandler != nil {
|
||||
if caps.Sampling == nil {
|
||||
caps.Sampling = &SamplingCapabilities{}
|
||||
if c.opts.CreateMessageWithToolsHandler != nil {
|
||||
caps.Sampling.Tools = &SamplingToolsCapabilities{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -453,12 +480,27 @@ func (c *Client) listRoots(_ context.Context, req *ListRootsRequest) (*ListRoots
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) createMessage(ctx context.Context, req *CreateMessageRequest) (*CreateMessageResult, error) {
|
||||
if c.opts.CreateMessageHandler == nil {
|
||||
// TODO: wrap or annotate this error? Pick a standard code?
|
||||
return nil, &jsonrpc.Error{Code: codeUnsupportedMethod, Message: "client does not support CreateMessage"}
|
||||
func (c *Client) createMessage(ctx context.Context, req *CreateMessageWithToolsRequest) (*CreateMessageWithToolsResult, error) {
|
||||
if c.opts.CreateMessageWithToolsHandler != nil {
|
||||
return c.opts.CreateMessageWithToolsHandler(ctx, req)
|
||||
}
|
||||
return c.opts.CreateMessageHandler(ctx, req)
|
||||
if c.opts.CreateMessageHandler != nil {
|
||||
// Downconvert the request for the basic handler.
|
||||
baseParams, err := req.Params.toBase()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseReq := &CreateMessageRequest{
|
||||
Session: req.Session,
|
||||
Params: baseParams,
|
||||
}
|
||||
res, err := c.opts.CreateMessageHandler(ctx, baseReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.toWithTools(), nil
|
||||
}
|
||||
return nil, &jsonrpc.Error{Code: codeUnsupportedMethod, Message: "client does not support CreateMessage"}
|
||||
}
|
||||
|
||||
// urlElicitationMiddleware returns middleware that automatically handles URL elicitation
|
||||
|
|
|
|||
Loading…
Reference in a new issue