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:
Robert Findley 2026-02-17 06:07:08 -05:00 • committed by GitHub
commit 3cf9f9908d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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