This is page 3 of 4. Use http://codebase.md/razorpay/razorpay-mcp-server?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .cursor │ └── rules │ └── new-tool-from-docs.mdc ├── .cursorignore ├── .dockerignore ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── pull_request_template.md │ └── workflows │ ├── assign.yml │ ├── build.yml │ ├── ci.yml │ ├── docker-publish.yml │ ├── lint.yml │ └── release.yml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yaml ├── cmd │ └── razorpay-mcp-server │ ├── main.go │ └── stdio.go ├── codecov.yml ├── CONTRIBUTING.md ├── Dockerfile ├── go.mod ├── go.sum ├── LICENSE ├── Makefile ├── pkg │ ├── contextkey │ │ └── context_key.go │ ├── log │ │ ├── config.go │ │ ├── log.go │ │ ├── slog_test.go │ │ └── slog.go │ ├── mcpgo │ │ ├── README.md │ │ ├── server.go │ │ ├── stdio.go │ │ ├── tool.go │ │ └── transport.go │ ├── observability │ │ └── observability.go │ ├── razorpay │ │ ├── mock │ │ │ ├── server_test.go │ │ │ └── server.go │ │ ├── orders_test.go │ │ ├── orders.go │ │ ├── payment_links_test.go │ │ ├── payment_links.go │ │ ├── payments_test.go │ │ ├── payments.go │ │ ├── payouts_test.go │ │ ├── payouts.go │ │ ├── qr_codes_test.go │ │ ├── qr_codes.go │ │ ├── README.md │ │ ├── refunds_test.go │ │ ├── refunds.go │ │ ├── server.go │ │ ├── settlements_test.go │ │ ├── settlements.go │ │ ├── test_helpers.go │ │ ├── tokens_test.go │ │ ├── tokens.go │ │ ├── tools_params_test.go │ │ ├── tools_params.go │ │ ├── tools_test.go │ │ └── tools.go │ └── toolsets │ └── toolsets.go ├── README.md └── SECURITY.md ``` # Files -------------------------------------------------------------------------------- /pkg/razorpay/payments.go: -------------------------------------------------------------------------------- ```go package razorpay import ( "context" "fmt" "net/http" "net/url" "strings" "time" rzpsdk "github.com/razorpay/razorpay-go" "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" "github.com/razorpay/razorpay-mcp-server/pkg/observability" ) // FetchPayment returns a tool that fetches payment details using payment_id func FetchPayment( obs *observability.Observability, client *rzpsdk.Client, ) mcpgo.Tool { parameters := []mcpgo.ToolParameter{ mcpgo.WithString( "payment_id", mcpgo.Description("payment_id is unique identifier "+ "of the payment to be retrieved."), mcpgo.Required(), ), } handler := func( ctx context.Context, r mcpgo.CallToolRequest, ) (*mcpgo.ToolResult, error) { // Get client from context or use default client, err := getClientFromContextOrDefault(ctx, client) if err != nil { return mcpgo.NewToolResultError(err.Error()), nil } params := make(map[string]interface{}) validator := NewValidator(&r). ValidateAndAddRequiredString(params, "payment_id") if result, err := validator.HandleErrorsIfAny(); result != nil { return result, err } paymentId := params["payment_id"].(string) payment, err := client.Payment.Fetch(paymentId, nil, nil) if err != nil { return mcpgo.NewToolResultError( fmt.Sprintf("fetching payment failed: %s", err.Error())), nil } return mcpgo.NewToolResultJSON(payment) } return mcpgo.NewTool( "fetch_payment", "Use this tool to retrieve the details of a specific payment "+ "using its id. Amount returned is in paisa", parameters, handler, ) } // FetchPaymentCardDetails returns a tool that fetches card details // for a payment func FetchPaymentCardDetails( obs *observability.Observability, client *rzpsdk.Client, ) mcpgo.Tool { parameters := []mcpgo.ToolParameter{ mcpgo.WithString( "payment_id", mcpgo.Description("Unique identifier of the payment for which "+ "you want to retrieve card details. Must start with 'pay_'"), mcpgo.Required(), ), } handler := func( ctx context.Context, r mcpgo.CallToolRequest, ) (*mcpgo.ToolResult, error) { // Get client from context or use default client, err := getClientFromContextOrDefault(ctx, client) if err != nil { return mcpgo.NewToolResultError(err.Error()), nil } params := make(map[string]interface{}) validator := NewValidator(&r). ValidateAndAddRequiredString(params, "payment_id") if result, err := validator.HandleErrorsIfAny(); result != nil { return result, err } paymentId := params["payment_id"].(string) cardDetails, err := client.Payment.FetchCardDetails( paymentId, nil, nil) if err != nil { return mcpgo.NewToolResultError( fmt.Sprintf("fetching card details failed: %s", err.Error())), nil } return mcpgo.NewToolResultJSON(cardDetails) } return mcpgo.NewTool( "fetch_payment_card_details", "Use this tool to retrieve the details of the card used to make a payment. "+ "Only works for payments made using a card.", parameters, handler, ) } // UpdatePayment returns a tool that updates the notes for a payment func UpdatePayment( obs *observability.Observability, client *rzpsdk.Client, ) mcpgo.Tool { parameters := []mcpgo.ToolParameter{ mcpgo.WithString( "payment_id", mcpgo.Description("Unique identifier of the payment to be updated. "+ "Must start with 'pay_'"), mcpgo.Required(), ), mcpgo.WithObject( "notes", mcpgo.Description("Key-value pairs that can be used to store additional "+ "information about the payment. Values must be strings or integers."), mcpgo.Required(), ), } handler := func( ctx context.Context, r mcpgo.CallToolRequest, ) (*mcpgo.ToolResult, error) { // Get client from context or use default client, err := getClientFromContextOrDefault(ctx, client) if err != nil { return mcpgo.NewToolResultError(err.Error()), nil } params := make(map[string]interface{}) paymentUpdateReq := make(map[string]interface{}) validator := NewValidator(&r). ValidateAndAddRequiredString(params, "payment_id"). ValidateAndAddRequiredMap(paymentUpdateReq, "notes") if result, err := validator.HandleErrorsIfAny(); result != nil { return result, err } paymentId := params["payment_id"].(string) // Update the payment updatedPayment, err := client.Payment.Edit(paymentId, paymentUpdateReq, nil) if err != nil { return mcpgo.NewToolResultError( fmt.Sprintf("updating payment failed: %s", err.Error())), nil } return mcpgo.NewToolResultJSON(updatedPayment) } return mcpgo.NewTool( "update_payment", "Use this tool to update the notes field of a payment. Notes are "+ "key-value pairs that can be used to store additional information.", //nolint:lll parameters, handler, ) } // CapturePayment returns a tool that captures an authorized payment func CapturePayment( obs *observability.Observability, client *rzpsdk.Client, ) mcpgo.Tool { parameters := []mcpgo.ToolParameter{ mcpgo.WithString( "payment_id", mcpgo.Description("Unique identifier of the payment to be captured. Should start with 'pay_'"), //nolint:lll mcpgo.Required(), ), mcpgo.WithNumber( "amount", mcpgo.Description("The amount to be captured (in paisa). "+ "Should be equal to the authorized amount"), mcpgo.Required(), ), mcpgo.WithString( "currency", mcpgo.Description("ISO code of the currency in which the payment "+ "was made (e.g., INR)"), mcpgo.Required(), ), } handler := func( ctx context.Context, r mcpgo.CallToolRequest, ) (*mcpgo.ToolResult, error) { // Get client from context or use default client, err := getClientFromContextOrDefault(ctx, client) if err != nil { return mcpgo.NewToolResultError(err.Error()), nil } params := make(map[string]interface{}) paymentCaptureReq := make(map[string]interface{}) validator := NewValidator(&r). ValidateAndAddRequiredString(params, "payment_id"). ValidateAndAddRequiredInt(params, "amount"). ValidateAndAddRequiredString(paymentCaptureReq, "currency") if result, err := validator.HandleErrorsIfAny(); result != nil { return result, err } paymentId := params["payment_id"].(string) amount := int(params["amount"].(int64)) // Capture the payment payment, err := client.Payment.Capture( paymentId, amount, paymentCaptureReq, nil, ) if err != nil { return mcpgo.NewToolResultError( fmt.Sprintf("capturing payment failed: %s", err.Error())), nil } return mcpgo.NewToolResultJSON(payment) } return mcpgo.NewTool( "capture_payment", "Use this tool to capture a previously authorized payment. Only payments with 'authorized' status can be captured", //nolint:lll parameters, handler, ) } // FetchAllPayments returns a tool to fetch multiple payments with filtering and pagination // //nolint:lll func FetchAllPayments( obs *observability.Observability, client *rzpsdk.Client, ) mcpgo.Tool { parameters := []mcpgo.ToolParameter{ // Pagination parameters mcpgo.WithNumber( "count", mcpgo.Description("Number of payments to fetch "+ "(default: 10, max: 100)"), mcpgo.Min(1), mcpgo.Max(100), ), mcpgo.WithNumber( "skip", mcpgo.Description("Number of payments to skip (default: 0)"), mcpgo.Min(0), ), // Time range filters mcpgo.WithNumber( "from", mcpgo.Description("Unix timestamp (in seconds) from when "+ "payments are to be fetched"), mcpgo.Min(0), ), mcpgo.WithNumber( "to", mcpgo.Description("Unix timestamp (in seconds) up till when "+ "payments are to be fetched"), mcpgo.Min(0), ), } handler := func( ctx context.Context, r mcpgo.CallToolRequest, ) (*mcpgo.ToolResult, error) { // Get client from context or use default client, err := getClientFromContextOrDefault(ctx, client) if err != nil { return mcpgo.NewToolResultError(err.Error()), nil } // Create query parameters map paymentListOptions := make(map[string]interface{}) validator := NewValidator(&r). ValidateAndAddPagination(paymentListOptions). ValidateAndAddOptionalInt(paymentListOptions, "from"). ValidateAndAddOptionalInt(paymentListOptions, "to") if result, err := validator.HandleErrorsIfAny(); result != nil { return result, err } // Fetch all payments using Razorpay SDK payments, err := client.Payment.All(paymentListOptions, nil) if err != nil { return mcpgo.NewToolResultError( fmt.Sprintf("fetching payments failed: %s", err.Error())), nil } return mcpgo.NewToolResultJSON(payments) } return mcpgo.NewTool( "fetch_all_payments", "Fetch all payments with optional filtering and pagination", parameters, handler, ) } // extractPaymentID extracts the payment ID from the payment response func extractPaymentID(payment map[string]interface{}) string { if id, exists := payment["razorpay_payment_id"]; exists && id != nil { return id.(string) } return "" } // extractNextActions extracts all available actions from the payment response func extractNextActions( payment map[string]interface{}, ) []map[string]interface{} { var actions []map[string]interface{} if nextArray, exists := payment["next"]; exists && nextArray != nil { if nextSlice, ok := nextArray.([]interface{}); ok { for _, item := range nextSlice { if nextItem, ok := item.(map[string]interface{}); ok { actions = append(actions, nextItem) } } } } return actions } // OTPResponse represents the response from OTP generation API // sendOtp sends an OTP to the customer and returns the response func sendOtp(otpUrl string) error { if otpUrl == "" { return fmt.Errorf("OTP URL is empty") } // Validate URL is safe and from Razorpay domain for security parsedURL, err := url.Parse(otpUrl) if err != nil { return fmt.Errorf("invalid OTP URL: %s", err.Error()) } if parsedURL.Scheme != "https" { return fmt.Errorf("OTP URL must use HTTPS") } if !strings.Contains(parsedURL.Host, "razorpay.com") { return fmt.Errorf("OTP URL must be from Razorpay domain") } // Create a secure HTTP client with timeout client := &http.Client{ Timeout: 10 * time.Second, } req, err := http.NewRequest("POST", otpUrl, nil) if err != nil { return fmt.Errorf("failed to create OTP request: %s", err.Error()) } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return fmt.Errorf("OTP generation failed: %s", err.Error()) } defer resp.Body.Close() // Validate HTTP response status if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("OTP generation failed with HTTP status: %d", resp.StatusCode) } return nil } // buildInitiatePaymentResponse constructs the response for initiate payment func buildInitiatePaymentResponse( payment map[string]interface{}, paymentID string, actions []map[string]interface{}, ) (map[string]interface{}, string) { response := map[string]interface{}{ "razorpay_payment_id": paymentID, "payment_details": payment, "status": "payment_initiated", "message": "Payment initiated successfully using " + "S2S JSON v1 flow", } otpUrl := "" if len(actions) > 0 { response["available_actions"] = actions // Add guidance based on available actions var actionTypes []string hasOTP := false hasRedirect := false hasUPICollect := false hasUPIIntent := false for _, action := range actions { if actionType, exists := action["action"]; exists { actionStr := actionType.(string) actionTypes = append(actionTypes, actionStr) if actionStr == "otp_generate" { hasOTP = true otpUrl = action["url"].(string) } if actionStr == "redirect" { hasRedirect = true } if actionStr == "upi_collect" { hasUPICollect = true } if actionStr == "upi_intent" { hasUPIIntent = true } } } switch { case hasOTP: response["message"] = "Payment initiated. OTP authentication is " + "available. " + "Use the 'submit_otp' tool to submit OTP received by the customer " + "for authentication." addNextStepInstructions(response, paymentID) case hasRedirect: response["message"] = "Payment initiated. Redirect authentication is " + "available. Use the redirect URL provided in available_actions." case hasUPICollect: response["message"] = fmt.Sprintf( "Payment initiated. Available actions: %v", actionTypes) case hasUPIIntent: response["message"] = fmt.Sprintf( "Payment initiated. Available actions: %v", actionTypes) default: response["message"] = fmt.Sprintf( "Payment initiated. Available actions: %v", actionTypes) } } else { addFallbackNextStepInstructions(response, paymentID) } return response, otpUrl } // addNextStepInstructions adds next step guidance to the response func addNextStepInstructions( response map[string]interface{}, paymentID string, ) { if paymentID != "" { response["next_step"] = "Use 'resend_otp' to regenerate OTP or " + "'submit_otp' to proceed to enter OTP." response["next_tool"] = "resend_otp" response["next_tool_params"] = map[string]interface{}{ "payment_id": paymentID, } } } // addFallbackNextStepInstructions adds fallback next step guidance func addFallbackNextStepInstructions( response map[string]interface{}, paymentID string, ) { if paymentID != "" { response["next_step"] = "Use 'resend_otp' to regenerate OTP or " + "'submit_otp' to proceed to enter OTP if " + "OTP authentication is required." response["next_tool"] = "resend_otp" response["next_tool_params"] = map[string]interface{}{ "payment_id": paymentID, } } } // addContactAndEmailToPaymentData adds contact and email to payment data func addContactAndEmailToPaymentData( paymentData map[string]interface{}, params map[string]interface{}, ) { // Add contact if provided if contact, exists := params["contact"]; exists && contact != "" { paymentData["contact"] = contact } // Add email if provided, otherwise generate from contact if email, exists := params["email"]; exists && email != "" { paymentData["email"] = email } else if contact, exists := paymentData["contact"]; exists && contact != "" { paymentData["email"] = contact.(string) + "@mcp.razorpay.com" } } // addAdditionalPaymentParameters adds additional parameters for UPI collect // and other flows func addAdditionalPaymentParameters( paymentData map[string]interface{}, params map[string]interface{}, ) { // Note: customer_id is now handled explicitly in buildPaymentData // Add method if provided if method, exists := params["method"]; exists && method != "" { paymentData["method"] = method } // Add save if provided if save, exists := params["save"]; exists { paymentData["save"] = save } // Add recurring if provided if recurring, exists := params["recurring"]; exists { paymentData["recurring"] = recurring } // Add UPI parameters if provided if upiParams, exists := params["upi"]; exists && upiParams != nil { if upiMap, ok := upiParams.(map[string]interface{}); ok { paymentData["upi"] = upiMap } } } // processUPIParameters handles VPA and UPI intent parameter processing func processUPIParameters(params map[string]interface{}) { vpa, hasVPA := params["vpa"] upiIntent, hasUPIIntent := params["upi_intent"] // Handle VPA parameter (UPI collect flow) if hasVPA && vpa != "" { // Set method to UPI params["method"] = "upi" // Set UPI parameters for collect flow params["upi"] = map[string]interface{}{ "flow": "collect", "expiry_time": "6", "vpa": vpa, } } // Handle UPI intent parameter (UPI intent flow) if hasUPIIntent && upiIntent == true { // Set method to UPI params["method"] = "upi" // Set UPI parameters for intent flow params["upi"] = map[string]interface{}{ "flow": "intent", } } } // createOrGetCustomer creates or gets a customer if contact is provided func createOrGetCustomer( client *rzpsdk.Client, params map[string]interface{}, ) (map[string]interface{}, error) { contactValue, exists := params["contact"] if !exists || contactValue == "" { return nil, nil } contact := contactValue.(string) customerData := map[string]interface{}{ "contact": contact, "fail_existing": "0", // Get existing customer if exists } // Create/get customer using Razorpay SDK customer, err := client.Customer.Create(customerData, nil) if err != nil { return nil, fmt.Errorf( "failed to create/fetch customer with contact %s: %v", contact, err, ) } return customer, nil } // buildPaymentData constructs the payment data for the API call func buildPaymentData( params map[string]interface{}, currency string, customerId string, ) *map[string]interface{} { paymentData := map[string]interface{}{ "amount": params["amount"], "currency": currency, "order_id": params["order_id"], } if customerId != "" { paymentData["customer_id"] = customerId } // Add token if provided (required for saved payment methods, // optional for UPI collect) if token, exists := params["token"]; exists && token != "" { paymentData["token"] = token } // Add contact and email parameters addContactAndEmailToPaymentData(paymentData, params) // Add additional parameters for UPI collect and other flows addAdditionalPaymentParameters(paymentData, params) // Add force_terminal_id if provided (for single block multiple debit orders) if terminalID, exists := params["force_terminal_id"]; exists && terminalID != "" { paymentData["force_terminal_id"] = terminalID } return &paymentData } // processPaymentResult processes the payment creation result func processPaymentResult( payment map[string]interface{}, ) (map[string]interface{}, error) { // Extract payment ID and next actions from the response paymentID := extractPaymentID(payment) actions := extractNextActions(payment) // Build structured response using the helper function response, otpUrl := buildInitiatePaymentResponse(payment, paymentID, actions) // Only send OTP if there's an OTP URL if otpUrl != "" { err := sendOtp(otpUrl) if err != nil { return nil, fmt.Errorf("OTP generation failed: %s", err.Error()) } } return response, nil } // InitiatePayment returns a tool that initiates a payment using order_id // and token // This implements the S2S JSON v1 flow for creating payments func InitiatePayment( obs *observability.Observability, client *rzpsdk.Client, ) mcpgo.Tool { parameters := []mcpgo.ToolParameter{ mcpgo.WithNumber( "amount", mcpgo.Description("Payment amount in the smallest currency sub-unit "+ "(e.g., for ₹100, use 10000)"), mcpgo.Required(), mcpgo.Min(100), ), mcpgo.WithString( "currency", mcpgo.Description("Currency code for the payment. Default is 'INR'"), ), mcpgo.WithString( "token", mcpgo.Description("Token ID of the saved payment method. "+ "Must start with 'token_'"), ), mcpgo.WithString( "order_id", mcpgo.Description("Order ID for which the payment is being initiated. "+ "Must start with 'order_'"), mcpgo.Required(), ), mcpgo.WithString( "email", mcpgo.Description("Customer's email address (optional)"), ), mcpgo.WithString( "contact", mcpgo.Description("Customer's phone number"), ), mcpgo.WithString( "customer_id", mcpgo.Description("Customer ID for the payment. "+ "Must start with 'cust_'"), ), mcpgo.WithBoolean( "save", mcpgo.Description("Whether to save the payment method for future use"), ), mcpgo.WithString( "vpa", mcpgo.Description("Virtual Payment Address (VPA) for UPI payment. "+ "When provided, automatically sets method='upi' and UPI parameters "+ "with flow='collect' and expiry_time='6' (e.g., '9876543210@ptsbi')"), ), mcpgo.WithBoolean( "upi_intent", mcpgo.Description("Enable UPI intent flow. "+ "When set to true, automatically sets method='upi' and UPI parameters "+ "with flow='intent'. The API will return a UPI URL in the response."), ), mcpgo.WithBoolean( "recurring", mcpgo.Description("Set this to true for recurring payments like "+ "single block multiple debit."), ), mcpgo.WithString( "force_terminal_id", mcpgo.Description("Terminal ID to be passed in case of single block "+ "multiple debit order."), ), } handler := func( ctx context.Context, r mcpgo.CallToolRequest, ) (*mcpgo.ToolResult, error) { // Get client from context or use default client, err := getClientFromContextOrDefault(ctx, client) if err != nil { return mcpgo.NewToolResultError(err.Error()), nil } params := make(map[string]interface{}) validator := NewValidator(&r). ValidateAndAddRequiredInt(params, "amount"). ValidateAndAddOptionalString(params, "currency"). ValidateAndAddOptionalString(params, "token"). ValidateAndAddRequiredString(params, "order_id"). ValidateAndAddOptionalString(params, "email"). ValidateAndAddOptionalString(params, "contact"). ValidateAndAddOptionalString(params, "customer_id"). ValidateAndAddOptionalBool(params, "save"). ValidateAndAddOptionalString(params, "vpa"). ValidateAndAddOptionalBool(params, "upi_intent"). ValidateAndAddOptionalBool(params, "recurring"). ValidateAndAddOptionalString(params, "force_terminal_id") if result, err := validator.HandleErrorsIfAny(); result != nil { return result, err } // Set default currency currency := "INR" if c, exists := params["currency"]; exists && c != "" { currency = c.(string) } // Process UPI parameters (VPA for collect flow, upi_intent for intent flow) processUPIParameters(params) // Handle customer ID var customerID string if custID, exists := params["customer_id"]; exists && custID != "" { customerID = custID.(string) } else { // Create or get customer if contact is provided customer, err := createOrGetCustomer(client, params) if err != nil { return mcpgo.NewToolResultError(err.Error()), nil } if customer != nil { if id, ok := customer["id"].(string); ok { customerID = id } } } // Build payment data paymentDataPtr := buildPaymentData(params, currency, customerID) paymentData := *paymentDataPtr // Create payment using Razorpay SDK's CreatePaymentJson method // This follows the S2S JSON v1 flow: // https://api.razorpay.com/v1/payments/create/json payment, err := client.Payment.CreatePaymentJson(paymentData, nil) if err != nil { return mcpgo.NewToolResultError( fmt.Sprintf("initiating payment failed: %s", err.Error())), nil } // Process payment result response, err := processPaymentResult(payment) if err != nil { return mcpgo.NewToolResultError(err.Error()), nil } return mcpgo.NewToolResultJSON(response) } return mcpgo.NewTool( "initiate_payment", "Initiate a payment using the S2S JSON v1 flow. "+ "Required parameters: amount and order_id. "+ "For saved payment methods, provide token. "+ "For UPI collect flow, provide 'vpa' parameter "+ "which automatically sets UPI with flow='collect' and expiry_time='6'. "+ "For UPI intent flow, set 'upi_intent=true' parameter "+ "which automatically sets UPI with flow='intent' and API returns UPI URL. "+ "Supports additional parameters like customer_id, email, "+ "contact, save, and recurring. "+ "Returns payment details including next action steps if required.", parameters, handler, ) } // ResendOtp returns a tool that sends OTP for payment authentication func ResendOtp( obs *observability.Observability, client *rzpsdk.Client, ) mcpgo.Tool { parameters := []mcpgo.ToolParameter{ mcpgo.WithString( "payment_id", mcpgo.Description("Unique identifier of the payment for which "+ "OTP needs to be generated. Must start with 'pay_'"), mcpgo.Required(), ), } handler := func( ctx context.Context, r mcpgo.CallToolRequest, ) (*mcpgo.ToolResult, error) { // Get client from context or use default client, err := getClientFromContextOrDefault(ctx, client) if err != nil { return mcpgo.NewToolResultError(err.Error()), nil } params := make(map[string]interface{}) validator := NewValidator(&r). ValidateAndAddRequiredString(params, "payment_id") if result, err := validator.HandleErrorsIfAny(); result != nil { return result, err } paymentID := params["payment_id"].(string) // Resend OTP using Razorpay SDK otpResponse, err := client.Payment.OtpResend(paymentID, nil, nil) if err != nil { return mcpgo.NewToolResultError( fmt.Sprintf("OTP resend failed: %s", err.Error())), nil } // Extract OTP submit URL from response otpSubmitURL := extractOtpSubmitURL(otpResponse) // Prepare response response := map[string]interface{}{ "payment_id": paymentID, "status": "success", "message": "OTP sent successfully. Please enter the OTP received on your " + "mobile number to complete the payment.", "response_data": otpResponse, } // Add next step instructions if OTP submit URL is available if otpSubmitURL != "" { response["otp_submit_url"] = otpSubmitURL response["next_step"] = "Use 'submit_otp' tool with the OTP code received " + "from user to complete payment authentication." response["next_tool"] = "submit_otp" response["next_tool_params"] = map[string]interface{}{ "payment_id": paymentID, "otp_string": "{OTP_CODE_FROM_USER}", } } else { response["next_step"] = "Use 'submit_otp' tool with the OTP code received " + "from user to complete payment authentication." response["next_tool"] = "submit_otp" response["next_tool_params"] = map[string]interface{}{ "payment_id": paymentID, "otp_string": "{OTP_CODE_FROM_USER}", } } result, err := mcpgo.NewToolResultJSON(response) if err != nil { return mcpgo.NewToolResultError( fmt.Sprintf("JSON marshal error: %v", err)), nil } return result, nil } return mcpgo.NewTool( "resend_otp", "Resend OTP to the customer's registered mobile number if the previous "+ "OTP was not received or has expired.", parameters, handler, ) } // SubmitOtp returns a tool that submits OTP for payment verification func SubmitOtp( obs *observability.Observability, client *rzpsdk.Client, ) mcpgo.Tool { parameters := []mcpgo.ToolParameter{ mcpgo.WithString( "otp_string", mcpgo.Description("OTP string received from the user"), mcpgo.Required(), ), mcpgo.WithString( "payment_id", mcpgo.Description("Unique identifier of the payment for which "+ "OTP needs to be submitted. Must start with 'pay_'"), mcpgo.Required(), ), } handler := func( ctx context.Context, r mcpgo.CallToolRequest, ) (*mcpgo.ToolResult, error) { // Get client from context or use default client, err := getClientFromContextOrDefault(ctx, client) if err != nil { return mcpgo.NewToolResultError(err.Error()), nil } params := make(map[string]interface{}) validator := NewValidator(&r). ValidateAndAddRequiredString(params, "otp_string"). ValidateAndAddRequiredString(params, "payment_id") if result, err := validator.HandleErrorsIfAny(); result != nil { return result, err } paymentID := params["payment_id"].(string) data := map[string]interface{}{ "otp": params["otp_string"].(string), } otpResponse, err := client.Payment.OtpSubmit(paymentID, data, nil) if err != nil { return mcpgo.NewToolResultError( fmt.Sprintf("OTP verification failed: %s", err.Error())), nil } // Prepare response response := map[string]interface{}{ "payment_id": paymentID, "status": "success", "message": "OTP verified successfully.", "response_data": otpResponse, } result, err := mcpgo.NewToolResultJSON(response) if err != nil { return mcpgo.NewToolResultError( fmt.Sprintf("JSON marshal error: %v", err)), nil } return result, nil } return mcpgo.NewTool( "submit_otp", "Verify and submit the OTP received by the customer to complete "+ "the payment authentication process.", parameters, handler, ) } // extractOtpSubmitURL extracts the OTP submit URL from the payment response func extractOtpSubmitURL(responseData interface{}) string { jsonData, ok := responseData.(map[string]interface{}) if !ok { return "" } nextArray, exists := jsonData["next"] if !exists || nextArray == nil { return "" } nextSlice, ok := nextArray.([]interface{}) if !ok { return "" } for _, item := range nextSlice { nextItem, ok := item.(map[string]interface{}) if !ok { continue } action, exists := nextItem["action"] if !exists || action != "otp_submit" { continue } submitURL, exists := nextItem["url"] if exists && submitURL != nil { if urlStr, ok := submitURL.(string); ok { return urlStr } } } return "" } ``` -------------------------------------------------------------------------------- /pkg/razorpay/tools_params_test.go: -------------------------------------------------------------------------------- ```go package razorpay import ( "testing" "github.com/stretchr/testify/assert" "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" ) func TestValidator(t *testing.T) { tests := []struct { name string args map[string]interface{} paramName string validationFunc func(*Validator, map[string]interface{}, string) *Validator expectError bool expectValue interface{} expectKey string }{ // String tests { name: "required string - valid", args: map[string]interface{}{"test_param": "test_value"}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddRequiredString, expectError: false, expectValue: "test_value", expectKey: "test_param", }, { name: "required string - missing", args: map[string]interface{}{}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddRequiredString, expectError: true, expectValue: nil, expectKey: "test_param", }, { name: "optional string - valid", args: map[string]interface{}{"test_param": "test_value"}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddOptionalString, expectError: false, expectValue: "test_value", expectKey: "test_param", }, { name: "optional string - empty", args: map[string]interface{}{"test_param": ""}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddOptionalString, expectError: false, expectValue: "", expectKey: "test_param", }, // Int tests { name: "required int - valid", args: map[string]interface{}{"test_param": float64(123)}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddRequiredInt, expectError: false, expectValue: int64(123), expectKey: "test_param", }, { name: "optional int - valid", args: map[string]interface{}{"test_param": float64(123)}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddOptionalInt, expectError: false, expectValue: int64(123), expectKey: "test_param", }, { name: "optional int - zero", args: map[string]interface{}{"test_param": float64(0)}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddOptionalInt, expectError: false, expectValue: int64(0), // we expect the zero values as is expectKey: "test_param", }, // Float tests { name: "required float - valid", args: map[string]interface{}{"test_param": float64(123.45)}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddRequiredFloat, expectError: false, expectValue: float64(123.45), expectKey: "test_param", }, { name: "optional float - valid", args: map[string]interface{}{"test_param": float64(123.45)}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddOptionalFloat, expectError: false, expectValue: float64(123.45), expectKey: "test_param", }, { name: "optional float - zero", args: map[string]interface{}{"test_param": float64(0)}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddOptionalFloat, expectError: false, expectValue: float64(0), expectKey: "test_param", }, // Bool tests { name: "required bool - true", args: map[string]interface{}{"test_param": true}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddRequiredBool, expectError: false, expectValue: true, expectKey: "test_param", }, { name: "required bool - false", args: map[string]interface{}{"test_param": false}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddRequiredBool, expectError: false, expectValue: false, expectKey: "test_param", }, { name: "optional bool - true", args: map[string]interface{}{"test_param": true}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddOptionalBool, expectError: false, expectValue: true, expectKey: "test_param", }, { name: "optional bool - false", args: map[string]interface{}{"test_param": false}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddOptionalBool, expectError: false, expectValue: false, expectKey: "test_param", }, // Map tests { name: "required map - valid", args: map[string]interface{}{ "test_param": map[string]interface{}{"key": "value"}, }, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddRequiredMap, expectError: false, expectValue: map[string]interface{}{"key": "value"}, expectKey: "test_param", }, { name: "optional map - valid", args: map[string]interface{}{ "test_param": map[string]interface{}{"key": "value"}, }, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddOptionalMap, expectError: false, expectValue: map[string]interface{}{"key": "value"}, expectKey: "test_param", }, { name: "optional map - empty", args: map[string]interface{}{ "test_param": map[string]interface{}{}, }, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddOptionalMap, expectError: false, expectValue: map[string]interface{}{}, expectKey: "test_param", }, // Array tests { name: "required array - valid", args: map[string]interface{}{ "test_param": []interface{}{"value1", "value2"}, }, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddRequiredArray, expectError: false, expectValue: []interface{}{"value1", "value2"}, expectKey: "test_param", }, { name: "optional array - valid", args: map[string]interface{}{ "test_param": []interface{}{"value1", "value2"}, }, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddOptionalArray, expectError: false, expectValue: []interface{}{"value1", "value2"}, expectKey: "test_param", }, { name: "optional array - empty", args: map[string]interface{}{"test_param": []interface{}{}}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddOptionalArray, expectError: false, expectValue: []interface{}{}, expectKey: "test_param", }, // Invalid type tests { name: "required string - wrong type", args: map[string]interface{}{"test_param": 123}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddRequiredString, expectError: true, expectValue: nil, expectKey: "test_param", }, { name: "required int - wrong type", args: map[string]interface{}{"test_param": "not a number"}, paramName: "test_param", validationFunc: (*Validator).ValidateAndAddRequiredInt, expectError: true, expectValue: nil, expectKey: "test_param", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := make(map[string]interface{}) request := &mcpgo.CallToolRequest{ Arguments: tt.args, } validator := NewValidator(request) tt.validationFunc(validator, result, tt.paramName) if tt.expectError { assert.True(t, validator.HasErrors(), "Expected validation error") } else { assert.False(t, validator.HasErrors(), "Did not expect validation error") assert.Equal(t, tt.expectValue, result[tt.expectKey], "Parameter value mismatch", ) } }) } } func TestValidatorPagination(t *testing.T) { tests := []struct { name string args map[string]interface{} expectCount interface{} expectSkip interface{} expectError bool }{ { name: "valid pagination params", args: map[string]interface{}{ "count": float64(10), "skip": float64(5), }, expectCount: int64(10), expectSkip: int64(5), expectError: false, }, { name: "zero pagination params", args: map[string]interface{}{"count": float64(0), "skip": float64(0)}, expectCount: int64(0), expectSkip: int64(0), expectError: false, }, { name: "invalid count type", args: map[string]interface{}{ "count": "not a number", "skip": float64(5), }, expectCount: nil, expectSkip: int64(5), expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := make(map[string]interface{}) request := &mcpgo.CallToolRequest{ Arguments: tt.args, } validator := NewValidator(request) validator.ValidateAndAddPagination(result) if tt.expectError { assert.True(t, validator.HasErrors(), "Expected validation error") } else { assert.False(t, validator.HasErrors(), "Did not expect validation error") } if tt.expectCount != nil { assert.Equal(t, tt.expectCount, result["count"], "Count mismatch") } else { _, exists := result["count"] assert.False(t, exists, "Count should not be added") } if tt.expectSkip != nil { assert.Equal(t, tt.expectSkip, result["skip"], "Skip mismatch") } else { _, exists := result["skip"] assert.False(t, exists, "Skip should not be added") } }) } } func TestValidatorExpand(t *testing.T) { tests := []struct { name string args map[string]interface{} expectExpand string expectError bool }{ { name: "valid expand param", args: map[string]interface{}{"expand": []interface{}{"payments"}}, expectExpand: "payments", expectError: false, }, { name: "empty expand array", args: map[string]interface{}{"expand": []interface{}{}}, expectExpand: "", expectError: false, }, { name: "invalid expand type", args: map[string]interface{}{"expand": "not an array"}, expectExpand: "", expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := make(map[string]interface{}) request := &mcpgo.CallToolRequest{ Arguments: tt.args, } validator := NewValidator(request) validator.ValidateAndAddExpand(result) if tt.expectError { assert.True(t, validator.HasErrors(), "Expected validation error") } else { assert.False(t, validator.HasErrors(), "Did not expect validation error") if tt.expectExpand != "" { assert.Equal(t, tt.expectExpand, result["expand[]"], "Expand value mismatch", ) } else { _, exists := result["expand[]"] assert.False(t, exists, "Expand should not be added") } } }) } } // Test validator "To" functions which write to target maps func TestValidatorToFunctions(t *testing.T) { tests := []struct { name string args map[string]interface{} paramName string targetKey string testFunc func( *Validator, map[string]interface{}, string, string, ) *Validator expectValue interface{} expectError bool }{ // ValidateAndAddOptionalStringToPath tests { name: "optional string to target - valid", args: map[string]interface{}{"customer_name": "Test User"}, paramName: "customer_name", targetKey: "name", testFunc: (*Validator).ValidateAndAddOptionalStringToPath, expectValue: "Test User", expectError: false, }, { name: "optional string to target - empty", args: map[string]interface{}{"customer_name": ""}, paramName: "customer_name", targetKey: "name", testFunc: (*Validator).ValidateAndAddOptionalStringToPath, expectValue: "", expectError: false, }, { name: "optional string to target - missing", args: map[string]interface{}{}, paramName: "customer_name", targetKey: "name", testFunc: (*Validator).ValidateAndAddOptionalStringToPath, expectValue: nil, expectError: false, }, { name: "optional string to target - wrong type", args: map[string]interface{}{"customer_name": 123}, paramName: "customer_name", targetKey: "name", testFunc: (*Validator).ValidateAndAddOptionalStringToPath, expectValue: nil, expectError: true, }, // ValidateAndAddOptionalBoolToPath tests { name: "optional bool to target - true", args: map[string]interface{}{"notify_sms": true}, paramName: "notify_sms", targetKey: "sms", testFunc: (*Validator).ValidateAndAddOptionalBoolToPath, expectValue: true, expectError: false, }, { name: "optional bool to target - false", args: map[string]interface{}{"notify_sms": false}, paramName: "notify_sms", targetKey: "sms", testFunc: (*Validator).ValidateAndAddOptionalBoolToPath, expectValue: false, expectError: false, }, { name: "optional bool to target - wrong type", args: map[string]interface{}{"notify_sms": "not a bool"}, paramName: "notify_sms", targetKey: "sms", testFunc: (*Validator).ValidateAndAddOptionalBoolToPath, expectValue: nil, expectError: true, }, // ValidateAndAddOptionalIntToPath tests { name: "optional int to target - valid", args: map[string]interface{}{"age": float64(25)}, paramName: "age", targetKey: "customer_age", testFunc: (*Validator).ValidateAndAddOptionalIntToPath, expectValue: int64(25), expectError: false, }, { name: "optional int to target - zero", args: map[string]interface{}{"age": float64(0)}, paramName: "age", targetKey: "customer_age", testFunc: (*Validator).ValidateAndAddOptionalIntToPath, expectValue: int64(0), expectError: false, }, { name: "optional int to target - missing", args: map[string]interface{}{}, paramName: "age", targetKey: "customer_age", testFunc: (*Validator).ValidateAndAddOptionalIntToPath, expectValue: nil, expectError: false, }, { name: "optional int to target - wrong type", args: map[string]interface{}{"age": "not a number"}, paramName: "age", targetKey: "customer_age", testFunc: (*Validator).ValidateAndAddOptionalIntToPath, expectValue: nil, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create a target map for this specific test target := make(map[string]interface{}) // Create the request and validator request := &mcpgo.CallToolRequest{ Arguments: tt.args, } validator := NewValidator(request) // Call the test function with target and verify its return value tt.testFunc(validator, target, tt.paramName, tt.targetKey) // Check if we got the expected errors if tt.expectError { assert.True(t, validator.HasErrors(), "Expected validation error") } else { assert.False(t, validator.HasErrors(), "Did not expect validation error") // For non-error cases, check target map value if tt.expectValue != nil { // Should have the value with the target key assert.Equal(t, tt.expectValue, target[tt.targetKey], "Target map value mismatch") } else { // Target key should not exist _, exists := target[tt.targetKey] assert.False(t, exists, "Key should not be in target map when value is empty") // nolint:lll } } }) } } // Test for nested validation with multiple fields into target maps func TestValidatorNestedObjects(t *testing.T) { t.Run("customer object validation", func(t *testing.T) { // Create request with customer details args := map[string]interface{}{ "customer_name": "John Doe", "customer_email": "[email protected]", "customer_contact": "+1234567890", } request := &mcpgo.CallToolRequest{ Arguments: args, } // Customer target map customer := make(map[string]interface{}) // Create validator and validate customer fields validator := NewValidator(request). ValidateAndAddOptionalStringToPath(customer, "customer_name", "name"). ValidateAndAddOptionalStringToPath(customer, "customer_email", "email"). ValidateAndAddOptionalStringToPath(customer, "customer_contact", "contact") // Should not have errors assert.False(t, validator.HasErrors()) // Customer map should have all three fields assert.Equal(t, "John Doe", customer["name"]) assert.Equal(t, "[email protected]", customer["email"]) assert.Equal(t, "+1234567890", customer["contact"]) }) t.Run("notification object validation", func(t *testing.T) { // Create request with notification settings args := map[string]interface{}{ "notify_sms": true, "notify_email": false, } request := &mcpgo.CallToolRequest{ Arguments: args, } // Notify target map notify := make(map[string]interface{}) // Create validator and validate notification fields validator := NewValidator(request). ValidateAndAddOptionalBoolToPath(notify, "notify_sms", "sms"). ValidateAndAddOptionalBoolToPath(notify, "notify_email", "email") // Should not have errors assert.False(t, validator.HasErrors()) // Notify map should have both fields assert.Equal(t, true, notify["sms"]) assert.Equal(t, false, notify["email"]) }) t.Run("mixed object with error", func(t *testing.T) { // Create request with mixed valid and invalid data args := map[string]interface{}{ "customer_name": "Jane Doe", "customer_email": 12345, // Wrong type } request := &mcpgo.CallToolRequest{ Arguments: args, } // Target map customer := make(map[string]interface{}) // Create validator and validate fields validator := NewValidator(request). ValidateAndAddOptionalStringToPath(customer, "customer_name", "name"). ValidateAndAddOptionalStringToPath(customer, "customer_email", "email") // Should have errors assert.True(t, validator.HasErrors()) // Customer map should have only the valid field assert.Equal(t, "Jane Doe", customer["name"]) _, hasEmail := customer["email"] assert.False(t, hasEmail, "Invalid field should not be added to target map") }) } // Test for optional bool handling func TestOptionalBoolBehavior(t *testing.T) { t.Run("explicit bool values", func(t *testing.T) { // Create request with explicit bool values args := map[string]interface{}{ "true_param": true, "false_param": false, } request := &mcpgo.CallToolRequest{ Arguments: args, } // Create result map result := make(map[string]interface{}) // Validate both parameters validator := NewValidator(request). ValidateAndAddOptionalBool(result, "true_param"). ValidateAndAddOptionalBool(result, "false_param") // Verify no errors occurred assert.False(t, validator.HasErrors()) // Both parameters should be set in the result assert.Equal(t, true, result["true_param"]) assert.Equal(t, false, result["false_param"]) }) t.Run("missing bool parameter", func(t *testing.T) { // Create request without bool parameters args := map[string]interface{}{ "other_param": "some value", } request := &mcpgo.CallToolRequest{ Arguments: args, } // Create result map result := make(map[string]interface{}) // Try to validate missing bool parameters validator := NewValidator(request). ValidateAndAddOptionalBool(result, "true_param"). ValidateAndAddOptionalBool(result, "false_param") // Verify no errors occurred assert.False(t, validator.HasErrors()) // Result should be empty since no bool values were provided assert.Empty(t, result) }) t.Run("explicit bool values with 'To' functions", func(t *testing.T) { // Create request with explicit bool values args := map[string]interface{}{ "notify_sms": true, "notify_email": false, } request := &mcpgo.CallToolRequest{ Arguments: args, } // Create target map target := make(map[string]interface{}) // Validate both parameters validator := NewValidator(request). ValidateAndAddOptionalBoolToPath(target, "notify_sms", "sms"). ValidateAndAddOptionalBoolToPath(target, "notify_email", "email") // Verify no errors occurred assert.False(t, validator.HasErrors()) // Both parameters should be set in the target map assert.Equal(t, true, target["sms"]) assert.Equal(t, false, target["email"]) }) t.Run("missing bool parameter with 'To' functions", func(t *testing.T) { // Create request without bool parameters args := map[string]interface{}{ "other_param": "some value", } request := &mcpgo.CallToolRequest{ Arguments: args, } // Create target map target := make(map[string]interface{}) // Try to validate missing bool parameters validator := NewValidator(request). ValidateAndAddOptionalBoolToPath(target, "notify_sms", "sms"). ValidateAndAddOptionalBoolToPath(target, "notify_email", "email") // Verify no errors occurred assert.False(t, validator.HasErrors()) // Target map should be empty since no bool values were provided assert.Empty(t, target) }) } // Test for extractValueGeneric function edge cases func TestExtractValueGeneric(t *testing.T) { t.Run("invalid arguments type", func(t *testing.T) { request := &mcpgo.CallToolRequest{ Arguments: "invalid_type", // Not a map } result, err := extractValueGeneric[string](request, "test", false) assert.Error(t, err) assert.Equal(t, "invalid arguments type", err.Error()) assert.Nil(t, result) }) t.Run("json marshal error", func(t *testing.T) { // Create a value that can't be marshaled to JSON args := map[string]interface{}{ "test_param": make(chan int), // Channels can't be marshaled } request := &mcpgo.CallToolRequest{ Arguments: args, } result, err := extractValueGeneric[string](request, "test_param", false) assert.Error(t, err) assert.Equal(t, "invalid parameter type: test_param", err.Error()) assert.Nil(t, result) }) t.Run("json unmarshal error", func(t *testing.T) { // Provide a value that can't be unmarshaled to the target type args := map[string]interface{}{ "test_param": []interface{}{1, 2, 3}, // Array can't be unmarshaled to string } request := &mcpgo.CallToolRequest{ Arguments: args, } result, err := extractValueGeneric[string](request, "test_param", false) assert.Error(t, err) assert.Equal(t, "invalid parameter type: test_param", err.Error()) assert.Nil(t, result) }) } // Test for validateAndAddRequired function func TestValidateAndAddRequired(t *testing.T) { t.Run("successful validation", func(t *testing.T) { args := map[string]interface{}{ "test_param": "test_value", } request := &mcpgo.CallToolRequest{ Arguments: args, } params := make(map[string]interface{}) validator := NewValidator(request) result := validateAndAddRequired[string](validator, params, "test_param") assert.False(t, result.HasErrors()) assert.Equal(t, "test_value", params["test_param"]) }) t.Run("validation error", func(t *testing.T) { request := &mcpgo.CallToolRequest{ Arguments: "invalid_type", } params := make(map[string]interface{}) validator := NewValidator(request) result := validateAndAddRequired[string](validator, params, "test_param") assert.True(t, result.HasErrors()) assert.Empty(t, params) }) t.Run("nil value after successful extraction", func(t *testing.T) { // This edge case is hard to trigger directly, but we can simulate it // by using a type that extractValueGeneric might return as nil args := map[string]interface{}{ "test_param": nil, } request := &mcpgo.CallToolRequest{ Arguments: args, } params := make(map[string]interface{}) validator := NewValidator(request) result := validateAndAddRequired[string](validator, params, "test_param") // This should result in an error because the parameter is required assert.True(t, result.HasErrors()) assert.Empty(t, params) }) } // Test for validateAndAddOptional function func TestValidateAndAddOptional(t *testing.T) { t.Run("successful validation", func(t *testing.T) { args := map[string]interface{}{ "test_param": "test_value", } request := &mcpgo.CallToolRequest{ Arguments: args, } params := make(map[string]interface{}) validator := NewValidator(request) result := validateAndAddOptional[string](validator, params, "test_param") assert.False(t, result.HasErrors()) assert.Equal(t, "test_value", params["test_param"]) }) t.Run("validation error", func(t *testing.T) { request := &mcpgo.CallToolRequest{ Arguments: "invalid_type", } params := make(map[string]interface{}) validator := NewValidator(request) result := validateAndAddOptional[string](validator, params, "test_param") assert.True(t, result.HasErrors()) assert.Empty(t, params) }) t.Run("nil value handling", func(t *testing.T) { args := map[string]interface{}{ "test_param": nil, } request := &mcpgo.CallToolRequest{ Arguments: args, } params := make(map[string]interface{}) validator := NewValidator(request) result := validateAndAddOptional[string](validator, params, "test_param") assert.False(t, result.HasErrors()) assert.Empty(t, params) }) } // Test for validateAndAddToPath function func TestValidateAndAddToPath(t *testing.T) { t.Run("successful validation", func(t *testing.T) { args := map[string]interface{}{ "test_param": "test_value", } request := &mcpgo.CallToolRequest{ Arguments: args, } target := make(map[string]interface{}) validator := NewValidator(request) result := validateAndAddToPath[string]( validator, target, "test_param", "target_key") assert.False(t, result.HasErrors()) assert.Equal(t, "test_value", target["target_key"]) }) t.Run("validation error", func(t *testing.T) { request := &mcpgo.CallToolRequest{ Arguments: "invalid_type", } target := make(map[string]interface{}) validator := NewValidator(request) result := validateAndAddToPath[string]( validator, target, "test_param", "target_key") assert.True(t, result.HasErrors()) assert.Empty(t, target) }) t.Run("nil value handling", func(t *testing.T) { args := map[string]interface{}{ "test_param": nil, } request := &mcpgo.CallToolRequest{ Arguments: args, } target := make(map[string]interface{}) validator := NewValidator(request) result := validateAndAddToPath[string]( validator, target, "test_param", "target_key") assert.False(t, result.HasErrors()) assert.Empty(t, target) }) } // Test for ValidateAndAddPagination function func TestValidateAndAddPagination(t *testing.T) { t.Run("all pagination parameters", func(t *testing.T) { args := map[string]interface{}{ "count": 10, "skip": 5, } request := &mcpgo.CallToolRequest{ Arguments: args, } params := make(map[string]interface{}) validator := NewValidator(request).ValidateAndAddPagination(params) assert.False(t, validator.HasErrors()) assert.Equal(t, int64(10), params["count"]) assert.Equal(t, int64(5), params["skip"]) }) t.Run("missing pagination parameters", func(t *testing.T) { args := map[string]interface{}{} request := &mcpgo.CallToolRequest{ Arguments: args, } params := make(map[string]interface{}) validator := NewValidator(request).ValidateAndAddPagination(params) assert.False(t, validator.HasErrors()) assert.Empty(t, params) }) t.Run("invalid count type", func(t *testing.T) { args := map[string]interface{}{ "count": "invalid", } request := &mcpgo.CallToolRequest{ Arguments: args, } params := make(map[string]interface{}) validator := NewValidator(request).ValidateAndAddPagination(params) assert.True(t, validator.HasErrors()) }) } // Test for ValidateAndAddExpand function func TestValidateAndAddExpand(t *testing.T) { t.Run("valid expand parameter", func(t *testing.T) { args := map[string]interface{}{ "expand": []string{"payments", "customer"}, } request := &mcpgo.CallToolRequest{ Arguments: args, } params := make(map[string]interface{}) validator := NewValidator(request).ValidateAndAddExpand(params) assert.False(t, validator.HasErrors()) // The function sets expand[] for each value, so check the last one assert.Equal(t, "customer", params["expand[]"]) }) t.Run("missing expand parameter", func(t *testing.T) { args := map[string]interface{}{} request := &mcpgo.CallToolRequest{ Arguments: args, } params := make(map[string]interface{}) validator := NewValidator(request).ValidateAndAddExpand(params) assert.False(t, validator.HasErrors()) assert.Empty(t, params) }) t.Run("invalid expand type", func(t *testing.T) { args := map[string]interface{}{ "expand": "invalid", // Should be []string, not string } request := &mcpgo.CallToolRequest{ Arguments: args, } params := make(map[string]interface{}) validator := NewValidator(request).ValidateAndAddExpand(params) assert.True(t, validator.HasErrors()) }) } // Test for token validation functions edge cases func TestTokenValidationEdgeCases(t *testing.T) { t.Run("validateTokenMaxAmount - int conversion", func(t *testing.T) { token := map[string]interface{}{ "max_amount": 100, // int instead of float64 } request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}} validator := NewValidator(request).validateTokenMaxAmount(token) assert.False(t, validator.HasErrors()) assert.Equal(t, float64(100), token["max_amount"]) }) t.Run("validateTokenExpireAt - int conversion", func(t *testing.T) { token := map[string]interface{}{ "expire_at": 1234567890, // int instead of float64 } request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}} validator := NewValidator(request).validateTokenExpireAt(token) assert.False(t, validator.HasErrors()) assert.Equal(t, float64(1234567890), token["expire_at"]) }) t.Run("validateTokenExpireAt - zero value", func(t *testing.T) { token := map[string]interface{}{ "expire_at": 0, } request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}} validator := NewValidator(request).validateTokenExpireAt(token) assert.True(t, validator.HasErrors()) }) t.Run("validateTokenMaxAmount - zero value", func(t *testing.T) { token := map[string]interface{}{ "max_amount": 0, } request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}} validator := NewValidator(request).validateTokenMaxAmount(token) assert.True(t, validator.HasErrors()) }) } // Test for ValidateAndAddToken edge cases func TestValidateAndAddTokenEdgeCases(t *testing.T) { t.Run("token extraction error", func(t *testing.T) { request := &mcpgo.CallToolRequest{ Arguments: "invalid_type", } params := make(map[string]interface{}) validator := NewValidator(request).ValidateAndAddToken(params, "token") assert.True(t, validator.HasErrors()) assert.Empty(t, params) }) t.Run("nil token value", func(t *testing.T) { args := map[string]interface{}{ "token": nil, } request := &mcpgo.CallToolRequest{ Arguments: args, } params := make(map[string]interface{}) validator := NewValidator(request).ValidateAndAddToken(params, "token") assert.False(t, validator.HasErrors()) assert.Empty(t, params) }) t.Run("token validation errors", func(t *testing.T) { args := map[string]interface{}{ "token": map[string]interface{}{ "max_amount": -100, // Invalid value }, } request := &mcpgo.CallToolRequest{ Arguments: args, } params := make(map[string]interface{}) validator := NewValidator(request).ValidateAndAddToken(params, "token") assert.True(t, validator.HasErrors()) assert.Empty(t, params) }) } ```