Skip to content

Latest commit

 

History

History
548 lines (469 loc) · 21.1 KB

File metadata and controls

548 lines (469 loc) · 21.1 KB

Support for MCP client features

  1. Roots
    1. Roots list changed
  2. Sampling
  3. Elicitation
    1. Schema defaults and enums
    2. Completing a URL elicitation
  4. Multi Round-Trip Requests
  5. Capabilities
    1. Capability inference
    2. Explicit capabilities
    3. Extensions

Roots

Note: The roots feature is deprecated as of protocol version 2026-07-28 (SEP-2577). It remains fully functional during the deprecation window (at least twelve months). The SDK continues to support roots for compatibility. New code should pass paths via tool parameters, resource URIs, or configuration instead.

MCP allows clients to specify a set of filesystem "roots". The SDK supports this as follows:

Client-side: The SDK client always has the roots.listChanged capability. To add roots to a client, use the Client.AddRoots and Client.RemoveRoots methods. If any servers are already connected to the client, a call to AddRoot or RemoveRoots will result in a notifications/roots/list_changed notification to each connected server.

Server-side: To query roots from the server, use the ServerSession.ListRoots method. To receive notifications about root changes, set ServerOptions.RootsListChangedHandler. For protocol versions 2026-07-28 and later, ListRoots requests are delivered via the Multi Round-Trip Requests pattern.

func Example_roots() {
	ctx := context.Background()

	// Create a client with two roots.
	c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
	c.AddRoots(&mcp.Root{URI: "file://a"}, &mcp.Root{URI: "file://b"})

	// Create a server with a tool that requests roots via the multi round-trip
	// pattern (SEP-2322): server-to-client requests are no longer sent as
	// standalone JSON-RPC calls on protocol version >= 2026-07-28.
	s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
	mcp.AddTool(s, &mcp.Tool{Name: "roots"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
		if len(req.Params.InputResponses) == 0 {
			return &mcp.CallToolResult{
				InputRequests: mcp.InputRequestMap{"roots": &mcp.ListRootsParams{}},
			}, nil, nil
		}
		rootList := req.Params.InputResponses["roots"].(*mcp.ListRootsResult)
		var roots []string
		for _, root := range rootList.Roots {
			roots = append(roots, root.URI)
		}
		fmt.Println(roots)
		return &mcp.CallToolResult{}, nil, nil
	})

	// Connect the server and client...
	t1, t2 := mcp.NewInMemoryTransports()
	serverSession, err := s.Connect(ctx, t1, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer serverSession.Close()

	clientSession, err := c.Connect(ctx, t2, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer clientSession.Close()

	// ...and call the tool. The client's multi round-trip driver fulfils the
	// embedded roots/list request and retries the call.
	if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "roots"}); err != nil {
		log.Fatal(err)
	}
	// Output: [file://a file://b]
}

Roots list changed

Client.AddRoots and Client.RemoveRoots notify every connected server that the list changed. Servers observe this through ServerOptions.RootsListChangedHandler; as with the server-side list-changed notifications, it reports only that something changed, so read the list back with ServerSession.ListRoots.

func Example_rootsListChanged() {
	ctx := context.Background()

	changed := make(chan struct{}, 2)
	s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
		RootsListChangedHandler: func(context.Context, *mcp.RootsListChangedRequest) {
			changed <- struct{}{}
		},
	})

	c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
	c.AddRoots(&mcp.Root{URI: "file:///project"})

	t1, t2 := mcp.NewInMemoryTransports()
	ss, err := s.Connect(ctx, t1, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer ss.Close()

	// ListRoots is a server-initiated request, so this session negotiates a
	// protocol version that still allows one.
	cs, err := c.Connect(ctx, t2, &mcp.ClientSessionOptions{ProtocolVersion: "2025-11-25"})
	if err != nil {
		log.Fatal(err)
	}
	defer cs.Close()

	// Roots added after the client connects notify every connected server.
	c.AddRoots(&mcp.Root{URI: "file:///scratch"})
	<-changed

	// The notification says only that the list changed, so read it back.
	res, err := ss.ListRoots(ctx, nil)
	if err != nil {
		log.Fatal(err)
	}
	for _, root := range res.Roots {
		fmt.Println(root.URI)
	}

	c.RemoveRoots("file:///scratch")
	<-changed
	fmt.Println("roots changed again")

	// Output:
	// file:///project
	// file:///scratch
	// roots changed again
}

Sampling

Note: The sampling feature is deprecated as of protocol version 2026-07-28 (SEP-2577). It remains fully functional during the deprecation window (at least twelve months). The SDK continues to support sampling for compatibility. Servers that need LLM completions should call LLM provider APIs directly.

Sampling is a way for servers to leverage the client's AI capabilities. It is implemented in the SDK as follows:

Client-side: To add the sampling capability to a client, set ClientOptions.CreateMessageHandler. This function is invoked whenever the server requests sampling.

Server-side: To use sampling from the server, call ServerSession.CreateMessage.

For protocol versions 2026-07-28 and later, sampling requests are delivered via the Multi Round-Trip Requests pattern.

func Example_sampling() {
	ctx := context.Background()

	// Create a client with a sampling handler.
	c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
		CreateMessageHandler: func(_ context.Context, req *mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) {
			return &mcp.CreateMessageResult{
				Content: &mcp.TextContent{
					Text: "would have created a message",
				},
			}, nil
		},
	})

	// Connect the server and client...
	ct, st := mcp.NewInMemoryTransports()
	// Create a server with a tool that requests sampling via the multi
	// round-trip pattern (SEP-2322): server-to-client requests are no longer
	// sent as standalone JSON-RPC calls on protocol version >= 2026-07-28.
	s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
	mcp.AddTool(s, &mcp.Tool{Name: "sample"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
		if len(req.Params.InputResponses) == 0 {
			return &mcp.CallToolResult{
				InputRequests: mcp.InputRequestMap{"msg": &mcp.CreateMessageParams{}},
			}, nil, nil
		}
		msg := req.Params.InputResponses["msg"].(*mcp.CreateMessageWithToolsResult)
		return &mcp.CallToolResult{Content: msg.Content}, nil, nil
	})
	session, err := s.Connect(ctx, st, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	clientSession, err := c.Connect(ctx, ct, nil)
	if err != nil {
		log.Fatal(err)
	}

	res, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "sample"})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Content[0].(*mcp.TextContent).Text)
	// Output: would have created a message
}

Elicitation

Elicitation allows servers to request user inputs. It is implemented in the SDK as follows:

Client-side: To add the elicitation capability to a client, set ClientOptions.ElicitationHandler. The elicitation handler must return a result that matches the requested schema; otherwise, elicitation returns an error. If your handler supports URL mode elicitation, you must declare that capability explicitly (see Capabilities)

Server-side: To use elicitation from the server, call ServerSession.Elicit.

For protocol versions 2026-07-28 and later, elicitation requests are delivered via the Multi Round-Trip Requests pattern.

func Example_elicitation() {
	ctx := context.Background()
	ct, st := mcp.NewInMemoryTransports()

	// Create a server with a tool that requests elicitation via the multi
	// round-trip pattern (SEP-2322): server-to-client requests are no longer
	// sent as standalone JSON-RPC calls on protocol version >= 2026-07-28.
	s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
	mcp.AddTool(s, &mcp.Tool{Name: "ask"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
		if len(req.Params.InputResponses) == 0 {
			return &mcp.CallToolResult{
				InputRequests: mcp.InputRequestMap{"input": &mcp.ElicitParams{
					Message: "This should fail",
					RequestedSchema: &jsonschema.Schema{
						Type: "object",
						Properties: map[string]*jsonschema.Schema{
							"test": {Type: "string"},
						},
					},
				}},
			}, nil, nil
		}
		res := req.Params.InputResponses["input"].(*mcp.ElicitResult)
		fmt.Println(res.Content["test"])
		return &mcp.CallToolResult{}, nil, nil
	})
	ss, err := s.Connect(ctx, st, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer ss.Close()

	c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
		ElicitationHandler: func(context.Context, *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
			return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"test": "value"}}, nil
		},
	})
	clientSession, err := c.Connect(ctx, ct, nil)
	if err != nil {
		log.Fatal(err)
	}
	if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "ask"}); err != nil {
		log.Fatal(err)
	}
	// Output: value
}

Schema defaults and enums

ElicitParams.RequestedSchema is a flat schema of primitive fields, which the client renders as a form. Two field keywords shape that form.

A Default (SEP-1034) prefills a field. When the user accepts without supplying it, the SDK fills the field in from the schema before the result reaches either side's caller — the client does so after its elicitation handler returns, and ServerSession.Elicit does so again on receipt. This is unconditional; there is no opt-in flag. Marking a defaulted field Required defeats it: accepted content is validated against the schema before defaults are applied, so an answer that omits the field is rejected rather than defaulted.

An Enum (SEP-1330) restricts a field to a fixed set of values, which the client renders as a choice. Enums are supported only on "string" fields; declaring one on another type is rejected. To label the choices, set the legacy enumNames keyword through Schema.Extra, with exactly one name per enum value — a mismatched length is rejected.

func Example_elicitationSchema() {
	ctx := context.Background()
	ct, st := mcp.NewInMemoryTransports()

	s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
	mcp.AddTool(s, &mcp.Tool{Name: "export_report"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
		if len(req.Params.InputResponses) == 0 {
			return &mcp.CallToolResult{
				InputRequests: mcp.InputRequestMap{"format": &mcp.ElicitParams{
					Message: "Export quarterly-sales as which format?",
					RequestedSchema: &jsonschema.Schema{
						Type: "object",
						Properties: map[string]*jsonschema.Schema{
							"format": {
								Type:    "string",
								Title:   "Format",
								Enum:    []any{"pdf", "csv"},
								Default: json.RawMessage(`"pdf"`),
								Extra:   map[string]any{"enumNames": []any{"PDF document", "CSV spreadsheet"}},
							},
						},
					},
				}},
			}, nil, nil
		}
		res := req.Params.InputResponses["format"].(*mcp.ElicitResult)
		return &mcp.CallToolResult{
			Content: []mcp.Content{&mcp.TextContent{Text: "Exported as " + res.Content["format"].(string)}},
		}, nil, nil
	})
	if _, err := s.Connect(ctx, st, nil); err != nil {
		log.Fatal(err)
	}

	// The user accepts without filling anything in.
	c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
		ElicitationHandler: func(context.Context, *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
			return &mcp.ElicitResult{Action: "accept", Content: map[string]any{}}, nil
		},
	})
	cs, err := c.Connect(ctx, ct, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer cs.Close()

	res, err := cs.CallTool(ctx, &mcp.CallToolParams{Name: "export_report"})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Content[0].(*mcp.TextContent).Text)
	// Output: Exported as pdf
}

Completing a URL elicitation

In URL mode the user finishes out of band, in a browser, so nothing in the elicitation result tells the client when they are done. The server signals that with ServerSession.NotifyElicitationComplete, passing the same ElicitationID the request carried; the client observes it through ClientOptions.ElicitationCompleteHandler. Send it from whatever endpoint the hosted flow redirects back to.

The notification matters most when a handler rejects a request with URLElicitationRequiredError: the client parks the original request until a notification names that ElicitationID, and then retries it automatically. Until one arrives, the client waits.

func Example_elicitationComplete() {
	ctx := context.Background()
	ct, st := mcp.NewInMemoryTransports()

	s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
	ss, err := s.Connect(ctx, st, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer ss.Close()

	done := make(chan struct{})
	c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
		Capabilities: &mcp.ClientCapabilities{
			Elicitation: &mcp.ElicitationCapabilities{URL: &mcp.URLElicitationCapabilities{}},
		},
		ElicitationHandler: func(_ context.Context, req *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
			fmt.Println("opening", req.Params.URL)
			return &mcp.ElicitResult{Action: "accept"}, nil
		},
		ElicitationCompleteHandler: func(_ context.Context, req *mcp.ElicitationCompleteNotificationRequest) {
			fmt.Println("flow finished:", req.Params.ElicitationID)
			close(done)
		},
	})
	cs, err := c.Connect(ctx, ct, &mcp.ClientSessionOptions{ProtocolVersion: "2025-11-25"})
	if err != nil {
		log.Fatal(err)
	}
	defer cs.Close()

	const elicitationID = "connect-calendar-1"
	if _, err := ss.Elicit(ctx, &mcp.ElicitParams{
		Message:       "Grant calendar access",
		URL:           "https://calendar.example.com/consent?state=" + elicitationID,
		ElicitationID: elicitationID,
	}); err != nil {
		log.Fatal(err)
	}

	// The hosted page redirects back to the server, whose callback endpoint
	// signals that the user is done.
	if err := ss.NotifyElicitationComplete(ctx, &mcp.ElicitationCompleteParams{ElicitationID: elicitationID}); err != nil {
		log.Fatal(err)
	}
	<-done

	// Output:
	// opening https://calendar.example.com/consent?state=connect-calendar-1
	// flow finished: connect-calendar-1
}

Multi Round-Trip Requests

SEP-2322 introduces the MRTR pattern: server-to-client requests for sampling, elicitation, and roots are no longer issued as fresh JSON-RPC requests but are carried inside the in-flight reply of a tools/call, prompts/get, or resources/read. The client must respond by retrying the original request with the produced responses.

The SDK installs clientMultiRoundTripMiddleware for every client by default. The middleware:

  1. Inspects each tools/call/prompts/get/resources/read reply.
  2. If the result's NeedsInput() is true, fans out the InputRequests map concurrently, calling the configured handler for each (elicit, createMessage/createMessageWithTools, or listRoots).
  3. Threads the server-supplied opaque RequestState back unchanged.
  4. Retries the original request with the responses set, repeating until the result no longer needs input.

The middleware is enabled by default. To opt out, set ClientOptions.MultiRoundTrip.Disabled = true; the client will then surface input-required results directly to the caller (the returned CallToolResult, GetPromptResult, or ReadResourceResult will report NeedsInput() == true and expose the server's InputRequests and opaque RequestState). Your code must fulfil each request and re-issue the original call with InputResponses set and RequestState echoed back.

For legacy (<= 2025-11-25) servers, the SDK transparently sends server requests on the legacy server-initiated channel; the MRTR machinery is a no-op in that direction. For legacy clients talking to MRTR-style servers, the server SDK applies the inverse compatibility shim — see the server-side documentation.

Capabilities

Client capabilities are advertised to servers during the initialization handshake. By default, the SDK advertises the logging capability. Additional capabilities are automatically added when server features are added (e.g. via AddTool), or when handlers are set in the ServerOptions struct (e.g., setting CompletionHandler adds the completions capability), or may be configured explicitly.

Capability inference

When handlers are set on ClientOptions (e.g., CreateMessageHandler or ElicitationHandler), the corresponding capability is automatically added if not already present, with a default configuration.

For elicitation, if the handler is set but no Capabilities.Elicitation is specified, the client defaults to form elicitation. To enable URL elicitation or both modes, configure Capabilities.Elicitation explicitly.

See the ClientCapabilities documentation for further details on inference.

Explicit capabilities

To explicitly declare capabilities, or to override the default inferred capability, set ClientOptions.Capabilities. This sets the initial client capabilities, before any capabilities are added based on configured handlers. If a capability is already present in Capabilities, adding a handler will not change its configuration.

This allows you to:

  • Disable default capabilities: Pass an empty &ClientCapabilities{} to disable all defaults, including roots.
  • Disable listChanged notifications: Set ListChanged: false on a capability to prevent the client from sending list-changed notifications when roots are added or removed.
  • Configure elicitation modes: Specify which elicitation modes (form, URL) the client supports.
// Configure elicitation modes and disable roots.
client := mcp.NewClient(impl, &mcp.ClientOptions{
    Capabilities: &mcp.ClientCapabilities{
        Elicitation: &mcp.ElicitationCapabilities{
            Form: &mcp.FormElicitationCapabilities{},
            URL:  &mcp.URLElicitationCapabilities{},
        },
    },
    ElicitationHandler: handler,
})

Extensions

SEP-2133 adds an extensions map to ClientCapabilities and ServerCapabilities so that optional capabilities outside the core protocol can be declared on the wire. Keys are namespaced as "{vendor-prefix}/{extension-name}"; values are per-extension settings objects.