Skip to content

Commit 142d45f

Browse files
Add basic client (#56)
1 parent 2f1bb12 commit 142d45f

18 files changed

+941
-68
lines changed

.cursorrules

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
This repository is an implementation of a golang model context protocol sdk.
2+
3+
Project docs are available in the docs folder.
4+
5+
Model context prtocol docs are available at https://modelcontextprotocol.io
6+
7+
Write tests for all new functionality.
8+
9+
Use go test to run tests after adding new functionality.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
.idea
1+
.idea
2+
.vscode

client.go

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package mcp_golang
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
7+
"github.com/metoro-io/mcp-golang/internal/protocol"
8+
"github.com/metoro-io/mcp-golang/internal/tools"
9+
"github.com/metoro-io/mcp-golang/transport"
10+
"github.com/pkg/errors"
11+
)
12+
13+
// Client represents an MCP client that can connect to and interact with MCP servers
14+
type Client struct {
15+
transport transport.Transport
16+
protocol *protocol.Protocol
17+
capabilities *ServerCapabilities
18+
initialized bool
19+
}
20+
21+
// NewClient creates a new MCP client with the specified transport
22+
func NewClient(transport transport.Transport) *Client {
23+
return &Client{
24+
transport: transport,
25+
protocol: protocol.NewProtocol(nil),
26+
}
27+
}
28+
29+
// Initialize connects to the server and retrieves its capabilities
30+
func (c *Client) Initialize(ctx context.Context) (*InitializeResponse, error) {
31+
if c.initialized {
32+
return nil, errors.New("client already initialized")
33+
}
34+
35+
err := c.protocol.Connect(c.transport)
36+
if err != nil {
37+
return nil, errors.Wrap(err, "failed to connect transport")
38+
}
39+
40+
// Make initialize request to server
41+
response, err := c.protocol.Request(ctx, "initialize", nil, nil)
42+
if err != nil {
43+
return nil, errors.Wrap(err, "failed to initialize")
44+
}
45+
46+
responseBytes, ok := response.(json.RawMessage)
47+
if !ok {
48+
return nil, errors.New("invalid response type")
49+
}
50+
51+
var initResult InitializeResponse
52+
err = json.Unmarshal(responseBytes, &initResult)
53+
if err != nil {
54+
return nil, errors.Wrap(err, "failed to unmarshal initialize response")
55+
}
56+
57+
c.capabilities = &initResult.Capabilities
58+
c.initialized = true
59+
return &initResult, nil
60+
}
61+
62+
// ListTools retrieves the list of available tools from the server
63+
func (c *Client) ListTools(ctx context.Context, cursor *string) (*tools.ToolsResponse, error) {
64+
if !c.initialized {
65+
return nil, errors.New("client not initialized")
66+
}
67+
68+
params := map[string]interface{}{
69+
"cursor": cursor,
70+
}
71+
72+
response, err := c.protocol.Request(ctx, "tools/list", params, nil)
73+
if err != nil {
74+
return nil, errors.Wrap(err, "failed to list tools")
75+
}
76+
77+
responseBytes, ok := response.(json.RawMessage)
78+
if !ok {
79+
return nil, errors.New("invalid response type")
80+
}
81+
82+
var toolsResponse tools.ToolsResponse
83+
err = json.Unmarshal(responseBytes, &toolsResponse)
84+
if err != nil {
85+
return nil, errors.Wrap(err, "failed to unmarshal tools response")
86+
}
87+
88+
return &toolsResponse, nil
89+
}
90+
91+
// CallTool calls a specific tool on the server with the provided arguments
92+
func (c *Client) CallTool(ctx context.Context, name string, arguments any) (*ToolResponse, error) {
93+
if !c.initialized {
94+
return nil, errors.New("client not initialized")
95+
}
96+
97+
argumentsJson, err := json.Marshal(arguments)
98+
if err != nil {
99+
return nil, errors.Wrap(err, "failed to marshal arguments")
100+
}
101+
102+
params := baseCallToolRequestParams{
103+
Name: name,
104+
Arguments: argumentsJson,
105+
}
106+
107+
response, err := c.protocol.Request(ctx, "tools/call", params, nil)
108+
if err != nil {
109+
return nil, errors.Wrap(err, "failed to call tool")
110+
}
111+
112+
responseBytes, ok := response.(json.RawMessage)
113+
if !ok {
114+
return nil, errors.New("invalid response type")
115+
}
116+
117+
var toolResponse ToolResponse
118+
err = json.Unmarshal(responseBytes, &toolResponse)
119+
if err != nil {
120+
return nil, errors.Wrap(err, "failed to unmarshal tool response")
121+
}
122+
123+
return &toolResponse, nil
124+
}
125+
126+
// ListPrompts retrieves the list of available prompts from the server
127+
func (c *Client) ListPrompts(ctx context.Context, cursor *string) (*ListPromptsResponse, error) {
128+
if !c.initialized {
129+
return nil, errors.New("client not initialized")
130+
}
131+
132+
params := map[string]interface{}{
133+
"cursor": cursor,
134+
}
135+
136+
response, err := c.protocol.Request(ctx, "prompts/list", params, nil)
137+
if err != nil {
138+
return nil, errors.Wrap(err, "failed to list prompts")
139+
}
140+
141+
responseBytes, ok := response.(json.RawMessage)
142+
if !ok {
143+
return nil, errors.New("invalid response type")
144+
}
145+
146+
var promptsResponse ListPromptsResponse
147+
err = json.Unmarshal(responseBytes, &promptsResponse)
148+
if err != nil {
149+
return nil, errors.Wrap(err, "failed to unmarshal prompts response")
150+
}
151+
152+
return &promptsResponse, nil
153+
}
154+
155+
// GetPrompt retrieves a specific prompt from the server
156+
func (c *Client) GetPrompt(ctx context.Context, name string, arguments any) (*PromptResponse, error) {
157+
if !c.initialized {
158+
return nil, errors.New("client not initialized")
159+
}
160+
161+
argumentsJson, err := json.Marshal(arguments)
162+
if err != nil {
163+
return nil, errors.Wrap(err, "failed to marshal arguments")
164+
}
165+
166+
params := baseGetPromptRequestParamsArguments{
167+
Name: name,
168+
Arguments: argumentsJson,
169+
}
170+
171+
response, err := c.protocol.Request(ctx, "prompts/get", params, nil)
172+
if err != nil {
173+
return nil, errors.Wrap(err, "failed to get prompt")
174+
}
175+
176+
responseBytes, ok := response.(json.RawMessage)
177+
if !ok {
178+
return nil, errors.New("invalid response type")
179+
}
180+
181+
var promptResponse PromptResponse
182+
err = json.Unmarshal(responseBytes, &promptResponse)
183+
if err != nil {
184+
return nil, errors.Wrap(err, "failed to unmarshal prompt response")
185+
}
186+
187+
return &promptResponse, nil
188+
}
189+
190+
// ListResources retrieves the list of available resources from the server
191+
func (c *Client) ListResources(ctx context.Context, cursor *string) (*ListResourcesResponse, error) {
192+
if !c.initialized {
193+
return nil, errors.New("client not initialized")
194+
}
195+
196+
params := map[string]interface{}{
197+
"cursor": cursor,
198+
}
199+
200+
response, err := c.protocol.Request(ctx, "resources/list", params, nil)
201+
if err != nil {
202+
return nil, errors.Wrap(err, "failed to list resources")
203+
}
204+
205+
responseBytes, ok := response.(json.RawMessage)
206+
if !ok {
207+
return nil, errors.New("invalid response type")
208+
}
209+
210+
var resourcesResponse ListResourcesResponse
211+
err = json.Unmarshal(responseBytes, &resourcesResponse)
212+
if err != nil {
213+
return nil, errors.Wrap(err, "failed to unmarshal resources response")
214+
}
215+
216+
return &resourcesResponse, nil
217+
}
218+
219+
// ReadResource reads a specific resource from the server
220+
func (c *Client) ReadResource(ctx context.Context, uri string) (*ResourceResponse, error) {
221+
if !c.initialized {
222+
return nil, errors.New("client not initialized")
223+
}
224+
225+
params := readResourceRequestParams{
226+
Uri: uri,
227+
}
228+
229+
response, err := c.protocol.Request(ctx, "resources/read", params, nil)
230+
if err != nil {
231+
return nil, errors.Wrap(err, "failed to read resource")
232+
}
233+
234+
responseBytes, ok := response.(json.RawMessage)
235+
if !ok {
236+
return nil, errors.New("invalid response type")
237+
}
238+
239+
var resourceResponse resourceResponseSent
240+
err = json.Unmarshal(responseBytes, &resourceResponse)
241+
if err != nil {
242+
return nil, errors.Wrap(err, "failed to unmarshal resource response")
243+
}
244+
245+
if resourceResponse.Error != nil {
246+
return nil, resourceResponse.Error
247+
}
248+
249+
return resourceResponse.Response, nil
250+
}
251+
252+
// Ping sends a ping request to the server to check connectivity
253+
func (c *Client) Ping(ctx context.Context) error {
254+
if !c.initialized {
255+
return errors.New("client not initialized")
256+
}
257+
258+
_, err := c.protocol.Request(ctx, "ping", nil, nil)
259+
if err != nil {
260+
return errors.Wrap(err, "failed to ping server")
261+
}
262+
263+
return nil
264+
}
265+
266+
// GetCapabilities returns the server capabilities obtained during initialization
267+
func (c *Client) GetCapabilities() *ServerCapabilities {
268+
return c.capabilities
269+
}

content_api.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package mcp_golang
33
import (
44
"encoding/json"
55
"fmt"
6+
67
"github.com/tidwall/sjson"
78
)
89

@@ -111,9 +112,43 @@ type Content struct {
111112
Annotations *Annotations
112113
}
113114

115+
func (c *Content) UnmarshalJSON(b []byte) error {
116+
type typeWrapper struct {
117+
Type ContentType `json:"type" yaml:"type" mapstructure:"type"`
118+
Text *string `json:"text" yaml:"text" mapstructure:"text"`
119+
Image *string `json:"image" yaml:"image" mapstructure:"image"`
120+
Annotations *Annotations `json:"annotations" yaml:"annotations" mapstructure:"annotations"`
121+
EmbeddedResource *EmbeddedResource `json:"resource" yaml:"resource" mapstructure:"resource"`
122+
}
123+
var tw typeWrapper
124+
err := json.Unmarshal(b, &tw)
125+
if err != nil {
126+
return err
127+
}
128+
switch tw.Type {
129+
case ContentTypeText:
130+
c.Type = ContentTypeText
131+
case ContentTypeImage:
132+
c.Type = ContentTypeImage
133+
case ContentTypeEmbeddedResource:
134+
c.Type = ContentTypeEmbeddedResource
135+
default:
136+
return fmt.Errorf("unknown content type: %s", tw.Type)
137+
}
138+
139+
switch c.Type {
140+
case ContentTypeText:
141+
c.TextContent = &TextContent{Text: *tw.Text}
142+
default:
143+
return fmt.Errorf("unknown content type: %s", c.Type)
144+
}
145+
146+
return nil
147+
}
148+
114149
// Custom JSON marshaling for ToolResponse Content
115150
func (c Content) MarshalJSON() ([]byte, error) {
116-
rawJson := []byte{}
151+
var rawJson []byte
117152

118153
switch c.Type {
119154
case ContentTypeText:

0 commit comments

Comments
 (0)