golib/mcp/server20250618/server_test.go

138 lines
4.7 KiB
Go

package mcpserver
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
mcptypes "github.com/therootcompany/golib/mcp/types20250618"
)
func TestServeHTTPRejectsBatch(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader("[]"))
req.Header.Set("Accept", "application/json, text/event-stream")
res := httptest.NewRecorder()
(&MCPServer{}).ServeHTTP(res, req)
if res.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d", res.Code, http.StatusBadRequest)
}
}
func TestServeHTTPRequiresAcceptTypes(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"tools/list"}`))
res := httptest.NewRecorder()
(&MCPServer{}).ServeHTTP(res, req)
if res.Code != http.StatusNotAcceptable {
t.Fatalf("status = %d, want %d", res.Code, http.StatusNotAcceptable)
}
}
func TestServeHTTPRejectsUntrustedOrigin(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"tools/list"}`))
req.Header.Set("Accept", "application/json, text/event-stream")
req.Header.Set("Origin", "https://evil.example")
res := httptest.NewRecorder()
(&MCPServer{}).ServeHTTP(res, req)
if res.Code != http.StatusForbidden {
t.Fatalf("status = %d, want %d", res.Code, http.StatusForbidden)
}
}
func TestServeHTTPRejectsUnsupportedProtocolVersion(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"tools/list"}`))
req.Header.Set("Accept", "application/json, text/event-stream")
req.Header.Set("MCP-Protocol-Version", "2099-01-01")
res := httptest.NewRecorder()
(&MCPServer{}).ServeHTTP(res, req)
if res.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d", res.Code, http.StatusBadRequest)
}
}
func TestHandleRequestRejectsNullID(t *testing.T) {
result := (&MCPServer{}).HandleRequest(nil, []byte(`{"jsonrpc":"2.0","id":null,"method":"tools/list"}`))
errResponse, ok := result.(mcptypes.JSONRPCError)
if !ok {
t.Fatalf("result type = %T, want mcptypes.JSONRPCError", result)
}
if errResponse.Error.Code != mcptypes.ErrCodeInvalidRequest {
t.Fatalf("error code = %d, want %d", errResponse.Error.Code, mcptypes.ErrCodeInvalidRequest)
}
}
func TestHandleRequestAcceptsJSONRPCResponse(t *testing.T) {
result := (&MCPServer{}).HandleRequest(nil, []byte(`{"jsonrpc":"2.0","id":1,"result":{}}`))
if result != nil {
t.Fatalf("result = %#v, want nil for accepted response", result)
}
}
func TestToolsCallLeavesArgumentValidationToHandler(t *testing.T) {
called := false
srv := &MCPServer{}
srv.HandleToolFunc("echo", mcptypes.Tool{
InputSchema: mcptypes.ToolInputSchema{
Type: "object",
Required: []string{"message"},
Properties: map[string]mcptypes.Property{
"message": {Type: "string"},
},
},
}, func(context.Context, mcptypes.CallToolRequest) (*mcptypes.CallToolResult, error) {
called = true
return mcptypes.NewToolResultText("ok"), nil
})
result := srv.HandleRequest(nil, []byte(`{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{}}}`))
if _, ok := result.(mcptypes.JSONRPCResponse); !ok {
t.Fatalf("result = %#v, want successful tool response", result)
}
if !called {
t.Fatal("tool was not called; argument validation belongs to the handler")
}
}
func TestResourceReadRejectsInvalidBlob(t *testing.T) {
srv := &MCPServer{}
srv.HandleResource("test://item", mcptypes.Resource{Name: "item"}, func(context.Context, mcptypes.ReadResourceRequest) ([]mcptypes.ResourceContents, error) {
return []mcptypes.ResourceContents{{URI: "test://item", Blob: "not base64"}}, nil
})
result := srv.HandleRequest(nil, []byte(`{"jsonrpc":"2.0","id":1,"method":"resources/read","params":{"uri":"test://item"}}`))
data, err := json.Marshal(result)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(data), "invalid binary data") {
t.Fatalf("result = %s, want invalid binary error", data)
}
}
func TestInitializeNegotiatesCurrentVersion(t *testing.T) {
srv := &MCPServer{Name: "test", Version: "1"}
result := srv.HandleRequest(nil, []byte(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"client","version":"1"}}}`))
response, ok := result.(mcptypes.JSONRPCResponse)
if !ok {
t.Fatalf("result type = %T, want mcptypes.JSONRPCResponse", result)
}
initialize, ok := response.Result.(mcptypes.InitializeResult)
if !ok {
t.Fatalf("result payload type = %T, want mcptypes.InitializeResult", response.Result)
}
if initialize.ProtocolVersion != ProtocolVersion {
t.Fatalf("protocol version = %q, want %q", initialize.ProtocolVersion, ProtocolVersion)
}
}