Skip to content

Commit 03e484c

Browse files
authored
Merge pull request #5 from Dogacel/dogac/add-http-sse-support
2 parents a168174 + e74d14f commit 03e484c

File tree

6 files changed

+205
-5
lines changed

6 files changed

+205
-5
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,6 @@ test: check-go
5252
@echo "$(YELLOW)Running tests...$(NC)"
5353
go test -v ./...
5454

55-
lint: check_go
55+
lint: check-go
5656
@echo "$(BLUE)Running linter...$(NC)"
57-
golangci-lint run ./...
57+
golangci-lint run ./...

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ go install github.com/f/mcptools/cmd/mcptools@latest
6161

6262
## Transport Options
6363

64-
MCP currently supports one transport method for communicating with MCP servers:
64+
MCP currently supports two transport method for communicating with MCP servers:
6565

6666
### Stdio Transport
6767

@@ -72,6 +72,20 @@ useful for command-line tools that implement the MCP protocol.
7272
mcp tools npx -y @modelcontextprotocol/server-filesystem ~/Code
7373
```
7474

75+
### Http SSE Transport
76+
77+
Uses HTTP and Server-Sent Events (SSE) to communicate with an MCP server via JSON-RPC 2.0.
78+
This is useful for connecting to remote server that implement the MCP protocol.
79+
80+
```
81+
mcp tools http://127.0.0.1:3001
82+
83+
# As an example, you can use the everything sample server
84+
# docker run -p 3001:3001 --rm -it tzolov/mcp-everything-server:v1
85+
```
86+
87+
_Note:_ Currently HTTP SSE supports only MCP protocol version 2024-11-05.
88+
7589
## Output Formats
7690

7791
MCP supports three output formats:
@@ -202,7 +216,6 @@ mcp shell npx -y @modelcontextprotocol/server-filesystem ~/Code
202216

203217
The following features are planned for future releases:
204218

205-
- HTTP Transport: Add support for connecting to MCP servers over HTTP
206219
- Authentication: Support for secure authentication mechanisms
207220

208221
## License

cmd/mcptools/main.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ func createClient(args []string) (*client.Client, error) {
9696
return nil, errCommandRequired
9797
}
9898

99+
if len(args) == 1 && (strings.HasPrefix(args[0], "http://") || strings.HasPrefix(args[0], "https://")) {
100+
return client.NewHTTP(args[0]), nil
101+
}
102+
99103
return client.NewStdio(args), nil
100104
}
101105

@@ -519,7 +523,11 @@ func newShellCmd() *cobra.Command { //nolint:gocyclo
519523
os.Exit(1)
520524
}
521525

522-
mcpClient := client.NewStdio(parsedArgs)
526+
mcpClient, clientErr := createClient(parsedArgs)
527+
if clientErr != nil {
528+
fmt.Fprintf(os.Stderr, "Error: %v\n", clientErr)
529+
os.Exit(1)
530+
}
523531

524532
_, listErr := mcpClient.ListTools()
525533
if listErr != nil {

pkg/client/client.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Package client implements mcp client functionality.
44
package client
55

66
import (
7+
"fmt"
8+
"os"
79
"strings"
810

911
"github.com/f/mcptools/pkg/transport"
@@ -32,6 +34,19 @@ func NewStdio(command []string) *Client {
3234
}
3335
}
3436

37+
// NewHTTP creates a MCP client that communicates with a server via HTTP using JSON-RPC.
38+
func NewHTTP(address string) *Client {
39+
transport, err := transport.NewHTTP(address)
40+
if err != nil {
41+
fmt.Fprintf(os.Stderr, "Error creating HTTP transport: %s\n", err)
42+
os.Exit(1)
43+
}
44+
45+
return &Client{
46+
transport: transport,
47+
}
48+
}
49+
3550
// ListTools retrieves the list of available tools from the MCP server.
3651
func (c *Client) ListTools() (map[string]any, error) {
3752
return c.transport.Execute("tools/list", nil)

pkg/transport/http.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package transport
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"net/url"
11+
"os"
12+
"strings"
13+
"time"
14+
)
15+
16+
// HTTP implements the Transport interface by communicating with a MCP server over HTTP using JSON-RPC.
17+
type HTTP struct {
18+
eventCh chan string
19+
address string
20+
debug bool
21+
nextID int
22+
}
23+
24+
// NewHTTP creates a new Http transport that will execute the given command.
25+
// It communicates with the command using JSON-RPC over HTTP.
26+
// Currently Http transport is implements MCP's Final draft version 2024-11-05,
27+
// https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse
28+
func NewHTTP(address string) (*HTTP, error) {
29+
debug := os.Getenv("MCP_DEBUG") == "1"
30+
31+
_, uriErr := url.ParseRequestURI(address)
32+
if uriErr != nil {
33+
return nil, fmt.Errorf("invalid address: %w", uriErr)
34+
}
35+
36+
resp, err := http.Get(address + "/sse")
37+
if err != nil {
38+
return nil, fmt.Errorf("error sending request: %w", err)
39+
}
40+
41+
eventCh := make(chan string, 1)
42+
43+
go func() {
44+
defer func() {
45+
if closeErr := resp.Body.Close(); closeErr != nil {
46+
fmt.Fprintf(os.Stderr, "Failed to close response body: %v\n", closeErr)
47+
}
48+
}()
49+
50+
reader := bufio.NewReader(resp.Body)
51+
for {
52+
line, lineErr := reader.ReadString('\n')
53+
if lineErr != nil {
54+
fmt.Fprintf(os.Stderr, "SSE read error: %v\n", lineErr)
55+
return
56+
}
57+
line = strings.TrimSpace(line)
58+
if debug {
59+
fmt.Fprintf(os.Stderr, "DEBUG: Received SSE: %s\n", line)
60+
}
61+
if strings.HasPrefix(line, "data:") {
62+
data := strings.TrimSpace(line[5:])
63+
select {
64+
case eventCh <- data:
65+
default:
66+
}
67+
}
68+
}
69+
}()
70+
71+
// First event we receive from SSE is the message address. We will use this endpoint to keep
72+
// a session alive.
73+
var messageAddress string
74+
select {
75+
case msg := <-eventCh:
76+
messageAddress = msg
77+
case <-time.After(10 * time.Second):
78+
return nil, fmt.Errorf("timeout waiting for SSE response")
79+
}
80+
81+
return &HTTP{
82+
// Use the SSE message address as the base address for the HTTP transport
83+
address: address + messageAddress,
84+
nextID: 1,
85+
debug: debug,
86+
eventCh: eventCh,
87+
}, nil
88+
}
89+
90+
// Execute implements the Transport via JSON-RPC over HTTP.
91+
func (t *HTTP) Execute(method string, params any) (map[string]any, error) {
92+
if t.debug {
93+
fmt.Fprintf(os.Stderr, "DEBUG: Connecting to server: %s\n", t.address)
94+
}
95+
96+
request := Request{
97+
JSONRPC: "2.0",
98+
Method: method,
99+
ID: t.nextID,
100+
Params: params,
101+
}
102+
t.nextID++
103+
104+
requestJSON, err := json.Marshal(request)
105+
if err != nil {
106+
return nil, fmt.Errorf("error marshaling request: %w", err)
107+
}
108+
109+
requestJSON = append(requestJSON, '\n')
110+
111+
if t.debug {
112+
fmt.Fprintf(os.Stderr, "DEBUG: Sending request: %s\n", string(requestJSON))
113+
}
114+
115+
resp, err := http.Post(t.address, "application/json", bytes.NewBuffer(requestJSON))
116+
if err != nil {
117+
return nil, fmt.Errorf("error sending request: %w", err)
118+
}
119+
120+
if t.debug {
121+
fmt.Fprintf(os.Stderr, "DEBUG: Sent request to server\n")
122+
}
123+
124+
defer func() {
125+
if closeErr := resp.Body.Close(); closeErr != nil {
126+
fmt.Fprintf(os.Stderr, "Failed to close response body: %v\n", closeErr)
127+
}
128+
}()
129+
130+
body, err := io.ReadAll(resp.Body)
131+
if err != nil {
132+
return nil, fmt.Errorf("error reading response: %w", err)
133+
}
134+
135+
if t.debug {
136+
fmt.Fprintf(os.Stderr, "DEBUG: Read from server: %s\n", string(body))
137+
}
138+
139+
if len(body) == 0 {
140+
return nil, fmt.Errorf("no response from server")
141+
}
142+
143+
// After sending the request, we listen the SSE channel for the response
144+
var response Response
145+
select {
146+
case msg := <-t.eventCh:
147+
if unmarshalErr := json.Unmarshal([]byte(msg), &response); unmarshalErr != nil {
148+
return nil, fmt.Errorf("error unmarshaling response: %w, response: %s", unmarshalErr, msg)
149+
}
150+
case <-time.After(10 * time.Second):
151+
return nil, fmt.Errorf("timeout waiting for SSE response")
152+
}
153+
154+
if response.Error != nil {
155+
return nil, fmt.Errorf("RPC error %d: %s", response.Error.Code, response.Error.Message)
156+
}
157+
158+
if t.debug {
159+
fmt.Fprintf(os.Stderr, "DEBUG: Successfully parsed response\n")
160+
}
161+
162+
return response.Result, nil
163+
}

pkg/transport/transport.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Package transport contains implementatations for different transport options for MCP.
12
package transport
23

34
import (

0 commit comments

Comments
 (0)