Use pointers for the protocol's struct types, to future-proof against them getting large. Change-Id: I55b204080a41c8140539697ba264e502e912e613 Reviewed-on: https://go-review.googlesource.com/c/tools/+/672575 Reviewed-by: Robert Findley <rfindley@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
239 lines
6.9 KiB
Go
239 lines
6.9 KiB
Go
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
|
// Use of this source code is governed by an MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// TODO: consider passing Transport to NewClient and merging {Connection,Client}Options
|
|
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"sync"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
|
|
)
|
|
|
|
// A Client is an MCP client, which may be connected to an MCP server
|
|
// using the [Client.Start] method.
|
|
type Client struct {
|
|
name string
|
|
version string
|
|
transport Transport
|
|
opts ClientOptions
|
|
mu sync.Mutex
|
|
conn *jsonrpc2.Connection
|
|
roots *featureSet[*Root]
|
|
initializeResult *initializeResult
|
|
}
|
|
|
|
// NewClient creates a new Client.
|
|
//
|
|
// Use [Client.Start] to connect it to an MCP server.
|
|
//
|
|
// If non-nil, the provided options configure the Client.
|
|
func NewClient(name, version string, t Transport, opts *ClientOptions) *Client {
|
|
c := &Client{
|
|
name: name,
|
|
version: version,
|
|
transport: t,
|
|
roots: newFeatureSet(func(r *Root) string { return r.URI }),
|
|
}
|
|
if opts != nil {
|
|
c.opts = *opts
|
|
}
|
|
return c
|
|
}
|
|
|
|
// ClientOptions configures the behavior of the client.
|
|
type ClientOptions struct {
|
|
ConnectionOptions
|
|
}
|
|
|
|
// bind implements the binder[*Client] interface, so that Clients can
|
|
// be connected using [connect].
|
|
func (c *Client) bind(conn *jsonrpc2.Connection) *Client {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.conn = conn
|
|
return c
|
|
}
|
|
|
|
// disconnect implements the binder[*Client] interface, so that
|
|
// Clients can be connected using [connect].
|
|
func (c *Client) disconnect(*Client) {
|
|
// Do nothing. In particular, do not set conn to nil: it needs to exist so it can
|
|
// return an error.
|
|
}
|
|
|
|
// Start begins an MCP session by connecting the MCP client over its transport.
|
|
//
|
|
// Typically, it is the responsibility of the client to close the connection
|
|
// when it is no longer needed. However, if the connection is closed by the
|
|
// server, calls or notifications will return an error wrapping
|
|
// [ErrConnectionClosed].
|
|
func (c *Client) Start(ctx context.Context) (err error) {
|
|
defer func() {
|
|
if err != nil {
|
|
_ = c.Close()
|
|
}
|
|
}()
|
|
_, err = connect(ctx, c.transport, &c.opts.ConnectionOptions, c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
params := &initializeParams{
|
|
ClientInfo: &implementation{Name: c.name, Version: c.version},
|
|
}
|
|
if err := call(ctx, c.conn, "initialize", params, &c.initializeResult); err != nil {
|
|
return err
|
|
}
|
|
if err := c.conn.Notify(ctx, "notifications/initialized", &initializedParams{}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Close performs a graceful close of the connection, preventing new requests
|
|
// from being handled, and waiting for ongoing requests to return. Close then
|
|
// terminates the connection.
|
|
func (c *Client) Close() error {
|
|
return c.conn.Close()
|
|
}
|
|
|
|
// Wait waits for the connection to be closed by the server.
|
|
// Generally, clients should be responsible for closing the connection.
|
|
func (c *Client) Wait() error {
|
|
return c.conn.Wait()
|
|
}
|
|
|
|
// AddRoots adds the given roots to the client,
|
|
// replacing any with the same URIs,
|
|
// and notifies any connected servers.
|
|
// TODO: notification
|
|
func (c *Client) AddRoots(roots ...*Root) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.roots.add(roots...)
|
|
}
|
|
|
|
// RemoveRoots removes the roots with the given URIs,
|
|
// and notifies any connected servers if the list has changed.
|
|
// It is not an error to remove a nonexistent root.
|
|
// TODO: notification
|
|
func (c *Client) RemoveRoots(uris ...string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.roots.remove(uris...)
|
|
}
|
|
|
|
func (c *Client) listRoots(_ context.Context, _ *ListRootsParams) (*ListRootsResult, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return &ListRootsResult{
|
|
Roots: slices.Collect(c.roots.all()),
|
|
}, nil
|
|
}
|
|
|
|
func (c *Client) handle(ctx context.Context, req *jsonrpc2.Request) (any, error) {
|
|
// TODO: when we switch to ClientSessions, use a copy of the server's dispatch function, or
|
|
// maybe just add another type parameter.
|
|
//
|
|
// No need to check that the connection is initialized, since we initialize
|
|
// it in Connect.
|
|
switch req.Method {
|
|
case "ping":
|
|
// The spec says that 'ping' expects an empty object result.
|
|
return struct{}{}, nil
|
|
case "roots/list":
|
|
// ListRootsParams happens to be unused.
|
|
return c.listRoots(ctx, nil)
|
|
}
|
|
return nil, jsonrpc2.ErrNotHandled
|
|
}
|
|
|
|
// Ping makes an MCP "ping" request to the server.
|
|
func (c *Client) Ping(ctx context.Context) error {
|
|
return call(ctx, c.conn, "ping", nil, nil)
|
|
}
|
|
|
|
// ListPrompts lists prompts that are currently available on the server.
|
|
func (c *Client) ListPrompts(ctx context.Context) ([]*Prompt, error) {
|
|
var (
|
|
params = &ListPromptsParams{}
|
|
result ListPromptsResult
|
|
)
|
|
if err := call(ctx, c.conn, "prompts/list", params, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return result.Prompts, nil
|
|
}
|
|
|
|
// GetPrompt gets a prompt from the server.
|
|
func (c *Client) GetPrompt(ctx context.Context, name string, args map[string]string) (*GetPromptResult, error) {
|
|
var (
|
|
params = &GetPromptParams{
|
|
Name: name,
|
|
Arguments: args,
|
|
}
|
|
result = &GetPromptResult{}
|
|
)
|
|
if err := call(ctx, c.conn, "prompts/get", params, result); err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// ListTools lists tools that are currently available on the server.
|
|
func (c *Client) ListTools(ctx context.Context) ([]*Tool, error) {
|
|
var (
|
|
params = &ListToolsParams{}
|
|
result ListToolsResult
|
|
)
|
|
if err := call(ctx, c.conn, "tools/list", params, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return result.Tools, nil
|
|
}
|
|
|
|
// CallTool calls the tool with the given name and arguments.
|
|
func (c *Client) CallTool(ctx context.Context, name string, args map[string]any) (_ *CallToolResult, err error) {
|
|
defer func() {
|
|
if err != nil {
|
|
err = fmt.Errorf("calling tool %q: %w", name, err)
|
|
}
|
|
}()
|
|
argsJSON := make(map[string]json.RawMessage)
|
|
for name, arg := range args {
|
|
argJSON, err := json.Marshal(arg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshaling argument %s: %v", name, err)
|
|
}
|
|
argsJSON[name] = argJSON
|
|
}
|
|
|
|
params := &CallToolParams{
|
|
Name: name,
|
|
Arguments: argsJSON,
|
|
}
|
|
return standardCall[CallToolResult](ctx, c.conn, "tools/call", params)
|
|
}
|
|
|
|
// ListResources lists the resources that are currently available on the server.
|
|
func (c *Client) ListResources(ctx context.Context, params *ListResourcesParams) (*ListResourcesResult, error) {
|
|
return standardCall[ListResourcesResult](ctx, c.conn, "resources/list", params)
|
|
}
|
|
|
|
// ReadResource ask the server to read a resource and return its contents.
|
|
func (c *Client) ReadResource(ctx context.Context, params *ReadResourceParams) (*ReadResourceResult, error) {
|
|
return standardCall[ReadResourceResult](ctx, c.conn, "resources/read", params)
|
|
}
|
|
|
|
func standardCall[TRes, TParams any](ctx context.Context, conn *jsonrpc2.Connection, method string, params TParams) (*TRes, error) {
|
|
var result TRes
|
|
if err := call(ctx, conn, method, params, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return &result, nil
|
|
}
|