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 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 new file mode 100644 index 0000000..91a5f92 --- /dev/null +++ b/client.go @@ -0,0 +1,269 @@ +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) (*InitializeResponse, error) { + if c.initialized { + return nil, errors.New("client already initialized") + } + + err := c.protocol.Connect(c.transport) + if err != nil { + 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 nil, errors.Wrap(err, "failed to initialize") + } + + responseBytes, ok := response.(json.RawMessage) + if !ok { + return nil, errors.New("invalid response type") + } + + var initResult InitializeResponse + err = json.Unmarshal(responseBytes, &initResult) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal initialize response") + } + + c.capabilities = &initResult.Capabilities + c.initialized = true + return &initResult, 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.(json.RawMessage) + 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 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: argumentsJson, + } + + 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.(json.RawMessage) + if !ok { + return nil, errors.New("invalid response type") + } + + var toolResponse ToolResponse + err = json.Unmarshal(responseBytes, &toolResponse) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal tool response") + } + + return &toolResponse, nil +} + +// ListPrompts retrieves the list of available prompts from the server +func (c *Client) ListPrompts(ctx context.Context, cursor *string) (*ListPromptsResponse, 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.(json.RawMessage) + if !ok { + return nil, errors.New("invalid response type") + } + + var promptsResponse ListPromptsResponse + 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 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: argumentsJson, + } + + 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.(json.RawMessage) + if !ok { + return nil, errors.New("invalid response type") + } + + var promptResponse PromptResponse + err = json.Unmarshal(responseBytes, &promptResponse) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal prompt response") + } + + return &promptResponse, nil +} + +// ListResources retrieves the list of available resources from the server +func (c *Client) ListResources(ctx context.Context, cursor *string) (*ListResourcesResponse, 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.(json.RawMessage) + if !ok { + return nil, errors.New("invalid response type") + } + + var resourcesResponse ListResourcesResponse + 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.(json.RawMessage) + 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 +} diff --git a/content_api.go b/content_api.go index dec3b4c..b111593 100644 --- a/content_api.go +++ b/content_api.go @@ -3,6 +3,7 @@ package mcp_golang import ( "encoding/json" "fmt" + "github.com/tidwall/sjson" ) @@ -111,9 +112,43 @@ 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) { - rawJson := []byte{} + var rawJson []byte switch c.Type { case ContentTypeText: diff --git a/docs/client.mdx b/docs/client.mdx new file mode 100644 index 0000000..dddb700 --- /dev/null +++ b/docs/client.mdx @@ -0,0 +1,200 @@ +--- +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 +// Define a type-safe struct for your tool arguments +type CalculateArgs struct { + Operation string `json:"operation"` + A int `json:"a"` + B int `json:"b"` +} + +// 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) +} + +// 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 +// 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) +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(), "calculate", 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. Define type-safe structs for tool and prompt arguments +6. Use struct tags to ensure correct JSON field names + +## 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" ] }, 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 diff --git a/examples/client/main.go b/examples/client/main.go new file mode 100644 index 0000000..e37efd9 --- /dev/null +++ b/examples/client/main.go @@ -0,0 +1,129 @@ +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) + } + + // List available tools + tools, err := client.ListTools(context.Background(), nil) + if err != nil { + log.Fatalf("Failed to list tools: %v", err) + } + + log.Println("Available Tools:") + for _, tool := range tools.Tools { + desc := "" + if tool.Description != nil { + desc = *tool.Description + } + log.Printf("Tool: %s. Description: %s", tool.Name, desc) + } + + // Example of calling the hello tool + helloArgs := map[string]interface{}{ + "name": "World", + } + + log.Println("\nCalling hello tool:") + 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 { + 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, + } + + log.Println("\nCalling calculate tool:") + 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 { + 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", + } + + log.Println("\nCalling time tool:") + 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 { + 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!", + } + + log.Printf("\nCalling uppercase prompt:") + 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 { + 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", 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 { + 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 new file mode 100644 index 0000000..87caa8b --- /dev/null +++ b/examples/client/server/main.go @@ -0,0 +1,117 @@ +package main + +import ( + "fmt" + "strings" + "time" + + 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"` +} + +// 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() + + // Create a new server with the transport + server := mcp.NewServer(serverTransport) + + // 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 + }) + if err != nil { + panic(err) + } + + // 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/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 78c7009..41810df 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/metoro-io/mcp-golang -go 1.23.4 +go 1.21 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 ) 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 dda1e22..1b9fca9 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 @@ -115,7 +116,7 @@ type prompt struct { Name string Description string Handler func(baseGetPromptRequestParamsArguments) *promptResponseSent - PromptInputSchema *promptSchema + PromptInputSchema *PromptSchema } type tool struct { @@ -358,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++ { @@ -384,7 +385,7 @@ func createPromptSchemaFromHandler(handler any) *promptSchema { } } - promptSchema.Arguments[i] = promptSchemaArgument{ + promptSchema.Arguments[i] = PromptSchemaArgument{ Name: fieldName, Description: description, Required: &required, @@ -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 @@ -498,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, @@ -607,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, } }(), @@ -671,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 { @@ -734,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, @@ -746,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 } 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