Skip to content

Commit

Permalink
Add basic client (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
Chrisbattarbee authored Jan 21, 2025
1 parent 2f1bb12 commit 142d45f
Show file tree
Hide file tree
Showing 18 changed files with 941 additions and 68 deletions.
9 changes: 9 additions & 0 deletions .cursorrules
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.idea
.idea
.vscode
269 changes: 269 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -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
}
37 changes: 36 additions & 1 deletion content_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package mcp_golang
import (
"encoding/json"
"fmt"

"github.com/tidwall/sjson"
)

Expand Down Expand Up @@ -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:
Expand Down
Loading

0 comments on commit 142d45f

Please sign in to comment.