go-sdk/docs/client.md
Robert Findley 6dbf6c6450
all: minor cleanup (#545)
Also fix a flaky test case in TestStreamableServerTransport.
2025-09-30 15:53:00 -04:00

5.6 KiB

Support for MCP client features

  1. Roots
  2. Sampling
  3. Elicitation

Roots

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.

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

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

	// Now create a server with a handler to receive notifications about roots.
	rootsChanged := make(chan struct{})
	handleRootsChanged := func(ctx context.Context, req *mcp.RootsListChangedRequest) {
		rootList, err := req.Session.ListRoots(ctx, nil)
		if err != nil {
			log.Fatal(err)
		}
		var roots []string
		for _, root := range rootList.Roots {
			roots = append(roots, root.URI)
		}
		fmt.Println(roots)
		close(rootsChanged)
	}
	s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
		RootsListChangedHandler: handleRootsChanged,
	})

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

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

	// ...and add a root. The server is notified about the change.
	c.AddRoots(&mcp.Root{URI: "file://b"})
	<-rootsChanged
	// Output: [file://a file://b]
}

Sampling

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.

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()
	s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
	session, err := s.Connect(ctx, st, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

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

	msg, err := session.CreateMessage(ctx, &mcp.CreateMessageParams{})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(msg.Content.(*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.

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

func Example_elicitation() {
	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()

	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
		},
	})
	if _, err := c.Connect(ctx, ct, nil); err != nil {
		log.Fatal(err)
	}
	res, err := ss.Elicit(ctx, &mcp.ElicitParams{
		Message: "This should fail",
		RequestedSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"test": {Type: "string"},
			},
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Content["test"])
	// Output: value
}