Skip to content

Commit 803a3b7

Browse files
authored
feat: define two endpoints for SSE to match MCP Spec (#32)
* feat: define two endpoints for SSE to match MCP Spec * Added integration tests
1 parent 7438ec6 commit 803a3b7

File tree

6 files changed

+188
-18
lines changed

6 files changed

+188
-18
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,14 @@ To run the server with the default StreamableHTTP transport:
7474
./build/gofetch
7575
```
7676

77-
The server will start and expose:
77+
The server will start and expose endpoints as mandated by the MCP specification:
7878

79-
- MCP endpoint: `http://localhost:8080/mcp`
79+
**Streamable HTTP Transport (default):**
80+
- MCP endpoint: `http://localhost:8080/mcp` (for streaming responses and commands)
81+
82+
**SSE Transport:**
83+
- SSE endpoint: `http://localhost:8080/sse`
84+
- Messages endpoint: `http://localhost:8080/messages`
8085

8186
#### Command Line Options
8287

Taskfile.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ tasks:
4242
desc: Run integration tests
4343
cmds:
4444
- ./test/integration-test.sh
45+
- ./test/integration-endpoints.sh
4546

4647
clean:
4748
desc: Clean the build directory

USAGE.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ This document provides examples of how to interact with the GoFetch MCP server u
88
TRANSPORT=sse MCP_PORT=8080 ./gofetch
99
```
1010

11+
### Available Endpoints:
12+
- **SSE endpoint**: `GET /sse` - for receiving server-to-client messages
13+
- **Messages endpoint**: `POST /messages` - for sending client-to-server commands
14+
1115
SSE transport uses query parameters for session management, not headers.
1216

1317
### 1. Initialize Session (GET request to establish session)
@@ -23,13 +27,13 @@ This will establish a session and return a URL with a session ID like:
2327
data: /sse?sessionid=TE2NFYZFIWX2E6RIUJX7TNO7H5
2428
```
2529

26-
### 2. Send Initialize Message (POST to session endpoint)
30+
### 2. Send Initialize Message (POST to messages endpoint)
2731
```bash
28-
# Use the session ID from step 1 in the URL
32+
# Use the session ID from step 1 and send to the messages endpoint
2933
curl -X POST \
3034
-H "Content-Type: application/json" \
3135
-H "Mcp-Protocol-Version: 2025-06-18" \
32-
"http://localhost:8080/sse?sessionid=<sessionId>" \
36+
"http://localhost:8080/messages?sessionid=<sessionId>" \
3337
-d '{
3438
"jsonrpc": "2.0",
3539
"id": 1,
@@ -50,7 +54,7 @@ curl -X POST \
5054
curl -X POST \
5155
-H "Content-Type: application/json" \
5256
-H "Mcp-Protocol-Version: 2025-06-18" \
53-
"http://localhost:8080/sse?sessionid=<sessionId>" \
57+
"http://localhost:8080/messages?sessionid=<sessionId>" \
5458
-d '{
5559
"jsonrpc": "2.0",
5660
"id": 2,
@@ -63,7 +67,7 @@ curl -X POST \
6367
curl -X POST \
6468
-H "Content-Type: application/json" \
6569
-H "Mcp-Protocol-Version: 2025-06-18" \
66-
"http://localhost:8080/sse?sessionid=<sessionId>" \
70+
"http://localhost:8080/messages?sessionid=<sessionId>" \
6771
-d '{
6872
"jsonrpc": "2.0",
6973
"id": 3,
@@ -84,6 +88,9 @@ curl -X POST \
8488
TRANSPORT=streamable-http MCP_PORT=8080 ./gofetch
8589
```
8690

91+
### Available Endpoints:
92+
- **MCP endpoint**: `GET/POST /mcp` - for streaming server-to-client communication and client-to-server commands
93+
8794
### 1. Initialize Session and Get Session ID
8895

8996
First, initialize the session and capture the session ID from the response headers:
@@ -174,8 +181,7 @@ The fetch tool accepts the following parameters:
174181

175182
| Feature | Streamable HTTP (Modern) | HTTP+SSE (Legacy) |
176183
|---------|-------------------------|-------------------|
177-
| Endpoints | Single `/mcp` for all operations | Two-endpoint process: `GET /mcp` or `GET /sse` to establish session, `POST /messages` or `POST /sse?sessionid=xxx` to send messages |
178-
| Client-to-Server | `POST /mcp` | `POST /messages?sessionid=xxx` |
179-
| Server-to-Client | Same POST response (streamed) | `GET /mcp` (SSE stream) |
184+
| Endpoint | `GET/POST /mcp` for streaming responses and commands | `GET /sse` for streaming responses |
185+
| Command Endpoint | Same as above | `POST /messages` for client commands |
180186
| Session Identification | Uses **HTTP headers** for session management (`Mcp-Session-Id` header) | Uses **query parameters** in URL for session management (`?sessionid=...`) |
181187
| Session Termination | `DELETE /mcp` | Connection close |

pkg/server/server.go

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,25 +53,59 @@ func NewFetchServer(cfg config.Config) *FetchServer {
5353
contentProcessor := processor.NewContentProcessor()
5454
httpFetcher := fetcher.NewHTTPFetcher(client, robotsChecker, contentProcessor, cfg.UserAgent)
5555

56+
fs := &FetchServer{
57+
config: cfg,
58+
fetcher: httpFetcher,
59+
}
60+
5661
// Create MCP server with proper implementation details
5762
// Capabilities are automatically generated based on registered tools/resources
5863
mcpServer := mcp.NewServer(&mcp.Implementation{
5964
Name: config.ServerName,
6065
Version: config.ServerVersion,
61-
}, nil)
66+
}, &mcp.ServerOptions{
67+
InitializedHandler: fs.handleInitialized,
68+
})
6269

63-
fs := &FetchServer{
64-
config: cfg,
65-
fetcher: httpFetcher,
66-
mcpServer: mcpServer,
67-
}
70+
fs.mcpServer = mcpServer
6871

6972
// Setup tools
7073
fs.setupTools()
7174

7275
return fs
7376
}
7477

78+
// handleInitialized sends an endpoint event to the client after initialization
79+
func (fs *FetchServer) handleInitialized(ctx context.Context, session *mcp.ServerSession, _ *mcp.InitializedParams) {
80+
// Build the endpoint URI based on the current server configuration
81+
var endpointURI string
82+
switch fs.config.Transport {
83+
case config.TransportSSE:
84+
endpointURI = fmt.Sprintf("http://localhost:%d/messages", fs.config.Port)
85+
case config.TransportStreamableHTTP:
86+
endpointURI = fmt.Sprintf("http://localhost:%d/mcp", fs.config.Port)
87+
default:
88+
endpointURI = fmt.Sprintf("http://localhost:%d/messages", fs.config.Port)
89+
}
90+
91+
// Send endpoint event as a log message with structured data
92+
err := session.Log(ctx, &mcp.LoggingMessageParams{
93+
Level: "info",
94+
Data: map[string]interface{}{
95+
"type": "endpoint_event",
96+
"message": "Client must use this endpoint for sending messages",
97+
"endpoint_uri": endpointURI,
98+
},
99+
Logger: "gofetch-server",
100+
})
101+
102+
if err != nil {
103+
log.Printf("Failed to send endpoint event: %v", err)
104+
} else {
105+
log.Printf("Sent endpoint event to client %s: %s", session.ID(), endpointURI)
106+
}
107+
}
108+
75109
// setupTools registers the fetch tool with the MCP server
76110
func (fs *FetchServer) setupTools() {
77111
fetchTool := &mcp.Tool{
@@ -145,6 +179,9 @@ func (fs *FetchServer) startSSEServer() error {
145179
// Handle SSE endpoint
146180
mux.Handle("/sse", sseHandler)
147181

182+
// HTTP POST endpoint for client-to-server communication
183+
mux.Handle("/messages", sseHandler)
184+
148185
// Start HTTP server
149186
server := &http.Server{
150187
Addr: ":" + strconv.Itoa(fs.config.Port),
@@ -199,9 +236,10 @@ func (fs *FetchServer) logServerStartup() {
199236
// Log endpoint based on transport
200237
switch fs.config.Transport {
201238
case config.TransportSSE:
202-
log.Printf("SSE endpoint: http://localhost:%d/sse", fs.config.Port)
239+
log.Printf("SSE endpoint (server-to-client): http://localhost:%d/sse", fs.config.Port)
240+
log.Printf("Messages endpoint (client-to-server): http://localhost:%d/messages", fs.config.Port)
203241
case config.TransportStreamableHTTP:
204-
log.Printf("Message endpoint: http://localhost:%d/mcp", fs.config.Port)
242+
log.Printf("MCP endpoint (streaming and commands): http://localhost:%d/mcp", fs.config.Port)
205243
}
206244

207245
log.Printf("=== Server starting ===")

test/integration-endpoints.sh

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#!/bin/bash
2+
3+
# Integration test script for gofetch MCP server
4+
# Tests SSE and streamable-http transports using client binary
5+
set -e
6+
7+
echo "Running integration tests for gofetch MCP server..."
8+
9+
# Test 1: Build the image using task build-image
10+
echo "🏗️ Building Docker image using task build-image..."
11+
task build-image
12+
if [ $? -eq 0 ]; then
13+
echo "✓ Docker image built successfully using task build-image"
14+
else
15+
echo "✗ Failed to build Docker image using task build-image"
16+
exit 1
17+
fi
18+
19+
IMAGE_NAME="ghcr.io/stackloklabs/gofetch/server:latest"
20+
21+
cleanup() {
22+
docker rm -f gofetch-sse-endpoints-test > /dev/null 2>&1 || true
23+
docker rm -f gofetch-http-endpoints-test > /dev/null 2>&1 || true
24+
}
25+
trap cleanup EXIT
26+
27+
check_status() {
28+
local method=$1
29+
local url=$2
30+
local data=$3
31+
local header=$4
32+
33+
if [ "$method" = "GET" ]; then
34+
curl -s -o /dev/null -m 5 -w "%{http_code}" ${header} "$url"
35+
else
36+
curl -s -o /dev/null -m 5 -w "%{http_code}" -X POST -H 'Content-Type: application/json' ${header} -d "${data}" "$url"
37+
fi
38+
}
39+
40+
###############################################################
41+
# SSE transport endpoint checks
42+
###############################################################
43+
echo ""
44+
echo "🌊 ========== SSE ENDPOINT CHECKS =========="
45+
docker run --rm -d --name gofetch-sse-endpoints-test -p 8080:8080 "$IMAGE_NAME" --transport sse --port 8080 > /dev/null 2>&1
46+
47+
if docker ps | grep -q gofetch-sse-endpoints-test; then
48+
echo "✓ SSE container started on port 8080"
49+
else
50+
echo "✗ Failed to start SSE container"
51+
exit 1
52+
fi
53+
54+
echo "🔎 Checking /sse (GET)"
55+
# For SSE, the connection stays open; curl may timeout with exit 28. Capture headers and validate.
56+
SSE_HEADERS=$(curl -sS -m 5 -D - -o /dev/null -H 'Accept: text/event-stream' http://localhost:8080/sse || true)
57+
if echo "$SSE_HEADERS" | grep -qiE '^HTTP/[^ ]+ 200' && echo "$SSE_HEADERS" | grep -qi 'content-type: *text/event-stream'; then
58+
echo "✓ /sse endpoint reachable (200, text/event-stream)"
59+
else
60+
echo "! /sse endpoint did not return expected headers"
61+
echo "$SSE_HEADERS" | sed 's/^/H: /'
62+
exit 1
63+
fi
64+
65+
echo "🔎 Checking /messages (POST)"
66+
MSG_STATUS=$(check_status POST "http://localhost:8080/messages" '{}' "" || true)
67+
if [ "$MSG_STATUS" = "200" ] || [ "$MSG_STATUS" = "204" ] || [ "$MSG_STATUS" = "400" ]; then
68+
echo "✓ /messages endpoint reachable ($MSG_STATUS)"
69+
else
70+
echo "! /messages endpoint not reachable (status: $MSG_STATUS)"
71+
exit 1
72+
fi
73+
74+
docker rm -f gofetch-sse-endpoints-test > /dev/null 2>&1 || true
75+
echo "✓ SSE container shut down"
76+
77+
###############################################################
78+
# Streamable HTTP transport endpoint checks
79+
###############################################################
80+
echo ""
81+
echo "🌐 ========== STREAMABLE-HTTP ENDPOINT CHECKS =========="
82+
docker run --rm -d --name gofetch-http-endpoints-test -p 8081:8081 "$IMAGE_NAME" --transport streamable-http --port 8081 > /dev/null 2>&1
83+
84+
if docker ps | grep -q gofetch-http-endpoints-test; then
85+
echo "✓ Streamable HTTP container started on port 8081"
86+
else
87+
echo "✗ Failed to start Streamable HTTP container"
88+
exit 1
89+
fi
90+
91+
echo "🔎 Checking /mcp (GET)"
92+
MCP_GET_STATUS=$(check_status GET "http://localhost:8081/mcp" "" "" || true)
93+
if [ "$MCP_GET_STATUS" = "200" ] || [ "$MCP_GET_STATUS" = "400" ]; then
94+
echo "✓ /mcp endpoint reachable via GET ($MCP_GET_STATUS)"
95+
else
96+
echo "! /mcp endpoint GET not reachable (status: $MCP_GET_STATUS)"
97+
exit 1
98+
fi
99+
100+
echo "🔎 Checking /mcp (POST)"
101+
MCP_POST_STATUS=$(check_status POST "http://localhost:8081/mcp" '{}' "" || true)
102+
if [ "$MCP_POST_STATUS" = "200" ] || [ "$MCP_POST_STATUS" = "204" ] || [ "$MCP_POST_STATUS" = "400" ]; then
103+
echo "✓ /mcp endpoint reachable via POST ($MCP_POST_STATUS)"
104+
else
105+
echo "! /mcp endpoint POST not reachable (status: $MCP_POST_STATUS)"
106+
exit 1
107+
fi
108+
109+
docker rm -f gofetch-http-endpoints-test > /dev/null 2>&1 || true
110+
echo "✓ Streamable HTTP container shut down"
111+
112+
echo "✅ Endpoint integration tests passed"
113+
114+

test/integration-test.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ fi
2929
# Get the image name from ko build output
3030
IMAGE_NAME="ghcr.io/stackloklabs/gofetch/server:latest"
3131

32+
cleanup() {
33+
docker rm -f gofetch-sse-test > /dev/null 2>&1 || true
34+
docker rm -f gofetch-http-test > /dev/null 2>&1 || true
35+
}
36+
trap cleanup EXIT
37+
3238
###################################################################
3339
################## START - SSE TRANSPORT TESTING ##################
3440
###################################################################

0 commit comments

Comments
 (0)