| 1 | package plugin |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "fmt" |
| 6 | "net/http" |
| 7 | "net/http/httptest" |
| 8 | "strings" |
| 9 | "sync/atomic" |
| 10 | "testing" |
| 11 | |
| 12 | mcpjsonrpc "github.com/modelcontextprotocol/go-sdk/jsonrpc" |
| 13 | ) |
| 14 | |
| 15 | func TestApplicationJSONRPCSessionErrorsAreNotTransportLoss(t *testing.T) { |
| 16 | for _, message := range []string{ |
| 17 | "session not found", |
| 18 | "session missing", |
| 19 | "session expired", |
| 20 | "invalid session", |
| 21 | "unknown session", |
| 22 | } { |
| 23 | err := fmt.Errorf("calling tool: %w", &mcpjsonrpc.Error{Code: -32042, Message: message}) |
| 24 | if isExplicitMCPSessionMissing(err) { |
| 25 | t.Errorf("isExplicitMCPSessionMissing(%q) = true, want false without transport rejection", message) |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | err := fmt.Errorf("calling tool: %w", &mcpjsonrpc.Error{Code: -32042, Message: "not found"}) |
| 30 | if isMCPHTTPNotFound(err) { |
| 31 | t.Fatal("application JSON-RPC not-found error was classified as HTTP 404") |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | func TestStructuredHTTP404SessionErrorIsTransportLoss(t *testing.T) { |
| 36 | sessionErr := &mcpjsonrpc.Error{Code: -32042, Message: "Session not found"} |
| 37 | rejectedErr := &mcpjsonrpc.Error{Code: -32005, Message: "rejected by transport"} |
| 38 | err := fmt.Errorf("sending tools/call: %w: %w: Not Found", sessionErr, rejectedErr) |
| 39 | if !isExplicitMCPSessionMissing(err) { |
| 40 | t.Fatal("structured HTTP 404 session error was not classified as transport session loss") |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | func TestHTTPTransportApplicationSessionErrorDoesNotReplayToolCall(t *testing.T) { |
| 45 | var initializeCount atomic.Int32 |
| 46 | var toolCallCount atomic.Int32 |
| 47 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 48 | if r.Method == http.MethodGet { |
| 49 | w.WriteHeader(http.StatusMethodNotAllowed) |
| 50 | return |
| 51 | } |
| 52 | var req struct { |
| 53 | ID json.RawMessage `json:"id"` |
| 54 | Method string `json:"method"` |
| 55 | } |
| 56 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| 57 | http.Error(w, "bad body", http.StatusBadRequest) |
| 58 | return |
| 59 | } |
| 60 | |
| 61 | switch req.Method { |
| 62 | case "server/discover": |
| 63 | writeRawHTTPRPCError(w, req.ID, -32601, "method not found") |
| 64 | case "initialize": |
| 65 | initializeCount.Add(1) |
| 66 | w.Header().Set("Mcp-Session-Id", "application-session") |
| 67 | writeRawHTTPRPCResult(w, req.ID, map[string]any{ |
| 68 | "protocolVersion": testLegacyProtocolVersion, |
| 69 | "serverInfo": map[string]any{"name": "application-error", "version": "1"}, |
| 70 | "capabilities": map[string]any{"tools": map[string]any{}}, |
| 71 | }) |
| 72 | case "notifications/initialized": |
| 73 | w.WriteHeader(http.StatusAccepted) |
| 74 | case "tools/list": |
| 75 | writeRawHTTPRPCResult(w, req.ID, map[string]any{"tools": []any{}}) |
| 76 | case "tools/call": |
| 77 | toolCallCount.Add(1) |
| 78 | writeRawHTTPRPCError(w, req.ID, -32042, "invalid session") |
| 79 | default: |
| 80 | http.Error(w, "unknown method", http.StatusBadRequest) |
| 81 | } |
| 82 | })) |
| 83 | defer srv.Close() |
| 84 | |
| 85 | transport, err := newHTTPTransport(Spec{Name: "application-error", Type: "http", URL: srv.URL}) |
| 86 | if err != nil { |
| 87 | t.Fatal(err) |
| 88 | } |
| 89 | defer transport.close() |
| 90 | if _, err := transport.call(t.Context(), "tools/list", map[string]any{}); err != nil { |
| 91 | t.Fatalf("tools/list: %v", err) |
| 92 | } |
| 93 | if _, err := transport.call(t.Context(), "tools/call", map[string]any{ |
| 94 | "name": "write", "arguments": map[string]any{}, |
| 95 | }); err == nil || !strings.Contains(err.Error(), "invalid session") { |
| 96 | t.Fatalf("tools/call error = %v, want application error", err) |
| 97 | } |
| 98 | if got := initializeCount.Load(); got != 1 { |
| 99 | t.Fatalf("initialize count = %d, want no session rebuild", got) |
| 100 | } |
| 101 | if got := toolCallCount.Load(); got != 1 { |
| 102 | t.Fatalf("tools/call count = %d, want no replay", got) |
| 103 | } |
| 104 | } |
| 105 |