From 3de0bf35aed0897e19c7b2bea68968c00f377af7 Mon Sep 17 00:00:00 2001 From: Chris Battarbee Date: Tue, 21 Jan 2025 15:10:25 +0000 Subject: [PATCH 01/13] Add cursor rules --- .cursorrules | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .cursorrules diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..20221fd --- /dev/null +++ b/.cursorrules @@ -0,0 +1,9 @@ +This repository is an implementation of a golang model context protocol sdk. + +Project docs are available in the docs folder. + +Model context prtocol docs are available at https://modelcontextprotocol.io + +Write tests for all new functionality. + +Use go test to run tests after adding new functionality. \ No newline at end of file From 749a7318ac9a711ce0d1bbae8eb518cbaaa88996 Mon Sep 17 00:00:00 2001 From: Chris Battarbee Date: Tue, 21 Jan 2025 15:56:14 +0000 Subject: [PATCH 02/13] Update go mod --- go.mod | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 78c7009..9d7f823 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/metoro-io/mcp-golang go 1.23.4 require ( - github.com/google/uuid v1.6.0 github.com/invopop/jsonschema v0.12.0 github.com/stretchr/testify v1.9.0 - golang.org/x/tools v0.28.0 + github.com/pkg/errors v0.9.1 + github.com/tidwall/sjson v1.2.5 ) require ( @@ -14,14 +14,10 @@ require ( github.com/buger/jsonparser v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - github.com/tidwall/sjson v1.2.5 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) From a4e8ace58c879fc630616e941e0ff96e7b592e03 Mon Sep 17 00:00:00 2001 From: Chris Battarbee Date: Tue, 21 Jan 2025 16:12:46 +0000 Subject: [PATCH 03/13] Add client --- client.go | 267 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 client.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..f7ad04e --- /dev/null +++ b/client.go @@ -0,0 +1,267 @@ +package mcp_golang + +import ( + "context" + "encoding/json" + + "github.com/metoro-io/mcp-golang/internal/protocol" + "github.com/metoro-io/mcp-golang/internal/tools" + "github.com/metoro-io/mcp-golang/transport" + "github.com/pkg/errors" +) + +// Client represents an MCP client that can connect to and interact with MCP servers +type Client struct { + transport transport.Transport + protocol *protocol.Protocol + capabilities *serverCapabilities + initialized bool +} + +// NewClient creates a new MCP client with the specified transport +func NewClient(transport transport.Transport) *Client { + return &Client{ + transport: transport, + protocol: protocol.NewProtocol(nil), + } +} + +// Initialize connects to the server and retrieves its capabilities +func (c *Client) Initialize(ctx context.Context) error { + if c.initialized { + return errors.New("client already initialized") + } + + err := c.protocol.Connect(c.transport) + if err != nil { + return errors.Wrap(err, "failed to connect transport") + } + + // Make initialize request to server + response, err := c.protocol.Request(ctx, "initialize", nil, nil) + if err != nil { + return errors.Wrap(err, "failed to initialize") + } + + responseBytes, ok := response.([]byte) + if !ok { + return errors.New("invalid response type") + } + + var initResult initializeResult + err = json.Unmarshal(responseBytes, &initResult) + if err != nil { + return errors.Wrap(err, "failed to unmarshal initialize response") + } + + c.capabilities = &initResult.Capabilities + c.initialized = true + return nil +} + +// ListTools retrieves the list of available tools from the server +func (c *Client) ListTools(ctx context.Context, cursor *string) (*tools.ToolsResponse, error) { + if !c.initialized { + return nil, errors.New("client not initialized") + } + + params := map[string]interface{}{ + "cursor": cursor, + } + + response, err := c.protocol.Request(ctx, "tools/list", params, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to list tools") + } + + responseBytes, ok := response.([]byte) + if !ok { + return nil, errors.New("invalid response type") + } + + var toolsResponse tools.ToolsResponse + err = json.Unmarshal(responseBytes, &toolsResponse) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal tools response") + } + + return &toolsResponse, nil +} + +// CallTool calls a specific tool on the server with the provided arguments +func (c *Client) CallTool(ctx context.Context, name string, arguments json.RawMessage) (*ToolResponse, error) { + if !c.initialized { + return nil, errors.New("client not initialized") + } + + params := baseCallToolRequestParams{ + Name: name, + Arguments: arguments, + } + + response, err := c.protocol.Request(ctx, "tools/call", params, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to call tool") + } + + responseBytes, ok := response.([]byte) + if !ok { + return nil, errors.New("invalid response type") + } + + var toolResponse toolResponseSent + err = json.Unmarshal(responseBytes, &toolResponse) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal tool response") + } + + if toolResponse.Error != nil { + return nil, toolResponse.Error + } + + return toolResponse.Response, nil +} + +// ListPrompts retrieves the list of available prompts from the server +func (c *Client) ListPrompts(ctx context.Context, cursor *string) (*listPromptsResult, error) { + if !c.initialized { + return nil, errors.New("client not initialized") + } + + params := map[string]interface{}{ + "cursor": cursor, + } + + response, err := c.protocol.Request(ctx, "prompts/list", params, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to list prompts") + } + + responseBytes, ok := response.([]byte) + if !ok { + return nil, errors.New("invalid response type") + } + + var promptsResponse listPromptsResult + err = json.Unmarshal(responseBytes, &promptsResponse) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal prompts response") + } + + return &promptsResponse, nil +} + +// GetPrompt retrieves a specific prompt from the server +func (c *Client) GetPrompt(ctx context.Context, name string, arguments json.RawMessage) (*PromptResponse, error) { + if !c.initialized { + return nil, errors.New("client not initialized") + } + + params := baseGetPromptRequestParamsArguments{ + Name: name, + Arguments: arguments, + } + + response, err := c.protocol.Request(ctx, "prompts/get", params, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to get prompt") + } + + responseBytes, ok := response.([]byte) + if !ok { + return nil, errors.New("invalid response type") + } + + var promptResponse promptResponseSent + err = json.Unmarshal(responseBytes, &promptResponse) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal prompt response") + } + + if promptResponse.Error != nil { + return nil, promptResponse.Error + } + + return promptResponse.Response, nil +} + +// ListResources retrieves the list of available resources from the server +func (c *Client) ListResources(ctx context.Context, cursor *string) (*listResourcesResult, error) { + if !c.initialized { + return nil, errors.New("client not initialized") + } + + params := map[string]interface{}{ + "cursor": cursor, + } + + response, err := c.protocol.Request(ctx, "resources/list", params, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to list resources") + } + + responseBytes, ok := response.([]byte) + if !ok { + return nil, errors.New("invalid response type") + } + + var resourcesResponse listResourcesResult + err = json.Unmarshal(responseBytes, &resourcesResponse) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal resources response") + } + + return &resourcesResponse, nil +} + +// ReadResource reads a specific resource from the server +func (c *Client) ReadResource(ctx context.Context, uri string) (*ResourceResponse, error) { + if !c.initialized { + return nil, errors.New("client not initialized") + } + + params := readResourceRequestParams{ + Uri: uri, + } + + response, err := c.protocol.Request(ctx, "resources/read", params, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to read resource") + } + + responseBytes, ok := response.([]byte) + if !ok { + return nil, errors.New("invalid response type") + } + + var resourceResponse resourceResponseSent + err = json.Unmarshal(responseBytes, &resourceResponse) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal resource response") + } + + if resourceResponse.Error != nil { + return nil, resourceResponse.Error + } + + return resourceResponse.Response, nil +} + +// Ping sends a ping request to the server to check connectivity +func (c *Client) Ping(ctx context.Context) error { + if !c.initialized { + return errors.New("client not initialized") + } + + _, err := c.protocol.Request(ctx, "ping", nil, nil) + if err != nil { + return errors.Wrap(err, "failed to ping server") + } + + return nil +} + +// GetCapabilities returns the server capabilities obtained during initialization +func (c *Client) GetCapabilities() *serverCapabilities { + return c.capabilities +} From b77bed74b8f3bbd90d74beaeddf04c4d116c005b Mon Sep 17 00:00:00 2001 From: Chris Battarbee Date: Tue, 21 Jan 2025 16:15:07 +0000 Subject: [PATCH 04/13] Small fixes --- content_api.go | 3 ++- server.go | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/content_api.go b/content_api.go index dec3b4c..ce117e8 100644 --- a/content_api.go +++ b/content_api.go @@ -3,6 +3,7 @@ package mcp_golang import ( "encoding/json" "fmt" + "github.com/tidwall/sjson" ) @@ -113,7 +114,7 @@ type Content struct { // Custom JSON marshaling for ToolResponse Content func (c Content) MarshalJSON() ([]byte, error) { - rawJson := []byte{} + var rawJson []byte switch c.Type { case ContentTypeText: diff --git a/server.go b/server.go index dda1e22..5c3ab08 100644 --- a/server.go +++ b/server.go @@ -4,15 +4,16 @@ import ( "encoding/base64" "encoding/json" "fmt" + "reflect" + "sort" + "strings" + "github.com/invopop/jsonschema" "github.com/metoro-io/mcp-golang/internal/datastructures" "github.com/metoro-io/mcp-golang/internal/protocol" "github.com/metoro-io/mcp-golang/internal/tools" "github.com/metoro-io/mcp-golang/transport" "github.com/pkg/errors" - "reflect" - "sort" - "strings" ) // Here we define the actual MCP server that users will create and run @@ -476,7 +477,7 @@ func createWrappedToolHandler(userHandler any) func(baseCallToolRequestParams) * } func (s *Server) Serve() error { - if s.isRunning == true { + if s.isRunning { return fmt.Errorf("server is already running") } pr := s.protocol From 7003a21c5e36bdde39c785ae78cbe041e24a0232 Mon Sep 17 00:00:00 2001 From: Chris Battarbee Date: Tue, 21 Jan 2025 16:57:22 +0000 Subject: [PATCH 05/13] Add basic client --- client.go | 14 +-- examples/client/main.go | 51 +++++++++ examples/client/server/main.go | 39 +++++++ examples/list_tools.go | 192 ++++++++++++++++++++++++++++++++ examples/server/main.go | 39 +++++++ go.mod | 2 +- transport/stdio/stdio_server.go | 5 +- 7 files changed, 332 insertions(+), 10 deletions(-) create mode 100644 examples/client/main.go create mode 100644 examples/client/server/main.go create mode 100644 examples/list_tools.go create mode 100644 examples/server/main.go diff --git a/client.go b/client.go index f7ad04e..05a154a 100644 --- a/client.go +++ b/client.go @@ -43,7 +43,7 @@ func (c *Client) Initialize(ctx context.Context) error { return errors.Wrap(err, "failed to initialize") } - responseBytes, ok := response.([]byte) + responseBytes, ok := response.(json.RawMessage) if !ok { return errors.New("invalid response type") } @@ -74,7 +74,7 @@ func (c *Client) ListTools(ctx context.Context, cursor *string) (*tools.ToolsRes return nil, errors.Wrap(err, "failed to list tools") } - responseBytes, ok := response.([]byte) + responseBytes, ok := response.(json.RawMessage) if !ok { return nil, errors.New("invalid response type") } @@ -104,7 +104,7 @@ func (c *Client) CallTool(ctx context.Context, name string, arguments json.RawMe return nil, errors.Wrap(err, "failed to call tool") } - responseBytes, ok := response.([]byte) + responseBytes, ok := response.(json.RawMessage) if !ok { return nil, errors.New("invalid response type") } @@ -137,7 +137,7 @@ func (c *Client) ListPrompts(ctx context.Context, cursor *string) (*listPromptsR return nil, errors.Wrap(err, "failed to list prompts") } - responseBytes, ok := response.([]byte) + responseBytes, ok := response.(json.RawMessage) if !ok { return nil, errors.New("invalid response type") } @@ -167,7 +167,7 @@ func (c *Client) GetPrompt(ctx context.Context, name string, arguments json.RawM return nil, errors.Wrap(err, "failed to get prompt") } - responseBytes, ok := response.([]byte) + responseBytes, ok := response.(json.RawMessage) if !ok { return nil, errors.New("invalid response type") } @@ -200,7 +200,7 @@ func (c *Client) ListResources(ctx context.Context, cursor *string) (*listResour return nil, errors.Wrap(err, "failed to list resources") } - responseBytes, ok := response.([]byte) + responseBytes, ok := response.(json.RawMessage) if !ok { return nil, errors.New("invalid response type") } @@ -229,7 +229,7 @@ func (c *Client) ReadResource(ctx context.Context, uri string) (*ResourceRespons return nil, errors.Wrap(err, "failed to read resource") } - responseBytes, ok := response.([]byte) + responseBytes, ok := response.(json.RawMessage) if !ok { return nil, errors.New("invalid response type") } diff --git a/examples/client/main.go b/examples/client/main.go new file mode 100644 index 0000000..e9a2e12 --- /dev/null +++ b/examples/client/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "log" + "os/exec" + + "context" + + mcp_golang "github.com/metoro-io/mcp-golang" + "github.com/metoro-io/mcp-golang/transport/stdio" +) + +func main() { + // Start the server process + cmd := exec.Command("go", "run", "./server/main.go") + stdin, err := cmd.StdinPipe() + if err != nil { + log.Fatalf("Failed to get stdin pipe: %v", err) + } + stdout, err := cmd.StdoutPipe() + if err != nil { + log.Fatalf("Failed to get stdout pipe: %v", err) + } + + if err := cmd.Start(); err != nil { + log.Fatalf("Failed to start server: %v", err) + } + defer cmd.Process.Kill() + + clientTransport := stdio.NewStdioServerTransportWithIO(stdout, stdin) + + client := mcp_golang.NewClient(clientTransport) + + if err := client.Initialize(context.Background()); err != nil { + log.Fatalf("Failed to initialize client: %v", err) + } + + tools, err := client.ListTools(context.Background(), nil) + if err != nil { + log.Fatalf("Failed to list tools: %v", err) + } + + log.Println("Tools:") + for _, tool := range tools.Tools { + desc := "" + if tool.Description != nil { + desc = *tool.Description + } + log.Printf("Tool: %s. Description: %s", tool.Name, desc) + } +} diff --git a/examples/client/server/main.go b/examples/client/server/main.go new file mode 100644 index 0000000..c12522b --- /dev/null +++ b/examples/client/server/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + + mcp "github.com/metoro-io/mcp-golang" + "github.com/metoro-io/mcp-golang/transport/stdio" +) + +// HelloArgs represents the arguments for the hello tool +type HelloArgs struct { + Name string `json:"name" jsonschema:"required,description=The name to say hello to"` +} + +func main() { + // Create a transport for the server + serverTransport := stdio.NewStdioServerTransport() + + // Create a new server with the transport + server := mcp.NewServer(serverTransport) + + // Register a simple tool with the server + err := server.RegisterTool("hello", "Says hello", func(args HelloArgs) (*mcp.ToolResponse, error) { + message := fmt.Sprintf("Hello, %s!", args.Name) + return mcp.NewToolResponse(mcp.NewTextContent(message)), nil + }) + if err != nil { + panic(err) + } + + // Start the server + err = server.Serve() + if err != nil { + panic(err) + } + + // Keep the server running + select {} +} diff --git a/examples/list_tools.go b/examples/list_tools.go new file mode 100644 index 0000000..1209124 --- /dev/null +++ b/examples/list_tools.go @@ -0,0 +1,192 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "os" + "os/exec" + "time" + + mcp "github.com/metoro-io/mcp-golang" + "github.com/metoro-io/mcp-golang/transport" + "github.com/metoro-io/mcp-golang/transport/stdio" +) + +// HelloArgs represents the arguments for the hello tool +type HelloArgs struct { + Name string `json:"name" jsonschema:"required,description=The name to say hello to"` +} + +// runServer starts an MCP server that registers a hello tool +func runServer() { + // Create a transport for the server + serverTransport := stdio.NewStdioServerTransport() + + // Create a new server with the transport + server := mcp.NewServer(serverTransport) + + // Register a simple tool with the server + err := server.RegisterTool("hello", "Says hello", func(args HelloArgs) (*mcp.ToolResponse, error) { + message := fmt.Sprintf("Hello, %s!", args.Name) + return mcp.NewToolResponse(mcp.NewTextContent(message)), nil + }) + if err != nil { + panic(err) + } + + // Start the server + err = server.Serve() + if err != nil { + panic(err) + } + + // Keep the server running + select {} +} + +// runClient starts an MCP client that connects to the server and interacts with it +func runClient() { + // Start the server process + cmd := exec.Command("go", "run", "-v", "-") + stdin, err := cmd.StdinPipe() + if err != nil { + log.Fatalf("Failed to get stdin pipe: %v", err) + } + stdout, err := cmd.StdoutPipe() + if err != nil { + log.Fatalf("Failed to get stdout pipe: %v", err) + } + stderr, err := cmd.StderrPipe() + if err != nil { + log.Fatalf("Failed to get stderr pipe: %v", err) + } + + // Pass the current source code to the server process + cmd.Env = append(cmd.Env, "RUN_AS_SERVER=true") + + if err := cmd.Start(); err != nil { + log.Fatalf("Failed to start server: %v", err) + } + defer cmd.Process.Kill() + + // Start a goroutine to read stderr + go func() { + buf := make([]byte, 1024) + for { + n, err := stderr.Read(buf) + if err != nil { + if err != io.EOF { + log.Printf("Error reading stderr: %v", err) + } + return + } + if n > 0 { + log.Printf("Server stderr: %s", string(buf[:n])) + } + } + }() + + // Helper function to send a request and read response + sendRequest := func(method string, params interface{}) (map[string]interface{}, error) { + paramsBytes, err := json.Marshal(params) + if err != nil { + return nil, err + } + + req := transport.BaseJSONRPCRequest{ + Jsonrpc: "2.0", + Method: method, + Params: json.RawMessage(paramsBytes), + Id: transport.RequestId(1), + } + + reqBytes, err := json.Marshal(req) + if err != nil { + return nil, err + } + reqBytes = append(reqBytes, '\n') + + log.Printf("Sending request: %s", string(reqBytes)) + _, err = stdin.Write(reqBytes) + if err != nil { + return nil, err + } + + // Read response with timeout + respChan := make(chan map[string]interface{}, 1) + errChan := make(chan error, 1) + + go func() { + decoder := json.NewDecoder(stdout) + var response map[string]interface{} + err := decoder.Decode(&response) + if err != nil { + errChan <- fmt.Errorf("failed to decode response: %v", err) + return + } + log.Printf("Got response: %+v", response) + respChan <- response + }() + + select { + case resp := <-respChan: + return resp, nil + case err := <-errChan: + return nil, err + case <-time.After(5 * time.Second): + return nil, fmt.Errorf("timeout waiting for response") + } + } + + // Initialize the server + resp, err := sendRequest("initialize", map[string]interface{}{ + "capabilities": map[string]interface{}{}, + }) + if err != nil { + log.Fatalf("Failed to initialize: %v", err) + } + log.Printf("Server initialized: %+v", resp) + + // List tools + resp, err = sendRequest("tools/list", map[string]interface{}{}) + if err != nil { + log.Fatalf("Failed to list tools: %v", err) + } + + // Print the available tools + fmt.Println("\nAvailable tools:") + tools := resp["result"].(map[string]interface{})["tools"].([]interface{}) + for _, t := range tools { + tool := t.(map[string]interface{}) + fmt.Printf("- %s: %s\n", tool["name"], tool["description"]) + } + + // Call the hello tool + callParams := map[string]interface{}{ + "name": "hello", + "arguments": map[string]interface{}{ + "name": "World", + }, + } + resp, err = sendRequest("tools/call", callParams) + if err != nil { + log.Fatalf("Failed to call hello tool: %v", err) + } + + // Print the response + fmt.Println("\nTool response:") + result := resp["result"].(map[string]interface{}) + content := result["content"].([]interface{})[0].(map[string]interface{}) + fmt.Println(content["text"]) +} + +func main() { + // If RUN_AS_SERVER is set, run as server, otherwise run as client + if os.Getenv("RUN_AS_SERVER") == "true" { + runServer() + } else { + runClient() + } +} diff --git a/examples/server/main.go b/examples/server/main.go new file mode 100644 index 0000000..c12522b --- /dev/null +++ b/examples/server/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + + mcp "github.com/metoro-io/mcp-golang" + "github.com/metoro-io/mcp-golang/transport/stdio" +) + +// HelloArgs represents the arguments for the hello tool +type HelloArgs struct { + Name string `json:"name" jsonschema:"required,description=The name to say hello to"` +} + +func main() { + // Create a transport for the server + serverTransport := stdio.NewStdioServerTransport() + + // Create a new server with the transport + server := mcp.NewServer(serverTransport) + + // Register a simple tool with the server + err := server.RegisterTool("hello", "Says hello", func(args HelloArgs) (*mcp.ToolResponse, error) { + message := fmt.Sprintf("Hello, %s!", args.Name) + return mcp.NewToolResponse(mcp.NewTextContent(message)), nil + }) + if err != nil { + panic(err) + } + + // Start the server + err = server.Serve() + if err != nil { + panic(err) + } + + // Keep the server running + select {} +} diff --git a/go.mod b/go.mod index 9d7f823..41810df 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/metoro-io/mcp-golang -go 1.23.4 +go 1.21 require ( github.com/invopop/jsonschema v0.12.0 diff --git a/transport/stdio/stdio_server.go b/transport/stdio/stdio_server.go index fa2df0a..93a4baf 100644 --- a/transport/stdio/stdio_server.go +++ b/transport/stdio/stdio_server.go @@ -5,11 +5,12 @@ import ( "context" "encoding/json" "fmt" - "github.com/metoro-io/mcp-golang/transport" - "github.com/metoro-io/mcp-golang/transport/stdio/internal/stdio" "io" "os" "sync" + + "github.com/metoro-io/mcp-golang/transport" + "github.com/metoro-io/mcp-golang/transport/stdio/internal/stdio" ) // StdioServerTransport implements server-side transport for stdio communication From e24ba1ccedc703bc3d90f1bb0a2b4cc14bdfc6d5 Mon Sep 17 00:00:00 2001 From: Chris Battarbee Date: Tue, 21 Jan 2025 18:15:05 +0000 Subject: [PATCH 06/13] Add basic example --- .gitignore | 3 +- client.go | 44 +++++++--------- content_api.go | 34 ++++++++++++ examples/client/main.go | 94 ++++++++++++++++++++++++++++++++-- examples/client/server/main.go | 86 +++++++++++++++++++++++++++++-- prompt_response_types.go | 12 ++--- resource_response_types.go | 6 +-- server.go | 39 +++++++------- server_test.go | 16 +++--- tool_api.go | 2 +- tool_response_types.go | 32 ++++++------ 11 files changed, 281 insertions(+), 87 deletions(-) diff --git a/.gitignore b/.gitignore index 723ef36..706fd07 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.idea \ No newline at end of file +.idea +.vscode diff --git a/client.go b/client.go index 05a154a..8764768 100644 --- a/client.go +++ b/client.go @@ -14,7 +14,7 @@ import ( type Client struct { transport transport.Transport protocol *protocol.Protocol - capabilities *serverCapabilities + capabilities *ServerCapabilities initialized bool } @@ -27,36 +27,36 @@ func NewClient(transport transport.Transport) *Client { } // Initialize connects to the server and retrieves its capabilities -func (c *Client) Initialize(ctx context.Context) error { +func (c *Client) Initialize(ctx context.Context) (*InitializeResponse, error) { if c.initialized { - return errors.New("client already initialized") + return nil, errors.New("client already initialized") } err := c.protocol.Connect(c.transport) if err != nil { - return errors.Wrap(err, "failed to connect transport") + return nil, errors.Wrap(err, "failed to connect transport") } // Make initialize request to server response, err := c.protocol.Request(ctx, "initialize", nil, nil) if err != nil { - return errors.Wrap(err, "failed to initialize") + return nil, errors.Wrap(err, "failed to initialize") } responseBytes, ok := response.(json.RawMessage) if !ok { - return errors.New("invalid response type") + return nil, errors.New("invalid response type") } - var initResult initializeResult + var initResult InitializeResponse err = json.Unmarshal(responseBytes, &initResult) if err != nil { - return errors.Wrap(err, "failed to unmarshal initialize response") + return nil, errors.Wrap(err, "failed to unmarshal initialize response") } c.capabilities = &initResult.Capabilities c.initialized = true - return nil + return &initResult, nil } // ListTools retrieves the list of available tools from the server @@ -109,21 +109,17 @@ func (c *Client) CallTool(ctx context.Context, name string, arguments json.RawMe return nil, errors.New("invalid response type") } - var toolResponse toolResponseSent + var toolResponse ToolResponse err = json.Unmarshal(responseBytes, &toolResponse) if err != nil { return nil, errors.Wrap(err, "failed to unmarshal tool response") } - if toolResponse.Error != nil { - return nil, toolResponse.Error - } - - return toolResponse.Response, nil + return &toolResponse, nil } // ListPrompts retrieves the list of available prompts from the server -func (c *Client) ListPrompts(ctx context.Context, cursor *string) (*listPromptsResult, error) { +func (c *Client) ListPrompts(ctx context.Context, cursor *string) (*ListPromptsResponse, error) { if !c.initialized { return nil, errors.New("client not initialized") } @@ -142,7 +138,7 @@ func (c *Client) ListPrompts(ctx context.Context, cursor *string) (*listPromptsR return nil, errors.New("invalid response type") } - var promptsResponse listPromptsResult + var promptsResponse ListPromptsResponse err = json.Unmarshal(responseBytes, &promptsResponse) if err != nil { return nil, errors.Wrap(err, "failed to unmarshal prompts response") @@ -172,21 +168,17 @@ func (c *Client) GetPrompt(ctx context.Context, name string, arguments json.RawM return nil, errors.New("invalid response type") } - var promptResponse promptResponseSent + var promptResponse PromptResponse err = json.Unmarshal(responseBytes, &promptResponse) if err != nil { return nil, errors.Wrap(err, "failed to unmarshal prompt response") } - if promptResponse.Error != nil { - return nil, promptResponse.Error - } - - return promptResponse.Response, nil + return &promptResponse, nil } // ListResources retrieves the list of available resources from the server -func (c *Client) ListResources(ctx context.Context, cursor *string) (*listResourcesResult, error) { +func (c *Client) ListResources(ctx context.Context, cursor *string) (*ListResourcesResponse, error) { if !c.initialized { return nil, errors.New("client not initialized") } @@ -205,7 +197,7 @@ func (c *Client) ListResources(ctx context.Context, cursor *string) (*listResour return nil, errors.New("invalid response type") } - var resourcesResponse listResourcesResult + var resourcesResponse ListResourcesResponse err = json.Unmarshal(responseBytes, &resourcesResponse) if err != nil { return nil, errors.Wrap(err, "failed to unmarshal resources response") @@ -262,6 +254,6 @@ func (c *Client) Ping(ctx context.Context) error { } // GetCapabilities returns the server capabilities obtained during initialization -func (c *Client) GetCapabilities() *serverCapabilities { +func (c *Client) GetCapabilities() *ServerCapabilities { return c.capabilities } diff --git a/content_api.go b/content_api.go index ce117e8..b111593 100644 --- a/content_api.go +++ b/content_api.go @@ -112,6 +112,40 @@ type Content struct { Annotations *Annotations } +func (c *Content) UnmarshalJSON(b []byte) error { + type typeWrapper struct { + Type ContentType `json:"type" yaml:"type" mapstructure:"type"` + Text *string `json:"text" yaml:"text" mapstructure:"text"` + Image *string `json:"image" yaml:"image" mapstructure:"image"` + Annotations *Annotations `json:"annotations" yaml:"annotations" mapstructure:"annotations"` + EmbeddedResource *EmbeddedResource `json:"resource" yaml:"resource" mapstructure:"resource"` + } + var tw typeWrapper + err := json.Unmarshal(b, &tw) + if err != nil { + return err + } + switch tw.Type { + case ContentTypeText: + c.Type = ContentTypeText + case ContentTypeImage: + c.Type = ContentTypeImage + case ContentTypeEmbeddedResource: + c.Type = ContentTypeEmbeddedResource + default: + return fmt.Errorf("unknown content type: %s", tw.Type) + } + + switch c.Type { + case ContentTypeText: + c.TextContent = &TextContent{Text: *tw.Text} + default: + return fmt.Errorf("unknown content type: %s", c.Type) + } + + return nil +} + // Custom JSON marshaling for ToolResponse Content func (c Content) MarshalJSON() ([]byte, error) { var rawJson []byte diff --git a/examples/client/main.go b/examples/client/main.go index e9a2e12..077c938 100644 --- a/examples/client/main.go +++ b/examples/client/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "log" "os/exec" @@ -10,6 +11,11 @@ import ( "github.com/metoro-io/mcp-golang/transport/stdio" ) +type toolResponseSent struct { + Content []*mcp_golang.Content `json:"content"` + IsError bool `json:"isError"` +} + func main() { // Start the server process cmd := exec.Command("go", "run", "./server/main.go") @@ -28,19 +34,19 @@ func main() { defer cmd.Process.Kill() clientTransport := stdio.NewStdioServerTransportWithIO(stdout, stdin) - client := mcp_golang.NewClient(clientTransport) - if err := client.Initialize(context.Background()); err != nil { + if _, err := client.Initialize(context.Background()); err != nil { log.Fatalf("Failed to initialize client: %v", err) } + // List available tools tools, err := client.ListTools(context.Background(), nil) if err != nil { log.Fatalf("Failed to list tools: %v", err) } - log.Println("Tools:") + log.Println("Available Tools:") for _, tool := range tools.Tools { desc := "" if tool.Description != nil { @@ -48,4 +54,86 @@ func main() { } log.Printf("Tool: %s. Description: %s", tool.Name, desc) } + + // Example of calling the hello tool + helloArgs := map[string]interface{}{ + "name": "World", + } + helloArgsJson, _ := json.Marshal(helloArgs) + + log.Println("\nCalling hello tool:") + helloResponse, err := client.CallTool(context.Background(), "hello", helloArgsJson) + if err != nil { + log.Printf("Failed to call hello tool: %v", err) + } else if helloResponse != nil && len(helloResponse.Content) > 0 && helloResponse.Content[0].TextContent != nil { + log.Printf("Hello response: %s", helloResponse.Content[0].TextContent.Text) + } + + // Example of calling the calculate tool + calcArgs := map[string]interface{}{ + "operation": "add", + "a": 10, + "b": 5, + } + calcArgsJson, _ := json.Marshal(calcArgs) + + log.Println("\nCalling calculate tool:") + calcResponse, err := client.CallTool(context.Background(), "calculate", calcArgsJson) + if err != nil { + log.Printf("Failed to call calculate tool: %v", err) + } else if calcResponse != nil && len(calcResponse.Content) > 0 && calcResponse.Content[0].TextContent != nil { + log.Printf("Calculate response: %s", calcResponse.Content[0].TextContent.Text) + } + + // Example of calling the time tool + timeArgs := map[string]interface{}{ + "format": "2006-01-02 15:04:05", + } + timeArgsJson, _ := json.Marshal(timeArgs) + + log.Println("\nCalling time tool:") + timeResponse, err := client.CallTool(context.Background(), "time", timeArgsJson) + if err != nil { + log.Printf("Failed to call time tool: %v", err) + } else if timeResponse != nil && len(timeResponse.Content) > 0 && timeResponse.Content[0].TextContent != nil { + log.Printf("Time response: %s", timeResponse.Content[0].TextContent.Text) + } + + // List available prompts + prompts, err := client.ListPrompts(context.Background(), nil) + if err != nil { + log.Printf("Failed to list prompts: %v", err) + } else { + log.Println("\nAvailable Prompts:") + for _, prompt := range prompts.Prompts { + desc := "" + if prompt.Description != nil { + desc = *prompt.Description + } + log.Printf("Prompt: %s. Description: %s", prompt.Name, desc) + } + + // Example of using the uppercase prompt + promptArgs := map[string]interface{}{ + "input": "Hello, Model Context Protocol!", + } + promptArgsJson, _ := json.Marshal(promptArgs) + + log.Printf("\nCalling uppercase prompt:") + upperResponse, err := client.GetPrompt(context.Background(), "uppercase", promptArgsJson) + if err != nil { + log.Printf("Failed to get uppercase prompt: %v", err) + } else if upperResponse != nil && len(upperResponse.Messages) > 0 && upperResponse.Messages[0].Content != nil { + log.Printf("Uppercase response: %s", upperResponse.Messages[0].Content.TextContent.Text) + } + + // Example of using the reverse prompt + log.Printf("\nCalling reverse prompt:") + reverseResponse, err := client.GetPrompt(context.Background(), "reverse", promptArgsJson) + if err != nil { + log.Printf("Failed to get reverse prompt: %v", err) + } else if reverseResponse != nil && len(reverseResponse.Messages) > 0 && reverseResponse.Messages[0].Content != nil { + log.Printf("Reverse response: %s", reverseResponse.Messages[0].Content.TextContent.Text) + } + } } diff --git a/examples/client/server/main.go b/examples/client/server/main.go index c12522b..87caa8b 100644 --- a/examples/client/server/main.go +++ b/examples/client/server/main.go @@ -2,6 +2,8 @@ package main import ( "fmt" + "strings" + "time" mcp "github.com/metoro-io/mcp-golang" "github.com/metoro-io/mcp-golang/transport/stdio" @@ -12,6 +14,23 @@ type HelloArgs struct { Name string `json:"name" jsonschema:"required,description=The name to say hello to"` } +// CalculateArgs represents the arguments for the calculate tool +type CalculateArgs struct { + Operation string `json:"operation" jsonschema:"required,enum=add,enum=subtract,enum=multiply,enum=divide,description=The mathematical operation to perform"` + A float64 `json:"a" jsonschema:"required,description=First number"` + B float64 `json:"b" jsonschema:"required,description=Second number"` +} + +// TimeArgs represents the arguments for the current time tool +type TimeArgs struct { + Format string `json:"format,omitempty" jsonschema:"description=Optional time format (default: RFC3339)"` +} + +// PromptArgs represents the arguments for custom prompts +type PromptArgs struct { + Input string `json:"input" jsonschema:"required,description=The input text to process"` +} + func main() { // Create a transport for the server serverTransport := stdio.NewStdioServerTransport() @@ -19,8 +38,8 @@ func main() { // Create a new server with the transport server := mcp.NewServer(serverTransport) - // Register a simple tool with the server - err := server.RegisterTool("hello", "Says hello", func(args HelloArgs) (*mcp.ToolResponse, error) { + // Register hello tool + err := server.RegisterTool("hello", "Says hello to the provided name", func(args HelloArgs) (*mcp.ToolResponse, error) { message := fmt.Sprintf("Hello, %s!", args.Name) return mcp.NewToolResponse(mcp.NewTextContent(message)), nil }) @@ -28,12 +47,71 @@ func main() { panic(err) } - // Start the server - err = server.Serve() + // Register calculate tool + err = server.RegisterTool("calculate", "Performs basic mathematical operations", func(args CalculateArgs) (*mcp.ToolResponse, error) { + var result float64 + switch args.Operation { + case "add": + result = args.A + args.B + case "subtract": + result = args.A - args.B + case "multiply": + result = args.A * args.B + case "divide": + if args.B == 0 { + return nil, fmt.Errorf("division by zero") + } + result = args.A / args.B + default: + return nil, fmt.Errorf("unknown operation: %s", args.Operation) + } + message := fmt.Sprintf("Result of %s: %.2f", args.Operation, result) + return mcp.NewToolResponse(mcp.NewTextContent(message)), nil + }) + if err != nil { + panic(err) + } + + // Register current time tool + err = server.RegisterTool("time", "Returns the current time", func(args TimeArgs) (*mcp.ToolResponse, error) { + format := time.RFC3339 + if args.Format != "" { + format = args.Format + } + message := time.Now().Format(format) + return mcp.NewToolResponse(mcp.NewTextContent(message)), nil + }) if err != nil { panic(err) } + // Register example prompts + err = server.RegisterPrompt("uppercase", "Converts text to uppercase", func(args PromptArgs) (*mcp.PromptResponse, error) { + text := strings.ToUpper(args.Input) + return mcp.NewPromptResponse("uppercase", mcp.NewPromptMessage(mcp.NewTextContent(text), mcp.RoleUser)), nil + }) + if err != nil { + panic(err) + } + + err = server.RegisterPrompt("reverse", "Reverses the input text", func(args PromptArgs) (*mcp.PromptResponse, error) { + // Reverse the string + runes := []rune(args.Input) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + text := string(runes) + return mcp.NewPromptResponse("reverse", mcp.NewPromptMessage(mcp.NewTextContent(text), mcp.RoleUser)), nil + }) + if err != nil { + panic(err) + } + + // Start the server + if err := server.Serve(); err != nil { + panic(err) + } + // Keep the server running select {} } diff --git a/prompt_response_types.go b/prompt_response_types.go index a459343..3f17535 100644 --- a/prompt_response_types.go +++ b/prompt_response_types.go @@ -11,17 +11,17 @@ type baseGetPromptRequestParamsArguments struct { } // The server's response to a prompts/list request from the client. -type listPromptsResult struct { +type ListPromptsResponse struct { // Prompts corresponds to the JSON schema field "prompts". - Prompts []*promptSchema `json:"prompts" yaml:"prompts" mapstructure:"prompts"` + Prompts []*PromptSchema `json:"prompts" yaml:"prompts" mapstructure:"prompts"` // NextCursor is a cursor for pagination. If not nil, there are more prompts available. NextCursor *string `json:"nextCursor,omitempty" yaml:"nextCursor,omitempty" mapstructure:"nextCursor,omitempty"` } -// A promptSchema or prompt template that the server offers. -type promptSchema struct { +// A PromptSchema or prompt template that the server offers. +type PromptSchema struct { // A list of arguments to use for templating the prompt. - Arguments []promptSchemaArgument `json:"arguments,omitempty" yaml:"arguments,omitempty" mapstructure:"arguments,omitempty"` + Arguments []PromptSchemaArgument `json:"arguments,omitempty" yaml:"arguments,omitempty" mapstructure:"arguments,omitempty"` // An optional description of what this prompt provides Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"` @@ -30,7 +30,7 @@ type promptSchema struct { Name string `json:"name" yaml:"name" mapstructure:"name"` } -type promptSchemaArgument struct { +type PromptSchemaArgument struct { // A human-readable description of the argument. Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"` diff --git a/resource_response_types.go b/resource_response_types.go index fbc7c2b..5b16e27 100644 --- a/resource_response_types.go +++ b/resource_response_types.go @@ -7,15 +7,15 @@ type readResourceRequestParams struct { } // The server's response to a resources/list request from the client. -type listResourcesResult struct { +type ListResourcesResponse struct { // Resources corresponds to the JSON schema field "resources". - Resources []*resourceSchema `json:"resources" yaml:"resources" mapstructure:"resources"` + Resources []*ResourceSchema `json:"resources" yaml:"resources" mapstructure:"resources"` // NextCursor is a cursor for pagination. If not nil, there are more resources available. NextCursor *string `json:"nextCursor,omitempty" yaml:"nextCursor,omitempty" mapstructure:"nextCursor,omitempty"` } // A known resource that the server is capable of reading. -type resourceSchema struct { +type ResourceSchema struct { // Annotations corresponds to the JSON schema field "annotations". Annotations *Annotations `json:"annotations,omitempty" yaml:"annotations,omitempty" mapstructure:"annotations,omitempty"` diff --git a/server.go b/server.go index 5c3ab08..1b9fca9 100644 --- a/server.go +++ b/server.go @@ -116,7 +116,7 @@ type prompt struct { Name string Description string Handler func(baseGetPromptRequestParamsArguments) *promptResponseSent - PromptInputSchema *promptSchema + PromptInputSchema *PromptSchema } type tool struct { @@ -359,13 +359,13 @@ func createWrappedPromptHandler(userHandler any) func(baseGetPromptRequestParams // Description *string `json:"description" jsonschema:"description=The description to submit"` // } // Then we get the jsonschema for the struct where Title is a required field and Description is an optional field -func createPromptSchemaFromHandler(handler any) *promptSchema { +func createPromptSchemaFromHandler(handler any) *PromptSchema { handlerValue := reflect.ValueOf(handler) handlerType := handlerValue.Type() argumentType := handlerType.In(0) - promptSchema := promptSchema{ - Arguments: make([]promptSchemaArgument, argumentType.NumField()), + promptSchema := PromptSchema{ + Arguments: make([]PromptSchemaArgument, argumentType.NumField()), } for i := 0; i < argumentType.NumField(); i++ { @@ -385,7 +385,7 @@ func createPromptSchemaFromHandler(handler any) *promptSchema { } } - promptSchema.Arguments[i] = promptSchemaArgument{ + promptSchema.Arguments[i] = PromptSchemaArgument{ Name: fieldName, Description: description, Required: &required, @@ -499,7 +499,7 @@ func (s *Server) Serve() error { } func (s *Server) handleInitialize(_ *transport.BaseJSONRPCRequest, _ protocol.RequestHandlerExtra) (transport.JsonRpcBody, error) { - return initializeResult{ + return InitializeResponse{ Meta: nil, Capabilities: s.generateCapabilities(), Instructions: s.serverInstructions, @@ -608,21 +608,21 @@ func (s *Server) handleToolCalls(req *transport.BaseJSONRPCRequest, _ protocol.R } return toolToUse.Handler(params), nil } -func (s *Server) generateCapabilities() serverCapabilities { +func (s *Server) generateCapabilities() ServerCapabilities { t := false - return serverCapabilities{ - Tools: func() *serverCapabilitiesTools { - return &serverCapabilitiesTools{ + return ServerCapabilities{ + Tools: func() *ServerCapabilitiesTools { + return &ServerCapabilitiesTools{ ListChanged: &t, } }(), - Prompts: func() *serverCapabilitiesPrompts { - return &serverCapabilitiesPrompts{ + Prompts: func() *ServerCapabilitiesPrompts { + return &ServerCapabilitiesPrompts{ ListChanged: &t, } }(), - Resources: func() *serverCapabilitiesResources { - return &serverCapabilitiesResources{ + Resources: func() *ServerCapabilitiesResources { + return &ServerCapabilitiesResources{ ListChanged: &t, } }(), @@ -672,14 +672,15 @@ func (s *Server) handleListPrompts(request *transport.BaseJSONRPCRequest, extra } } - promptsToReturn := make([]*promptSchema, 0) + promptsToReturn := make([]*PromptSchema, 0) for i := startPosition; i < endPosition; i++ { schema := orderedPrompts[i].PromptInputSchema + schema.Description = &orderedPrompts[i].Description schema.Name = orderedPrompts[i].Name promptsToReturn = append(promptsToReturn, schema) } - return listPromptsResult{ + return ListPromptsResponse{ Prompts: promptsToReturn, NextCursor: func() *string { if s.paginationLimit != nil && len(promptsToReturn) >= *s.paginationLimit { @@ -735,10 +736,10 @@ func (s *Server) handleListResources(request *transport.BaseJSONRPCRequest, extr } } - resourcesToReturn := make([]*resourceSchema, 0) + resourcesToReturn := make([]*ResourceSchema, 0) for i := startPosition; i < endPosition; i++ { r := orderedResources[i] - resourcesToReturn = append(resourcesToReturn, &resourceSchema{ + resourcesToReturn = append(resourcesToReturn, &ResourceSchema{ Annotations: nil, Description: &r.Description, MimeType: &r.mimeType, @@ -747,7 +748,7 @@ func (s *Server) handleListResources(request *transport.BaseJSONRPCRequest, extr }) } - return listResourcesResult{ + return ListResourcesResponse{ Resources: resourcesToReturn, NextCursor: func() *string { if s.paginationLimit != nil && len(resourcesToReturn) >= *s.paginationLimit { diff --git a/server_test.go b/server_test.go index 8ee591d..89e26d1 100644 --- a/server_test.go +++ b/server_test.go @@ -318,7 +318,7 @@ func TestHandleListPromptsPagination(t *testing.T) { t.Fatal(err) } - promptsResp, ok := resp.(listPromptsResult) + promptsResp, ok := resp.(ListPromptsResponse) if !ok { t.Fatal("Expected listPromptsResult") } @@ -342,7 +342,7 @@ func TestHandleListPromptsPagination(t *testing.T) { t.Fatal(err) } - promptsResp, ok = resp.(listPromptsResult) + promptsResp, ok = resp.(ListPromptsResponse) if !ok { t.Fatal("Expected listPromptsResult") } @@ -366,7 +366,7 @@ func TestHandleListPromptsPagination(t *testing.T) { t.Fatal(err) } - promptsResp, ok = resp.(listPromptsResult) + promptsResp, ok = resp.(ListPromptsResponse) if !ok { t.Fatal("Expected listPromptsResult") } @@ -399,7 +399,7 @@ func TestHandleListPromptsPagination(t *testing.T) { t.Fatal(err) } - promptsResp, ok = resp.(listPromptsResult) + promptsResp, ok = resp.(ListPromptsResponse) if !ok { t.Fatal("Expected listPromptsResult") } @@ -443,7 +443,7 @@ func TestHandleListResourcesPagination(t *testing.T) { t.Fatal(err) } - resourcesResp, ok := resp.(listResourcesResult) + resourcesResp, ok := resp.(ListResourcesResponse) if !ok { t.Fatal("Expected listResourcesResult") } @@ -467,7 +467,7 @@ func TestHandleListResourcesPagination(t *testing.T) { t.Fatal(err) } - resourcesResp, ok = resp.(listResourcesResult) + resourcesResp, ok = resp.(ListResourcesResponse) if !ok { t.Fatal("Expected listResourcesResult") } @@ -491,7 +491,7 @@ func TestHandleListResourcesPagination(t *testing.T) { t.Fatal(err) } - resourcesResp, ok = resp.(listResourcesResult) + resourcesResp, ok = resp.(ListResourcesResponse) if !ok { t.Fatal("Expected listResourcesResult") } @@ -524,7 +524,7 @@ func TestHandleListResourcesPagination(t *testing.T) { t.Fatal(err) } - resourcesResp, ok = resp.(listResourcesResult) + resourcesResp, ok = resp.(ListResourcesResponse) if !ok { t.Fatal("Expected listResourcesResult") } diff --git a/tool_api.go b/tool_api.go index 4adbd78..bcbe35c 100644 --- a/tool_api.go +++ b/tool_api.go @@ -3,7 +3,7 @@ package mcp_golang // This is a union type of all the different ToolResponse that can be sent back to the client. // We allow creation through constructors only to make sure that the ToolResponse is valid. type ToolResponse struct { - Content []*Content + Content []*Content `json:"content" yaml:"content" mapstructure:"content"` } func NewToolResponse(content ...*Content) *ToolResponse { diff --git a/tool_response_types.go b/tool_response_types.go index 233cda5..affbb36 100644 --- a/tool_response_types.go +++ b/tool_response_types.go @@ -8,37 +8,37 @@ import ( // Capabilities that a server may support. Known capabilities are defined here, in // this schema, but this is not a closed set: any server can define its own, // additional capabilities. -type serverCapabilities struct { +type ServerCapabilities struct { // Experimental, non-standard capabilities that the server supports. - Experimental serverCapabilitiesExperimental `json:"experimental,omitempty" yaml:"experimental,omitempty" mapstructure:"experimental,omitempty"` + Experimental ServerCapabilitiesExperimental `json:"experimental,omitempty" yaml:"experimental,omitempty" mapstructure:"experimental,omitempty"` // Present if the server supports sending log messages to the client. - Logging serverCapabilitiesLogging `json:"logging,omitempty" yaml:"logging,omitempty" mapstructure:"logging,omitempty"` + Logging ServerCapabilitiesLogging `json:"logging,omitempty" yaml:"logging,omitempty" mapstructure:"logging,omitempty"` // Present if the server offers any prompt templates. - Prompts *serverCapabilitiesPrompts `json:"prompts,omitempty" yaml:"prompts,omitempty" mapstructure:"prompts,omitempty"` + Prompts *ServerCapabilitiesPrompts `json:"prompts,omitempty" yaml:"prompts,omitempty" mapstructure:"prompts,omitempty"` // Present if the server offers any resources to read. - Resources *serverCapabilitiesResources `json:"resources,omitempty" yaml:"resources,omitempty" mapstructure:"resources,omitempty"` + Resources *ServerCapabilitiesResources `json:"resources,omitempty" yaml:"resources,omitempty" mapstructure:"resources,omitempty"` // Present if the server offers any tools to call. - Tools *serverCapabilitiesTools `json:"tools,omitempty" yaml:"tools,omitempty" mapstructure:"tools,omitempty"` + Tools *ServerCapabilitiesTools `json:"tools,omitempty" yaml:"tools,omitempty" mapstructure:"tools,omitempty"` } // Experimental, non-standard capabilities that the server supports. -type serverCapabilitiesExperimental map[string]map[string]interface{} +type ServerCapabilitiesExperimental map[string]map[string]interface{} // Present if the server supports sending log messages to the client. -type serverCapabilitiesLogging map[string]interface{} +type ServerCapabilitiesLogging map[string]interface{} // Present if the server offers any prompt templates. -type serverCapabilitiesPrompts struct { +type ServerCapabilitiesPrompts struct { // Whether this server supports notifications for changes to the prompt list. ListChanged *bool `json:"listChanged,omitempty" yaml:"listChanged,omitempty" mapstructure:"listChanged,omitempty"` } // Present if the server offers any resources to read. -type serverCapabilitiesResources struct { +type ServerCapabilitiesResources struct { // Whether this server supports notifications for changes to the resource list. ListChanged *bool `json:"listChanged,omitempty" yaml:"listChanged,omitempty" mapstructure:"listChanged,omitempty"` @@ -47,20 +47,20 @@ type serverCapabilitiesResources struct { } // Present if the server offers any tools to call. -type serverCapabilitiesTools struct { +type ServerCapabilitiesTools struct { // Whether this server supports notifications for changes to the tool list. ListChanged *bool `json:"listChanged,omitempty" yaml:"listChanged,omitempty" mapstructure:"listChanged,omitempty"` } // After receiving an initialize request from the client, the server sends this // response. -type initializeResult struct { +type InitializeResponse struct { // This result property is reserved by the protocol to allow clients and servers // to attach additional metadata to their responses. Meta initializeResultMeta `json:"_meta,omitempty" yaml:"_meta,omitempty" mapstructure:"_meta,omitempty"` // Capabilities corresponds to the JSON schema field "capabilities". - Capabilities serverCapabilities `json:"capabilities" yaml:"capabilities" mapstructure:"capabilities"` + Capabilities ServerCapabilities `json:"capabilities" yaml:"capabilities" mapstructure:"capabilities"` // Instructions describing how to use the server and its features. // @@ -83,7 +83,7 @@ type initializeResult struct { type initializeResultMeta map[string]interface{} // UnmarshalJSON implements json.Unmarshaler. -func (j *initializeResult) UnmarshalJSON(b []byte) error { +func (j *InitializeResponse) UnmarshalJSON(b []byte) error { var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { return err @@ -97,12 +97,12 @@ func (j *initializeResult) UnmarshalJSON(b []byte) error { if _, ok := raw["serverInfo"]; raw != nil && !ok { return fmt.Errorf("field serverInfo in initializeResult: required") } - type Plain initializeResult + type Plain InitializeResponse var plain Plain if err := json.Unmarshal(b, &plain); err != nil { return err } - *j = initializeResult(plain) + *j = InitializeResponse(plain) return nil } From 2bb3ea9ddd78e9caf25e007a2ee840645936b622 Mon Sep 17 00:00:00 2001 From: Chris Battarbee Date: Tue, 21 Jan 2025 18:19:29 +0000 Subject: [PATCH 07/13] Update --- client.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/client.go b/client.go index 8764768..91a5f92 100644 --- a/client.go +++ b/client.go @@ -89,14 +89,19 @@ func (c *Client) ListTools(ctx context.Context, cursor *string) (*tools.ToolsRes } // CallTool calls a specific tool on the server with the provided arguments -func (c *Client) CallTool(ctx context.Context, name string, arguments json.RawMessage) (*ToolResponse, error) { +func (c *Client) CallTool(ctx context.Context, name string, arguments any) (*ToolResponse, error) { if !c.initialized { return nil, errors.New("client not initialized") } + argumentsJson, err := json.Marshal(arguments) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal arguments") + } + params := baseCallToolRequestParams{ Name: name, - Arguments: arguments, + Arguments: argumentsJson, } response, err := c.protocol.Request(ctx, "tools/call", params, nil) @@ -148,14 +153,19 @@ func (c *Client) ListPrompts(ctx context.Context, cursor *string) (*ListPromptsR } // GetPrompt retrieves a specific prompt from the server -func (c *Client) GetPrompt(ctx context.Context, name string, arguments json.RawMessage) (*PromptResponse, error) { +func (c *Client) GetPrompt(ctx context.Context, name string, arguments any) (*PromptResponse, error) { if !c.initialized { return nil, errors.New("client not initialized") } + argumentsJson, err := json.Marshal(arguments) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal arguments") + } + params := baseGetPromptRequestParamsArguments{ Name: name, - Arguments: arguments, + Arguments: argumentsJson, } response, err := c.protocol.Request(ctx, "prompts/get", params, nil) From f025320ae747ee8f4e127dba1992b8faa0a69f2c Mon Sep 17 00:00:00 2001 From: Chris Battarbee Date: Tue, 21 Jan 2025 18:25:11 +0000 Subject: [PATCH 08/13] Update --- examples/client/main.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/client/main.go b/examples/client/main.go index 077c938..bee1a18 100644 --- a/examples/client/main.go +++ b/examples/client/main.go @@ -117,10 +117,9 @@ func main() { promptArgs := map[string]interface{}{ "input": "Hello, Model Context Protocol!", } - promptArgsJson, _ := json.Marshal(promptArgs) log.Printf("\nCalling uppercase prompt:") - upperResponse, err := client.GetPrompt(context.Background(), "uppercase", promptArgsJson) + upperResponse, err := client.GetPrompt(context.Background(), "uppercase", promptArgs) if err != nil { log.Printf("Failed to get uppercase prompt: %v", err) } else if upperResponse != nil && len(upperResponse.Messages) > 0 && upperResponse.Messages[0].Content != nil { @@ -129,7 +128,7 @@ func main() { // Example of using the reverse prompt log.Printf("\nCalling reverse prompt:") - reverseResponse, err := client.GetPrompt(context.Background(), "reverse", promptArgsJson) + reverseResponse, err := client.GetPrompt(context.Background(), "reverse", promptArgs) if err != nil { log.Printf("Failed to get reverse prompt: %v", err) } else if reverseResponse != nil && len(reverseResponse.Messages) > 0 && reverseResponse.Messages[0].Content != nil { From ade1831f3c1b0ddf90dbc85ded530e9460d2b9e8 Mon Sep 17 00:00:00 2001 From: Chris Battarbee Date: Tue, 21 Jan 2025 18:28:33 +0000 Subject: [PATCH 09/13] docs: add README for client example explaining usage and features --- examples/client/README.md | 74 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 examples/client/README.md diff --git a/examples/client/README.md b/examples/client/README.md new file mode 100644 index 0000000..8164f77 --- /dev/null +++ b/examples/client/README.md @@ -0,0 +1,74 @@ +# MCP Client Example + +This example demonstrates how to use the Model Context Protocol (MCP) client to interact with an MCP server. The example includes both a client and a server implementation, showcasing various MCP features like tools and prompts. + +## Features Demonstrated + +- Client initialization and connection to server +- Listing available tools +- Calling different tools: + - Hello tool: Basic greeting functionality + - Calculate tool: Simple arithmetic operations + - Time tool: Current time formatting +- Listing available prompts +- Using prompts: + - Uppercase prompt: Converts text to uppercase + - Reverse prompt: Reverses input text + +## Running the Example + +1. Make sure you're in the `examples/client` directory: + ```bash + cd examples/client + ``` + +2. Run the example: + ```bash + go run main.go + ``` + +The program will: +1. Start a local MCP server (implemented in `server/main.go`) +2. Create an MCP client and connect to the server +3. Demonstrate various interactions with the server + +## Expected Output + +You should see output similar to this: + +``` +Available Tools: +Tool: hello. Description: A simple greeting tool +Tool: calculate. Description: A basic calculator +Tool: time. Description: Returns formatted current time + +Calling hello tool: +Hello response: Hello, World! + +Calling calculate tool: +Calculate response: Result of 10 + 5 = 15 + +Calling time tool: +Time response: [current time in format: 2006-01-02 15:04:05] + +Available Prompts: +Prompt: uppercase. Description: Converts text to uppercase +Prompt: reverse. Description: Reverses the input text + +Calling uppercase prompt: +Uppercase response: HELLO, MODEL CONTEXT PROTOCOL! + +Calling reverse prompt: +Reverse response: !locotorP txetnoC ledoM ,olleH +``` + +## Code Structure + +- `main.go`: Client implementation and example usage +- `server/main.go`: Example MCP server implementation with sample tools and prompts + +## Notes + +- The server is automatically started and stopped by the client program +- The example uses stdio transport for communication between client and server +- All tools and prompts are simple examples to demonstrate the protocol functionality \ No newline at end of file From 6fb440db84e35014b36e3431a2713862bf702074 Mon Sep 17 00:00:00 2001 From: Chris Battarbee Date: Tue, 21 Jan 2025 18:32:40 +0000 Subject: [PATCH 10/13] Update --- examples/client/main.go | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/examples/client/main.go b/examples/client/main.go index bee1a18..e37efd9 100644 --- a/examples/client/main.go +++ b/examples/client/main.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "log" "os/exec" @@ -11,11 +10,6 @@ import ( "github.com/metoro-io/mcp-golang/transport/stdio" ) -type toolResponseSent struct { - Content []*mcp_golang.Content `json:"content"` - IsError bool `json:"isError"` -} - func main() { // Start the server process cmd := exec.Command("go", "run", "./server/main.go") @@ -59,10 +53,9 @@ func main() { helloArgs := map[string]interface{}{ "name": "World", } - helloArgsJson, _ := json.Marshal(helloArgs) log.Println("\nCalling hello tool:") - helloResponse, err := client.CallTool(context.Background(), "hello", helloArgsJson) + helloResponse, err := client.CallTool(context.Background(), "hello", helloArgs) if err != nil { log.Printf("Failed to call hello tool: %v", err) } else if helloResponse != nil && len(helloResponse.Content) > 0 && helloResponse.Content[0].TextContent != nil { @@ -75,10 +68,9 @@ func main() { "a": 10, "b": 5, } - calcArgsJson, _ := json.Marshal(calcArgs) log.Println("\nCalling calculate tool:") - calcResponse, err := client.CallTool(context.Background(), "calculate", calcArgsJson) + calcResponse, err := client.CallTool(context.Background(), "calculate", calcArgs) if err != nil { log.Printf("Failed to call calculate tool: %v", err) } else if calcResponse != nil && len(calcResponse.Content) > 0 && calcResponse.Content[0].TextContent != nil { @@ -89,10 +81,9 @@ func main() { timeArgs := map[string]interface{}{ "format": "2006-01-02 15:04:05", } - timeArgsJson, _ := json.Marshal(timeArgs) log.Println("\nCalling time tool:") - timeResponse, err := client.CallTool(context.Background(), "time", timeArgsJson) + timeResponse, err := client.CallTool(context.Background(), "time", timeArgs) if err != nil { log.Printf("Failed to call time tool: %v", err) } else if timeResponse != nil && len(timeResponse.Content) > 0 && timeResponse.Content[0].TextContent != nil { From 31ebbb2c4e6b3ca9829d5565bb859afb7543e892 Mon Sep 17 00:00:00 2001 From: Chris Battarbee Date: Tue, 21 Jan 2025 18:35:55 +0000 Subject: [PATCH 11/13] docs: add comprehensive client usage documentation --- docs/client.mdx | 185 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/mint.json | 1 + 2 files changed, 186 insertions(+) create mode 100644 docs/client.mdx diff --git a/docs/client.mdx b/docs/client.mdx new file mode 100644 index 0000000..a3f240e --- /dev/null +++ b/docs/client.mdx @@ -0,0 +1,185 @@ +--- +title: 'Using the MCP Client' +description: 'Learn how to use the MCP client to interact with MCP servers' +--- + +# MCP Client Usage Guide + +The MCP client provides a simple and intuitive way to interact with MCP servers. This guide will walk you through initializing the client, connecting to a server, and using various MCP features. + +## Installation + +Add the MCP Golang package to your project: + +```bash +go get github.com/metoro-io/mcp-golang +``` + +## Basic Usage + +Here's a simple example of creating and initializing an MCP client: + +```go +import ( + mcp "github.com/metoro-io/mcp-golang" + "github.com/metoro-io/mcp-golang/transport/stdio" +) + +// Create a transport (stdio in this example) +transport := stdio.NewStdioServerTransportWithIO(stdout, stdin) + +// Create a new client +client := mcp.NewClient(transport) + +// Initialize the client +response, err := client.Initialize(context.Background()) +if err != nil { + log.Fatalf("Failed to initialize client: %v", err) +} +``` + +## Working with Tools + +### Listing Available Tools + +```go +tools, err := client.ListTools(context.Background(), nil) +if err != nil { + log.Fatalf("Failed to list tools: %v", err) +} + +for _, tool := range tools.Tools { + log.Printf("Tool: %s. Description: %s", tool.Name, *tool.Description) +} +``` + +### Calling a Tool + +```go +// Example: Calling a tool with arguments +args := map[string]interface{}{ + "param1": "value1", + "param2": 42, +} + +response, err := client.CallTool(context.Background(), "tool_name", args) +if err != nil { + log.Printf("Failed to call tool: %v", err) +} + +// Handle the response +if response != nil && len(response.Content) > 0 { + if response.Content[0].TextContent != nil { + log.Printf("Response: %s", response.Content[0].TextContent.Text) + } +} +``` + +## Working with Prompts + +### Listing Available Prompts + +```go +prompts, err := client.ListPrompts(context.Background(), nil) +if err != nil { + log.Printf("Failed to list prompts: %v", err) +} + +for _, prompt := range prompts.Prompts { + log.Printf("Prompt: %s. Description: %s", prompt.Name, *prompt.Description) +} +``` + +### Using a Prompt + +```go +args := map[string]interface{}{ + "input": "Hello, MCP!", +} + +response, err := client.GetPrompt(context.Background(), "prompt_name", args) +if err != nil { + log.Printf("Failed to get prompt: %v", err) +} + +if response != nil && len(response.Messages) > 0 { + log.Printf("Response: %s", response.Messages[0].Content.TextContent.Text) +} +``` + +## Working with Resources + +### Listing Resources + +```go +resources, err := client.ListResources(context.Background(), nil) +if err != nil { + log.Printf("Failed to list resources: %v", err) +} + +for _, resource := range resources.Resources { + log.Printf("Resource: %s", resource.Uri) +} +``` + +### Reading a Resource + +```go +resource, err := client.ReadResource(context.Background(), "resource_uri") +if err != nil { + log.Printf("Failed to read resource: %v", err) +} + +if resource != nil { + log.Printf("Resource content: %s", resource.Content) +} +``` + +## Pagination + +Both `ListTools` and `ListPrompts` support pagination. You can pass a cursor to get the next page of results: + +```go +var cursor *string +for { + tools, err := client.ListTools(context.Background(), cursor) + if err != nil { + log.Fatalf("Failed to list tools: %v", err) + } + + // Process tools... + + if tools.NextCursor == nil { + break // No more pages + } + cursor = tools.NextCursor +} +``` + +## Error Handling + +The client includes comprehensive error handling. All methods return an error as their second return value: + +```go +response, err := client.CallTool(context.Background(), "tool_name", args) +if err != nil { + switch { + case errors.Is(err, mcp.ErrClientNotInitialized): + // Handle initialization error + default: + // Handle other errors + } +} +``` + +## Best Practices + +1. Always initialize the client before making any calls +2. Use appropriate context management for timeouts and cancellation +3. Handle errors appropriately for your use case +4. Close or clean up resources when done +5. Use type-safe arguments when possible instead of raw maps + +## Complete Example + +For a complete working example, check out our [example client implementation](https://github.com/metoro-io/mcp-golang/tree/main/examples/client). \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index 012f3cd..67c435c 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -48,6 +48,7 @@ { "group": "Usage Guide", "pages": [ + "client", "tools" ] }, From f2d4d3fee1b6947ad2482c4f328865272b3e66c9 Mon Sep 17 00:00:00 2001 From: Chris Battarbee Date: Tue, 21 Jan 2025 18:36:30 +0000 Subject: [PATCH 12/13] Update --- examples/list_tools.go | 192 ----------------------------------------- 1 file changed, 192 deletions(-) delete mode 100644 examples/list_tools.go diff --git a/examples/list_tools.go b/examples/list_tools.go deleted file mode 100644 index 1209124..0000000 --- a/examples/list_tools.go +++ /dev/null @@ -1,192 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "log" - "os" - "os/exec" - "time" - - mcp "github.com/metoro-io/mcp-golang" - "github.com/metoro-io/mcp-golang/transport" - "github.com/metoro-io/mcp-golang/transport/stdio" -) - -// HelloArgs represents the arguments for the hello tool -type HelloArgs struct { - Name string `json:"name" jsonschema:"required,description=The name to say hello to"` -} - -// runServer starts an MCP server that registers a hello tool -func runServer() { - // Create a transport for the server - serverTransport := stdio.NewStdioServerTransport() - - // Create a new server with the transport - server := mcp.NewServer(serverTransport) - - // Register a simple tool with the server - err := server.RegisterTool("hello", "Says hello", func(args HelloArgs) (*mcp.ToolResponse, error) { - message := fmt.Sprintf("Hello, %s!", args.Name) - return mcp.NewToolResponse(mcp.NewTextContent(message)), nil - }) - if err != nil { - panic(err) - } - - // Start the server - err = server.Serve() - if err != nil { - panic(err) - } - - // Keep the server running - select {} -} - -// runClient starts an MCP client that connects to the server and interacts with it -func runClient() { - // Start the server process - cmd := exec.Command("go", "run", "-v", "-") - stdin, err := cmd.StdinPipe() - if err != nil { - log.Fatalf("Failed to get stdin pipe: %v", err) - } - stdout, err := cmd.StdoutPipe() - if err != nil { - log.Fatalf("Failed to get stdout pipe: %v", err) - } - stderr, err := cmd.StderrPipe() - if err != nil { - log.Fatalf("Failed to get stderr pipe: %v", err) - } - - // Pass the current source code to the server process - cmd.Env = append(cmd.Env, "RUN_AS_SERVER=true") - - if err := cmd.Start(); err != nil { - log.Fatalf("Failed to start server: %v", err) - } - defer cmd.Process.Kill() - - // Start a goroutine to read stderr - go func() { - buf := make([]byte, 1024) - for { - n, err := stderr.Read(buf) - if err != nil { - if err != io.EOF { - log.Printf("Error reading stderr: %v", err) - } - return - } - if n > 0 { - log.Printf("Server stderr: %s", string(buf[:n])) - } - } - }() - - // Helper function to send a request and read response - sendRequest := func(method string, params interface{}) (map[string]interface{}, error) { - paramsBytes, err := json.Marshal(params) - if err != nil { - return nil, err - } - - req := transport.BaseJSONRPCRequest{ - Jsonrpc: "2.0", - Method: method, - Params: json.RawMessage(paramsBytes), - Id: transport.RequestId(1), - } - - reqBytes, err := json.Marshal(req) - if err != nil { - return nil, err - } - reqBytes = append(reqBytes, '\n') - - log.Printf("Sending request: %s", string(reqBytes)) - _, err = stdin.Write(reqBytes) - if err != nil { - return nil, err - } - - // Read response with timeout - respChan := make(chan map[string]interface{}, 1) - errChan := make(chan error, 1) - - go func() { - decoder := json.NewDecoder(stdout) - var response map[string]interface{} - err := decoder.Decode(&response) - if err != nil { - errChan <- fmt.Errorf("failed to decode response: %v", err) - return - } - log.Printf("Got response: %+v", response) - respChan <- response - }() - - select { - case resp := <-respChan: - return resp, nil - case err := <-errChan: - return nil, err - case <-time.After(5 * time.Second): - return nil, fmt.Errorf("timeout waiting for response") - } - } - - // Initialize the server - resp, err := sendRequest("initialize", map[string]interface{}{ - "capabilities": map[string]interface{}{}, - }) - if err != nil { - log.Fatalf("Failed to initialize: %v", err) - } - log.Printf("Server initialized: %+v", resp) - - // List tools - resp, err = sendRequest("tools/list", map[string]interface{}{}) - if err != nil { - log.Fatalf("Failed to list tools: %v", err) - } - - // Print the available tools - fmt.Println("\nAvailable tools:") - tools := resp["result"].(map[string]interface{})["tools"].([]interface{}) - for _, t := range tools { - tool := t.(map[string]interface{}) - fmt.Printf("- %s: %s\n", tool["name"], tool["description"]) - } - - // Call the hello tool - callParams := map[string]interface{}{ - "name": "hello", - "arguments": map[string]interface{}{ - "name": "World", - }, - } - resp, err = sendRequest("tools/call", callParams) - if err != nil { - log.Fatalf("Failed to call hello tool: %v", err) - } - - // Print the response - fmt.Println("\nTool response:") - result := resp["result"].(map[string]interface{}) - content := result["content"].([]interface{})[0].(map[string]interface{}) - fmt.Println(content["text"]) -} - -func main() { - // If RUN_AS_SERVER is set, run as server, otherwise run as client - if os.Getenv("RUN_AS_SERVER") == "true" { - runServer() - } else { - runClient() - } -} From 269e64c0d64420794215c35ff7478586518fa310 Mon Sep 17 00:00:00 2001 From: Chris Battarbee Date: Tue, 21 Jan 2025 18:41:06 +0000 Subject: [PATCH 13/13] docs: update client examples to use typed structs instead of maps --- docs/client.mdx | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/docs/client.mdx b/docs/client.mdx index a3f240e..dddb700 100644 --- a/docs/client.mdx +++ b/docs/client.mdx @@ -56,13 +56,21 @@ for _, tool := range tools.Tools { ### Calling a Tool ```go -// Example: Calling a tool with arguments -args := map[string]interface{}{ - "param1": "value1", - "param2": 42, +// Define a type-safe struct for your tool arguments +type CalculateArgs struct { + Operation string `json:"operation"` + A int `json:"a"` + B int `json:"b"` } -response, err := client.CallTool(context.Background(), "tool_name", args) +// Create typed arguments +args := CalculateArgs{ + Operation: "add", + A: 10, + B: 5, +} + +response, err := client.CallTool(context.Background(), "calculate", args) if err != nil { log.Printf("Failed to call tool: %v", err) } @@ -93,8 +101,14 @@ for _, prompt := range prompts.Prompts { ### Using a Prompt ```go -args := map[string]interface{}{ - "input": "Hello, MCP!", +// Define a type-safe struct for your prompt arguments +type PromptArgs struct { + Input string `json:"input"` +} + +// Create typed arguments +args := PromptArgs{ + Input: "Hello, MCP!", } response, err := client.GetPrompt(context.Background(), "prompt_name", args) @@ -161,7 +175,7 @@ for { The client includes comprehensive error handling. All methods return an error as their second return value: ```go -response, err := client.CallTool(context.Background(), "tool_name", args) +response, err := client.CallTool(context.Background(), "calculate", args) if err != nil { switch { case errors.Is(err, mcp.ErrClientNotInitialized): @@ -178,7 +192,8 @@ if err != nil { 2. Use appropriate context management for timeouts and cancellation 3. Handle errors appropriately for your use case 4. Close or clean up resources when done -5. Use type-safe arguments when possible instead of raw maps +5. Define type-safe structs for tool and prompt arguments +6. Use struct tags to ensure correct JSON field names ## Complete Example