Skip to content

Commit 55c8682

Browse files
author
Doğaç Eldenk
committed
Implement HTTP transport using SSE
Signed-off-by: Doğaç Eldenk <dogac@carbonhealth.com>
1 parent a168174 commit 55c8682

File tree

4 files changed

+192
-3
lines changed

4 files changed

+192
-3
lines changed

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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ func NewStdio(command []string) *Client {
3232
}
3333
}
3434

35+
// NewHttp creates a MCP client that communicates with a server via HTTP using JSON-RPC.
36+
func NewHttp(address string) *Client {
37+
return &Client{
38+
transport: transport.NewHttp(address),
39+
}
40+
}
41+
3542
// ListTools retrieves the list of available tools from the MCP server.
3643
func (c *Client) ListTools() (map[string]any, error) {
3744
return c.transport.Execute("tools/list", nil)

pkg/transport/http.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
address string
19+
nextID int
20+
debug bool
21+
eventCh chan string
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 {
29+
debug := os.Getenv("MCP_DEBUG") == "1"
30+
31+
_, err := url.ParseRequestURI(address)
32+
if err != nil {
33+
fmt.Fprintf(os.Stderr, "invalid address: %s", err)
34+
os.Exit(1)
35+
}
36+
37+
resp, err := http.Get(address + "/sse")
38+
if err != nil {
39+
fmt.Fprintf(os.Stderr, "error sending request: %s", err)
40+
os.Exit(1)
41+
}
42+
43+
eventCh := make(chan string, 1)
44+
45+
go func() {
46+
defer resp.Body.Close()
47+
reader := bufio.NewReader(resp.Body)
48+
for {
49+
select {
50+
default:
51+
line, err := reader.ReadString('\n')
52+
if err != nil {
53+
fmt.Fprintf(os.Stderr, "SSE read error: %s", err)
54+
return
55+
}
56+
line = strings.TrimSpace(line)
57+
if debug {
58+
fmt.Fprintf(os.Stderr, "DEBUG: Received SSE: %s\n", line)
59+
}
60+
if strings.HasPrefix(line, "data:") {
61+
data := strings.TrimSpace(line[5:])
62+
select {
63+
case eventCh <- data:
64+
default:
65+
}
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+
fmt.Errorf("timeout waiting for SSE response")
79+
os.Exit(1)
80+
}
81+
82+
return &Http{
83+
// Use the SSE message address as the base address for the HTTP transport
84+
address: address + messageAddress,
85+
nextID: 1,
86+
debug: debug,
87+
eventCh: eventCh,
88+
}
89+
}
90+
91+
// Execute implements the Transport via JSON-RPC over HTTP.
92+
func (t *Http) Execute(method string, params any) (map[string]any, error) {
93+
if t.debug {
94+
fmt.Fprintf(os.Stderr, "DEBUG: Connecting to server: %s\n", t.address)
95+
}
96+
97+
request := Request{
98+
JSONRPC: "2.0",
99+
Method: method,
100+
ID: t.nextID,
101+
Params: params,
102+
}
103+
t.nextID++
104+
105+
requestJSON, err := json.Marshal(request)
106+
if err != nil {
107+
return nil, fmt.Errorf("error marshaling request: %w", err)
108+
}
109+
110+
requestJSON = append(requestJSON, '\n')
111+
112+
if t.debug {
113+
fmt.Fprintf(os.Stderr, "DEBUG: Sending request: %s\n", string(requestJSON))
114+
}
115+
116+
resp, err := http.Post(t.address, "application/json", bytes.NewBuffer(requestJSON))
117+
if err != nil {
118+
return nil, fmt.Errorf("error sending request: %w", err)
119+
}
120+
121+
if t.debug {
122+
fmt.Fprintf(os.Stderr, "DEBUG: Sent request to server\n")
123+
}
124+
125+
defer resp.Body.Close()
126+
127+
body, err := io.ReadAll(resp.Body)
128+
if err != nil {
129+
return nil, fmt.Errorf("error reading response: %w", err)
130+
}
131+
132+
if t.debug {
133+
fmt.Fprintf(os.Stderr, "DEBUG: Read from server: %s\n", string(body))
134+
}
135+
136+
if len(body) == 0 {
137+
return nil, fmt.Errorf("no response from server")
138+
}
139+
140+
// After sending the request, we listen the SSE channel for the response
141+
var response Response
142+
select {
143+
case msg := <-t.eventCh:
144+
if unmarshalErr := json.Unmarshal([]byte(msg), &response); unmarshalErr != nil {
145+
return nil, fmt.Errorf("error unmarshaling response: %w, response: %s", unmarshalErr, msg)
146+
}
147+
case <-time.After(10 * time.Second):
148+
fmt.Errorf("timeout waiting for SSE response")
149+
os.Exit(1)
150+
}
151+
152+
if response.Error != nil {
153+
return nil, fmt.Errorf("RPC error %d: %s", response.Error.Code, response.Error.Message)
154+
}
155+
156+
if t.debug {
157+
fmt.Fprintf(os.Stderr, "DEBUG: Successfully parsed response\n")
158+
}
159+
160+
return response.Result, nil
161+
}

0 commit comments

Comments
 (0)