Files
ombi-mcp/internal/transport/sse_test.go
gronod 49bc0775de
Build and publish / Test and build (windows) (push) Failing after 11s
Build and publish / Test and build (darwin) (push) Successful in 1m32s
Build and publish / Test and build (linux) (push) Canceled after 0s
Build and publish / Build and publish Docker image (push) Canceled after 0s
Add HTTP/SSE transport alongside stdio (Phase 09)
Network MCP clients (e.g. browser-based frontends) can't spawn stdio
subprocesses, so the server now optionally serves the 2024-11-05 MCP
HTTP/SSE transport via MCP_TRANSPORT=sse and MCP_PORT (default 8080),
backed by the SDK's SSEHandler behind permissive CORS. stdio remains
the default and is behaviourally unchanged.
2026-09-18 21:13:09 +01:00

106 lines
2.7 KiB
Go

package transport
import (
"bufio"
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func newTestServer() *mcp.Server {
return mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.0"}, nil)
}
func TestSSEEstablishesConnection(t *testing.T) {
ts := httptest.NewServer(NewSSEHandler(newTestServer()))
defer ts.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL+"/sse", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Accept", "text/event-stream")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("GET /sse: status %d", resp.StatusCode)
}
if ct := resp.Header.Get("Content-Type"); ct != "text/event-stream" {
t.Fatalf("GET /sse: Content-Type %q", ct)
}
if acao := resp.Header.Get("Access-Control-Allow-Origin"); acao != "*" {
t.Fatalf("GET /sse: Access-Control-Allow-Origin %q", acao)
}
// The first SSE event must be 'endpoint' carrying the session POST URL.
sc := bufio.NewScanner(resp.Body)
var event, data string
for sc.Scan() {
line := sc.Text()
if strings.HasPrefix(line, "event:") {
event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
} else if strings.HasPrefix(line, "data:") {
data = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
break
}
}
if event != "endpoint" {
t.Fatalf("first event = %q, want %q", event, "endpoint")
}
if !strings.Contains(data, "sessionid=") {
t.Fatalf("endpoint data %q lacks sessionid", data)
}
}
func TestSSECORSPreflight(t *testing.T) {
ts := httptest.NewServer(NewSSEHandler(newTestServer()))
defer ts.Close()
req, err := http.NewRequest(http.MethodOptions, ts.URL+"/sse", nil)
if err != nil {
t.Fatal(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
if resp.StatusCode != http.StatusNoContent {
t.Fatalf("OPTIONS /sse: status %d", resp.StatusCode)
}
if resp.Header.Get("Access-Control-Allow-Origin") != "*" {
t.Fatal("OPTIONS /sse: missing CORS headers")
}
}
func TestSSEPostMessagesRequiresSession(t *testing.T) {
ts := httptest.NewServer(NewSSEHandler(newTestServer()))
defer ts.Close()
resp, err := http.Post(ts.URL+"/messages", "application/json",
strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"initialize"}`))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("POST /messages without sessionid: status %d", resp.StatusCode)
}
}