580238b32e
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
202 lines
5.4 KiB
Go
202 lines
5.4 KiB
Go
package infra
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// --- helpers ----------------------------------------------------------------
|
|
|
|
func newTestMCPHandler(auth MCPHTTPAuthFunc) http.Handler {
|
|
tools := []MCPToolDef{
|
|
{
|
|
Name: "greet",
|
|
Description: "Returns a greeting",
|
|
InputSchema: json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"}}}`),
|
|
},
|
|
}
|
|
handler := func(_ context.Context, name string, _ json.RawMessage) (any, bool, error) {
|
|
if name == "greet" {
|
|
return map[string]string{"hello": "world"}, false, nil
|
|
}
|
|
return nil, true, errors.New("unknown tool")
|
|
}
|
|
return MCPHTTPHandler(MCPHTTPOpts{
|
|
Name: "test-server",
|
|
Version: "0.0.1",
|
|
Tools: tools,
|
|
Handler: handler,
|
|
Auth: auth,
|
|
})
|
|
}
|
|
|
|
func postMCP(h http.Handler, body string) *httptest.ResponseRecorder {
|
|
r := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(body))
|
|
r.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
return w
|
|
}
|
|
|
|
// --- tests ------------------------------------------------------------------
|
|
|
|
func TestMCPHTTPHandler_Initialize(t *testing.T) {
|
|
h := newTestMCPHandler(nil)
|
|
w := postMCP(h, `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var resp jsonrpcResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode error: %v", err)
|
|
}
|
|
if resp.Error != nil {
|
|
t.Fatalf("unexpected error: %+v", resp.Error)
|
|
}
|
|
|
|
result, ok := resp.Result.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("result is not map: %T", resp.Result)
|
|
}
|
|
if _, ok := result["protocolVersion"]; !ok {
|
|
t.Error("missing protocolVersion in result")
|
|
}
|
|
si, ok := result["serverInfo"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("missing serverInfo")
|
|
}
|
|
if si["name"] != "test-server" {
|
|
t.Errorf("serverInfo.name = %v, want test-server", si["name"])
|
|
}
|
|
}
|
|
|
|
func TestMCPHTTPHandler_ToolsList(t *testing.T) {
|
|
h := newTestMCPHandler(nil)
|
|
w := postMCP(h, `{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}`)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var resp jsonrpcResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Error != nil {
|
|
t.Fatalf("unexpected rpc error: %+v", resp.Error)
|
|
}
|
|
|
|
result := resp.Result.(map[string]any)
|
|
tools, ok := result["tools"].([]any)
|
|
if !ok || len(tools) == 0 {
|
|
t.Fatalf("expected non-empty tools array, got %v", result["tools"])
|
|
}
|
|
}
|
|
|
|
func TestMCPHTTPHandler_ToolsCall(t *testing.T) {
|
|
h := newTestMCPHandler(nil)
|
|
w := postMCP(h, `{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"greet","arguments":{}}}`)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var resp jsonrpcResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Error != nil {
|
|
t.Fatalf("unexpected rpc error: %+v", resp.Error)
|
|
}
|
|
|
|
result := resp.Result.(map[string]any)
|
|
content, ok := result["content"].([]any)
|
|
if !ok || len(content) == 0 {
|
|
t.Fatalf("expected content array, got %v", result["content"])
|
|
}
|
|
first := content[0].(map[string]any)
|
|
if first["text"] != `{"hello":"world"}` {
|
|
t.Errorf("content[0].text = %q, want {\"hello\":\"world\"}", first["text"])
|
|
}
|
|
}
|
|
|
|
func TestMCPHTTPHandler_BadAuth(t *testing.T) {
|
|
auth := func(_ *http.Request) (context.Context, error) {
|
|
return nil, errors.New("bad token")
|
|
}
|
|
h := newTestMCPHandler(auth)
|
|
w := postMCP(h, `{"jsonrpc":"2.0","id":4,"method":"initialize","params":{}}`)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestMCPHTTPHandler_BodyTooLarge(t *testing.T) {
|
|
h := newTestMCPHandler(nil)
|
|
big := strings.Repeat("x", mcpHTTPBodyLimit+1)
|
|
body := `{"jsonrpc":"2.0","id":5,"method":"initialize","params":{"x":"` + big + `"}}`
|
|
r := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
|
|
if w.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("expected 413, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestMCPHTTPHandler_ParseError(t *testing.T) {
|
|
h := newTestMCPHandler(nil)
|
|
w := postMCP(h, `not valid json`)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected HTTP 200 for parse error, got %d", w.Code)
|
|
}
|
|
|
|
var resp jsonrpcResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Error == nil {
|
|
t.Fatal("expected JSON-RPC error, got nil")
|
|
}
|
|
if resp.Error.Code != -32700 {
|
|
t.Errorf("error code = %d, want -32700", resp.Error.Code)
|
|
}
|
|
}
|
|
|
|
func TestMCPHTTPHandler_Notification(t *testing.T) {
|
|
h := newTestMCPHandler(nil)
|
|
// A notification has no "id" key at all.
|
|
w := postMCP(h, `{"jsonrpc":"2.0","method":"initialized","params":{}}`)
|
|
|
|
if w.Code != http.StatusAccepted {
|
|
t.Fatalf("expected 202 for notification, got %d", w.Code)
|
|
}
|
|
if w.Body.Len() != 0 {
|
|
t.Errorf("expected empty body for notification, got %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestMCPHTTPHandler_MethodNotAllowed(t *testing.T) {
|
|
h := newTestMCPHandler(nil)
|
|
|
|
for _, method := range []string{http.MethodGet, http.MethodDelete} {
|
|
t.Run(method, func(t *testing.T) {
|
|
r := httptest.NewRequest(method, "/mcp", nil)
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("%s: expected 405, got %d", method, w.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|