package x402 import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" x402 "github.com/x402-foundation/x402/go/v2" x402http "github.com/x402-foundation/x402/go/v2/http" ) type DummyAuthProvider struct{} func (d *DummyAuthProvider) GetAuthHeaders(ctx context.Context) (x402http.AuthHeaders, error) { return x402http.AuthHeaders{}, nil } func TestX402Server_HTTPBoundaries(t *testing.T) { mockFacilitator := setupMockFacilitator() defer mockFacilitator.Close() srv, err := NewX402Server( "Test Agent API", "1.0.0", mockFacilitator.URL, &DummyAuthProvider{}, X402Middlewares{}, func(next http.Handler) http.Handler { return next }, func(w http.ResponseWriter, r *http.Request, resp *x402.SettleResponse) {}, ) if err != nil { t.Fatalf("failed to create server: %v", err) } srv.AddPayableTool( mcp.Tool{ Name: "getTestPost", Description: "Gated test endpoint", }, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { return &mcp.CallToolResult{ Content: []mcp.Content{&mcp.TextContent{Text: `{"status": "success"}`}}, }, nil }, x402.PaymentRequirements{ Scheme: "exact", Network: "eip155:8453", Asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", Amount: "1000", PayTo: "0x0000000000000000000000000000000000000000", }, ) reqRec := httptest.NewRecorder() reqHealth, _ := http.NewRequest("GET", "/healthz", nil) srv.mux.ServeHTTP(reqRec, reqHealth) if reqRec.Code != http.StatusOK { t.Errorf("expected healthz status 200, got %d", reqRec.Code) } reqRecOpenAPI := httptest.NewRecorder() reqOpenAPI, _ := http.NewRequest("GET", "/openapi.json", nil) srv.mux.ServeHTTP(reqRecOpenAPI, reqOpenAPI) if reqRecOpenAPI.Code != http.StatusOK { t.Errorf("expected OpenAPI status 200, got %d", reqRecOpenAPI.Code) } if !strings.Contains(reqRecOpenAPI.Body.String(), "getTestPost") { t.Error("OpenAPI spec did not render registered tool definitions") } reqRecPayable := httptest.NewRecorder() reqPayable, _ := http.NewRequest("POST", "/v1/getTestPost", strings.NewReader(`{}`)) srv.mux.ServeHTTP(reqRecPayable, reqPayable) if reqRecPayable.Code != http.StatusPaymentRequired { t.Errorf("expected status 402 (Payment Required), got %d", reqRecPayable.Code) } payHeader := reqRecPayable.Header().Get("PAYMENT-REQUIRED") if payHeader == "" { t.Error("missing PAYMENT-REQUIRED response header on 402 challenge") } } func TestX402Server_MCPBoundaries(t *testing.T) { ctx := context.Background() serverTransport, clientTransport := mcp.NewInMemoryTransports() mockFacilitator := setupMockFacilitator() defer mockFacilitator.Close() srv, err := NewX402Server( "Test Agent API", "1.0.0", mockFacilitator.URL, &DummyAuthProvider{}, X402Middlewares{}, func(next http.Handler) http.Handler { return next }, func(w http.ResponseWriter, r *http.Request, resp *x402.SettleResponse) {}, ) if err != nil { t.Fatalf("failed to create server: %v", err) } srv.AddPayableTool( mcp.Tool{ Name: "getTestPost", Description: "Gated test endpoint", }, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { return &mcp.CallToolResult{ Content: []mcp.Content{&mcp.TextContent{Text: `{"status": "success"}`}}, }, nil }, x402.PaymentRequirements{ Scheme: "exact", Network: "eip155:8453", Asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", Amount: "1000", PayTo: "0x0000000000000000000000000000000000000000", }, ) serverSession, err := srv.mcpServer.Connect(ctx, serverTransport, nil) if err != nil { t.Fatalf("failed to connect server session: %v", err) } defer serverSession.Close() client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "1.0.0"}, nil) clientSession, err := client.Connect(ctx, clientTransport, nil) if err != nil { t.Fatalf("failed to connect client session: %v", err) } defer clientSession.Close() toolsResponse, err := clientSession.ListTools(ctx, nil) if err != nil { t.Fatalf("failed to list tools over MCP: %v", err) } found := false for _, tool := range toolsResponse.Tools { if tool.Name == "getTestPost" { found = true break } } if !found { t.Error("expected tool 'getTestPost' to be discovered by the MCP client, but it was missing") } result, err := clientSession.CallTool(ctx, &mcp.CallToolParams{ Name: "getTestPost", Arguments: json.RawMessage(`{}`), }) if err != nil { t.Errorf("Unexpected protocol-level error: %v", err) } else { if !result.IsError { t.Error("expected CallTool to return a payment challenge, but it returned a successful, free result") } else { t.Logf("Intercepted expected application-level payment challenge.") t.Logf("Content: %v", result.Content) t.Logf("Meta fields: %v", result.Meta) } } } func setupMockFacilitator() *httptest.Server { s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == "GET" && r.URL.Path == "/supported": _, _ = w.Write([]byte(`{ "kinds": [ { "x402Version": 2, "scheme": "exact", "network": "eip155:8453" } ] }`)) case r.Method == "POST" && r.URL.Path == "/verify": _, _ = w.Write([]byte(`{ "isValid": true, "invalidReason": "" }`)) case r.Method == "POST" && r.URL.Path == "/settle": _, _ = w.Write([]byte(`{ "success": true, "transaction": "0x1234abcd", "network": "eip155:8453", "payer": "0xabcd1234" }`)) default: w.WriteHeader(http.StatusNotFound) } })) return s }