This is page 2 of 5. Use http://codebase.md/razorpay/razorpay-mcp-server?lines=true&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/settlements.go: -------------------------------------------------------------------------------- ```go 1 | package razorpay 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | rzpsdk "github.com/razorpay/razorpay-go" 8 | 9 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" 10 | "github.com/razorpay/razorpay-mcp-server/pkg/observability" 11 | ) 12 | 13 | // FetchSettlement returns a tool that fetches a settlement by ID 14 | func FetchSettlement( 15 | obs *observability.Observability, 16 | client *rzpsdk.Client, 17 | ) mcpgo.Tool { 18 | parameters := []mcpgo.ToolParameter{ 19 | mcpgo.WithString( 20 | "settlement_id", 21 | mcpgo.Description("The ID of the settlement to fetch."+ 22 | "ID starts with the 'setl_'"), 23 | mcpgo.Required(), 24 | ), 25 | } 26 | 27 | handler := func( 28 | ctx context.Context, 29 | r mcpgo.CallToolRequest, 30 | ) (*mcpgo.ToolResult, error) { 31 | client, err := getClientFromContextOrDefault(ctx, client) 32 | if err != nil { 33 | return mcpgo.NewToolResultError(err.Error()), nil 34 | } 35 | 36 | // Create a parameters map to collect validated parameters 37 | fetchSettlementOptions := make(map[string]interface{}) 38 | 39 | // Validate using fluent validator 40 | validator := NewValidator(&r). 41 | ValidateAndAddRequiredString(fetchSettlementOptions, "settlement_id") 42 | 43 | if result, err := validator.HandleErrorsIfAny(); result != nil { 44 | return result, err 45 | } 46 | 47 | settlementID := fetchSettlementOptions["settlement_id"].(string) 48 | settlement, err := client.Settlement.Fetch(settlementID, nil, nil) 49 | if err != nil { 50 | return mcpgo.NewToolResultError( 51 | fmt.Sprintf("fetching settlement failed: %s", err.Error())), nil 52 | } 53 | 54 | return mcpgo.NewToolResultJSON(settlement) 55 | } 56 | 57 | return mcpgo.NewTool( 58 | "fetch_settlement_with_id", 59 | "Fetch details of a specific settlement using its ID", 60 | parameters, 61 | handler, 62 | ) 63 | } 64 | 65 | // FetchSettlementRecon returns a tool that fetches settlement 66 | // reconciliation reports 67 | func FetchSettlementRecon( 68 | obs *observability.Observability, 69 | client *rzpsdk.Client, 70 | ) mcpgo.Tool { 71 | parameters := []mcpgo.ToolParameter{ 72 | mcpgo.WithNumber( 73 | "year", 74 | mcpgo.Description("Year for which the settlement report is "+ 75 | "requested (YYYY format)"), 76 | mcpgo.Required(), 77 | ), 78 | mcpgo.WithNumber( 79 | "month", 80 | mcpgo.Description("Month for which the settlement report is "+ 81 | "requested (MM format)"), 82 | mcpgo.Required(), 83 | ), 84 | mcpgo.WithNumber( 85 | "day", 86 | mcpgo.Description("Optional: Day for which the settlement report is "+ 87 | "requested (DD format)"), 88 | ), 89 | mcpgo.WithNumber( 90 | "count", 91 | mcpgo.Description("Optional: Number of records to fetch "+ 92 | "(default: 10, max: 100)"), 93 | ), 94 | mcpgo.WithNumber( 95 | "skip", 96 | mcpgo.Description("Optional: Number of records to skip for pagination"), 97 | ), 98 | } 99 | 100 | handler := func( 101 | ctx context.Context, 102 | r mcpgo.CallToolRequest, 103 | ) (*mcpgo.ToolResult, error) { 104 | client, err := getClientFromContextOrDefault(ctx, client) 105 | if err != nil { 106 | return mcpgo.NewToolResultError(err.Error()), nil 107 | } 108 | 109 | // Create a parameters map to collect validated parameters 110 | fetchReconOptions := make(map[string]interface{}) 111 | 112 | // Validate using fluent validator 113 | validator := NewValidator(&r). 114 | ValidateAndAddRequiredInt(fetchReconOptions, "year"). 115 | ValidateAndAddRequiredInt(fetchReconOptions, "month"). 116 | ValidateAndAddOptionalInt(fetchReconOptions, "day"). 117 | ValidateAndAddPagination(fetchReconOptions) 118 | 119 | if result, err := validator.HandleErrorsIfAny(); result != nil { 120 | return result, err 121 | } 122 | 123 | report, err := client.Settlement.Reports(fetchReconOptions, nil) 124 | if err != nil { 125 | return mcpgo.NewToolResultError( 126 | fmt.Sprintf("fetching settlement reconciliation report failed: %s", 127 | err.Error())), nil 128 | } 129 | 130 | return mcpgo.NewToolResultJSON(report) 131 | } 132 | 133 | return mcpgo.NewTool( 134 | "fetch_settlement_recon_details", 135 | "Fetch settlement reconciliation report for a specific time period", 136 | parameters, 137 | handler, 138 | ) 139 | } 140 | 141 | // FetchAllSettlements returns a tool to fetch multiple settlements with 142 | // filtering and pagination 143 | func FetchAllSettlements( 144 | obs *observability.Observability, 145 | client *rzpsdk.Client, 146 | ) mcpgo.Tool { 147 | parameters := []mcpgo.ToolParameter{ 148 | // Pagination parameters 149 | mcpgo.WithNumber( 150 | "count", 151 | mcpgo.Description("Number of settlement records to fetch "+ 152 | "(default: 10, max: 100)"), 153 | mcpgo.Min(1), 154 | mcpgo.Max(100), 155 | ), 156 | mcpgo.WithNumber( 157 | "skip", 158 | mcpgo.Description("Number of settlement records to skip (default: 0)"), 159 | mcpgo.Min(0), 160 | ), 161 | // Time range filters 162 | mcpgo.WithNumber( 163 | "from", 164 | mcpgo.Description("Unix timestamp (in seconds) from when "+ 165 | "settlements are to be fetched"), 166 | mcpgo.Min(0), 167 | ), 168 | mcpgo.WithNumber( 169 | "to", 170 | mcpgo.Description("Unix timestamp (in seconds) up till when "+ 171 | "settlements are to be fetched"), 172 | mcpgo.Min(0), 173 | ), 174 | } 175 | 176 | handler := func( 177 | ctx context.Context, 178 | r mcpgo.CallToolRequest, 179 | ) (*mcpgo.ToolResult, error) { 180 | client, err := getClientFromContextOrDefault(ctx, client) 181 | if err != nil { 182 | return mcpgo.NewToolResultError(err.Error()), nil 183 | } 184 | 185 | // Create parameters map to collect validated parameters 186 | fetchAllSettlementsOptions := make(map[string]interface{}) 187 | 188 | // Validate using fluent validator 189 | validator := NewValidator(&r). 190 | ValidateAndAddPagination(fetchAllSettlementsOptions). 191 | ValidateAndAddOptionalInt(fetchAllSettlementsOptions, "from"). 192 | ValidateAndAddOptionalInt(fetchAllSettlementsOptions, "to") 193 | 194 | if result, err := validator.HandleErrorsIfAny(); result != nil { 195 | return result, err 196 | } 197 | 198 | // Fetch all settlements using Razorpay SDK 199 | settlements, err := client.Settlement.All(fetchAllSettlementsOptions, nil) 200 | if err != nil { 201 | return mcpgo.NewToolResultError( 202 | fmt.Sprintf("fetching settlements failed: %s", err.Error())), nil 203 | } 204 | 205 | return mcpgo.NewToolResultJSON(settlements) 206 | } 207 | 208 | return mcpgo.NewTool( 209 | "fetch_all_settlements", 210 | "Fetch all settlements with optional filtering and pagination", 211 | parameters, 212 | handler, 213 | ) 214 | } 215 | 216 | // CreateInstantSettlement returns a tool that creates an instant settlement 217 | func CreateInstantSettlement( 218 | obs *observability.Observability, 219 | client *rzpsdk.Client, 220 | ) mcpgo.Tool { 221 | parameters := []mcpgo.ToolParameter{ 222 | mcpgo.WithNumber( 223 | "amount", 224 | mcpgo.Description("The amount you want to get settled instantly in amount in the smallest "+ //nolint:lll 225 | "currency sub-unit (e.g., for ₹295, use 29500)"), 226 | mcpgo.Required(), 227 | mcpgo.Min(200), // Minimum amount is 200 (₹2) 228 | ), 229 | mcpgo.WithBoolean( 230 | "settle_full_balance", 231 | mcpgo.Description("If true, Razorpay will settle the maximum amount "+ 232 | "possible and ignore amount parameter"), 233 | mcpgo.DefaultValue(false), 234 | ), 235 | mcpgo.WithString( 236 | "description", 237 | mcpgo.Description("Custom note for the instant settlement."), 238 | mcpgo.Max(30), 239 | mcpgo.Pattern("^[a-zA-Z0-9 ]*$"), 240 | ), 241 | mcpgo.WithObject( 242 | "notes", 243 | mcpgo.Description("Key-value pairs for additional information. "+ 244 | "Max 15 pairs, 256 chars each"), 245 | mcpgo.MaxProperties(15), 246 | ), 247 | } 248 | 249 | handler := func( 250 | ctx context.Context, 251 | r mcpgo.CallToolRequest, 252 | ) (*mcpgo.ToolResult, error) { 253 | client, err := getClientFromContextOrDefault(ctx, client) 254 | if err != nil { 255 | return mcpgo.NewToolResultError(err.Error()), nil 256 | } 257 | 258 | // Create parameters map to collect validated parameters 259 | createInstantSettlementReq := make(map[string]interface{}) 260 | 261 | // Validate using fluent validator 262 | validator := NewValidator(&r). 263 | ValidateAndAddRequiredInt(createInstantSettlementReq, "amount"). 264 | ValidateAndAddOptionalBool(createInstantSettlementReq, "settle_full_balance"). // nolint:lll 265 | ValidateAndAddOptionalString(createInstantSettlementReq, "description"). 266 | ValidateAndAddOptionalMap(createInstantSettlementReq, "notes") 267 | 268 | if result, err := validator.HandleErrorsIfAny(); result != nil { 269 | return result, err 270 | } 271 | 272 | // Create the instant settlement 273 | settlement, err := client.Settlement.CreateOnDemandSettlement( 274 | createInstantSettlementReq, nil) 275 | if err != nil { 276 | return mcpgo.NewToolResultError( 277 | fmt.Sprintf("creating instant settlement failed: %s", 278 | err.Error())), nil 279 | } 280 | 281 | return mcpgo.NewToolResultJSON(settlement) 282 | } 283 | 284 | return mcpgo.NewTool( 285 | "create_instant_settlement", 286 | "Create an instant settlement to get funds transferred to your bank account", // nolint:lll 287 | parameters, 288 | handler, 289 | ) 290 | } 291 | 292 | // FetchAllInstantSettlements returns a tool to fetch all instant settlements 293 | // with filtering and pagination 294 | func FetchAllInstantSettlements( 295 | obs *observability.Observability, 296 | client *rzpsdk.Client, 297 | ) mcpgo.Tool { 298 | parameters := []mcpgo.ToolParameter{ 299 | // Pagination parameters 300 | mcpgo.WithNumber( 301 | "count", 302 | mcpgo.Description("Number of instant settlement records to fetch "+ 303 | "(default: 10, max: 100)"), 304 | mcpgo.Min(1), 305 | mcpgo.Max(100), 306 | ), 307 | mcpgo.WithNumber( 308 | "skip", 309 | mcpgo.Description("Number of instant settlement records to skip (default: 0)"), //nolint:lll 310 | mcpgo.Min(0), 311 | ), 312 | // Time range filters 313 | mcpgo.WithNumber( 314 | "from", 315 | mcpgo.Description("Unix timestamp (in seconds) from when "+ 316 | "instant settlements are to be fetched"), 317 | mcpgo.Min(0), 318 | ), 319 | mcpgo.WithNumber( 320 | "to", 321 | mcpgo.Description("Unix timestamp (in seconds) up till when "+ 322 | "instant settlements are to be fetched"), 323 | mcpgo.Min(0), 324 | ), 325 | // Expand parameter for payout details 326 | mcpgo.WithArray( 327 | "expand", 328 | mcpgo.Description("Pass this if you want to fetch payout details "+ 329 | "as part of the response for all instant settlements. "+ 330 | "Supported values: ondemand_payouts"), 331 | ), 332 | } 333 | 334 | handler := func( 335 | ctx context.Context, 336 | r mcpgo.CallToolRequest, 337 | ) (*mcpgo.ToolResult, error) { 338 | client, err := getClientFromContextOrDefault(ctx, client) 339 | if err != nil { 340 | return mcpgo.NewToolResultError(err.Error()), nil 341 | } 342 | 343 | // Create parameters map to collect validated parameters 344 | options := make(map[string]interface{}) 345 | 346 | // Validate using fluent validator 347 | validator := NewValidator(&r). 348 | ValidateAndAddPagination(options). 349 | ValidateAndAddExpand(options). 350 | ValidateAndAddOptionalInt(options, "from"). 351 | ValidateAndAddOptionalInt(options, "to") 352 | 353 | if result, err := validator.HandleErrorsIfAny(); result != nil { 354 | return result, err 355 | } 356 | 357 | // Fetch all instant settlements using Razorpay SDK 358 | settlements, err := client.Settlement.FetchAllOnDemandSettlement(options, nil) 359 | if err != nil { 360 | return mcpgo.NewToolResultError( 361 | fmt.Sprintf("fetching instant settlements failed: %s", err.Error())), nil 362 | } 363 | 364 | return mcpgo.NewToolResultJSON(settlements) 365 | } 366 | 367 | return mcpgo.NewTool( 368 | "fetch_all_instant_settlements", 369 | "Fetch all instant settlements with optional filtering, pagination, and payout details", //nolint:lll 370 | parameters, 371 | handler, 372 | ) 373 | } 374 | 375 | // FetchInstantSettlement returns a tool that fetches instant settlement by ID 376 | func FetchInstantSettlement( 377 | obs *observability.Observability, 378 | client *rzpsdk.Client, 379 | ) mcpgo.Tool { 380 | parameters := []mcpgo.ToolParameter{ 381 | mcpgo.WithString( 382 | "settlement_id", 383 | mcpgo.Description("The ID of the instant settlement to fetch. "+ 384 | "ID starts with 'setlod_'"), 385 | mcpgo.Required(), 386 | ), 387 | } 388 | 389 | handler := func( 390 | ctx context.Context, 391 | r mcpgo.CallToolRequest, 392 | ) (*mcpgo.ToolResult, error) { 393 | client, err := getClientFromContextOrDefault(ctx, client) 394 | if err != nil { 395 | return mcpgo.NewToolResultError(err.Error()), nil 396 | } 397 | 398 | // Create parameters map to collect validated parameters 399 | params := make(map[string]interface{}) 400 | 401 | // Validate using fluent validator 402 | validator := NewValidator(&r). 403 | ValidateAndAddRequiredString(params, "settlement_id") 404 | 405 | if result, err := validator.HandleErrorsIfAny(); result != nil { 406 | return result, err 407 | } 408 | 409 | settlementID := params["settlement_id"].(string) 410 | 411 | // Fetch the instant settlement by ID using SDK 412 | settlement, err := client.Settlement.FetchOnDemandSettlementById( 413 | settlementID, nil, nil) 414 | if err != nil { 415 | return mcpgo.NewToolResultError( 416 | fmt.Sprintf("fetching instant settlement failed: %s", err.Error())), nil 417 | } 418 | 419 | return mcpgo.NewToolResultJSON(settlement) 420 | } 421 | 422 | return mcpgo.NewTool( 423 | "fetch_instant_settlement_with_id", 424 | "Fetch details of a specific instant settlement using its ID", 425 | parameters, 426 | handler, 427 | ) 428 | } 429 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/orders.go: -------------------------------------------------------------------------------- ```go 1 | package razorpay 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | rzpsdk "github.com/razorpay/razorpay-go" 8 | 9 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" 10 | "github.com/razorpay/razorpay-mcp-server/pkg/observability" 11 | ) 12 | 13 | // CreateOrder returns a tool that creates new orders in Razorpay 14 | func CreateOrder( 15 | obs *observability.Observability, 16 | client *rzpsdk.Client, 17 | ) mcpgo.Tool { 18 | parameters := []mcpgo.ToolParameter{ 19 | mcpgo.WithNumber( 20 | "amount", 21 | mcpgo.Description("Payment amount in the smallest "+ 22 | "currency sub-unit (e.g., for ₹295, use 29500)"), 23 | mcpgo.Required(), 24 | mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency) 25 | ), 26 | mcpgo.WithString( 27 | "currency", 28 | mcpgo.Description("ISO code for the currency "+ 29 | "(e.g., INR, USD, SGD)"), 30 | mcpgo.Required(), 31 | mcpgo.Pattern("^[A-Z]{3}$"), // ISO currency codes are 3 uppercase letters 32 | ), 33 | mcpgo.WithString( 34 | "receipt", 35 | mcpgo.Description("Receipt number for internal "+ 36 | "reference (max 40 chars, must be unique)"), 37 | mcpgo.Max(40), 38 | ), 39 | mcpgo.WithObject( 40 | "notes", 41 | mcpgo.Description("Key-value pairs for additional "+ 42 | "information (max 15 pairs, 256 chars each)"), 43 | mcpgo.MaxProperties(15), 44 | ), 45 | mcpgo.WithBoolean( 46 | "partial_payment", 47 | mcpgo.Description("Whether the customer can make partial payments"), 48 | mcpgo.DefaultValue(false), 49 | ), 50 | mcpgo.WithNumber( 51 | "first_payment_min_amount", 52 | mcpgo.Description("Minimum amount for first partial "+ 53 | "payment (only if partial_payment is true)"), 54 | mcpgo.Min(100), 55 | ), 56 | mcpgo.WithArray( 57 | "transfers", 58 | mcpgo.Description("Array of transfer objects for distributing "+ 59 | "payment amounts among multiple linked accounts. Each transfer "+ 60 | "object should contain: account (linked account ID), amount "+ 61 | "(in currency subunits), currency (ISO code), and optional fields "+ 62 | "like notes, linked_account_notes, on_hold, on_hold_until"), 63 | ), 64 | mcpgo.WithString( 65 | "method", 66 | mcpgo.Description("Payment method for mandate orders. "+ 67 | "REQUIRED for mandate orders. Must be 'upi' when using "+ 68 | "token.type='single_block_multiple_debit'. This field is used "+ 69 | "only for mandate/recurring payment orders."), 70 | ), 71 | mcpgo.WithString( 72 | "customer_id", 73 | mcpgo.Description("Customer ID for mandate orders. "+ 74 | "REQUIRED for mandate orders. Must start with 'cust_' followed by "+ 75 | "alphanumeric characters. Example: 'cust_xxx'. "+ 76 | "This identifies the customer for recurring payments."), 77 | ), 78 | mcpgo.WithObject( 79 | "token", 80 | mcpgo.Description("Token object for mandate orders. "+ 81 | "REQUIRED for mandate orders. Must contain: max_amount "+ 82 | "(positive number, maximum debit amount), frequency "+ 83 | "(as_presented/monthly/one_time/yearly/weekly/daily), "+ 84 | "type='single_block_multiple_debit' (only supported type), "+ 85 | "and optionally expire_at (Unix timestamp, defaults to today+60days). "+ 86 | "Example: {\"max_amount\": 100, \"frequency\": \"as_presented\", "+ 87 | "\"type\": \"single_block_multiple_debit\"}"), 88 | ), 89 | } 90 | 91 | handler := func( 92 | ctx context.Context, 93 | r mcpgo.CallToolRequest, 94 | ) (*mcpgo.ToolResult, error) { 95 | // Get client from context or use default 96 | client, err := getClientFromContextOrDefault(ctx, client) 97 | if err != nil { 98 | return mcpgo.NewToolResultError(err.Error()), nil 99 | } 100 | 101 | payload := make(map[string]interface{}) 102 | 103 | validator := NewValidator(&r). 104 | ValidateAndAddRequiredFloat(payload, "amount"). 105 | ValidateAndAddRequiredString(payload, "currency"). 106 | ValidateAndAddOptionalString(payload, "receipt"). 107 | ValidateAndAddOptionalMap(payload, "notes"). 108 | ValidateAndAddOptionalBool(payload, "partial_payment"). 109 | ValidateAndAddOptionalArray(payload, "transfers"). 110 | ValidateAndAddOptionalString(payload, "method"). 111 | ValidateAndAddOptionalString(payload, "customer_id"). 112 | ValidateAndAddToken(payload, "token") 113 | 114 | // Add first_payment_min_amount only if partial_payment is true 115 | if payload["partial_payment"] == true { 116 | validator.ValidateAndAddOptionalFloat(payload, "first_payment_min_amount") 117 | } 118 | 119 | if result, err := validator.HandleErrorsIfAny(); result != nil { 120 | return result, err 121 | } 122 | 123 | order, err := client.Order.Create(payload, nil) 124 | if err != nil { 125 | return mcpgo.NewToolResultError( 126 | fmt.Sprintf("creating order failed: %s", err.Error()), 127 | ), nil 128 | } 129 | 130 | return mcpgo.NewToolResultJSON(order) 131 | } 132 | 133 | return mcpgo.NewTool( 134 | "create_order", 135 | "Create a new order in Razorpay. Supports both regular orders and "+ 136 | "mandate orders. "+ 137 | "\n\nFor REGULAR ORDERS: Provide amount, currency, and optional "+ 138 | "receipt/notes. "+ 139 | "\n\nFor MANDATE ORDERS (recurring payments): You MUST provide ALL "+ 140 | "of these fields: "+ 141 | "amount, currency, method='upi', customer_id (starts with 'cust_'), "+ 142 | "and token object. "+ 143 | "\n\nThe token object is required for mandate orders and must contain: "+ 144 | "max_amount (positive number), frequency "+ 145 | "(as_presented/monthly/one_time/yearly/weekly/daily), "+ 146 | "type='single_block_multiple_debit', and optionally expire_at "+ 147 | "(defaults to today+60days). "+ 148 | "\n\nIMPORTANT: When token.type is 'single_block_multiple_debit', "+ 149 | "the method MUST be 'upi'. "+ 150 | "\n\nExample mandate order payload: "+ 151 | `{"amount": 100, "currency": "INR", "method": "upi", `+ 152 | `"customer_id": "cust_abc123", `+ 153 | `"token": {"max_amount": 100, "frequency": "as_presented", `+ 154 | `"type": "single_block_multiple_debit"}, `+ 155 | `"receipt": "Receipt No. 1", "notes": {"key": "value"}}`, 156 | parameters, 157 | handler, 158 | ) 159 | } 160 | 161 | // FetchOrder returns a tool to fetch order details by ID 162 | func FetchOrder( 163 | obs *observability.Observability, 164 | client *rzpsdk.Client, 165 | ) mcpgo.Tool { 166 | parameters := []mcpgo.ToolParameter{ 167 | mcpgo.WithString( 168 | "order_id", 169 | mcpgo.Description("Unique identifier of the order to be retrieved"), 170 | mcpgo.Required(), 171 | ), 172 | } 173 | 174 | handler := func( 175 | ctx context.Context, 176 | r mcpgo.CallToolRequest, 177 | ) (*mcpgo.ToolResult, error) { 178 | // Get client from context or use default 179 | client, err := getClientFromContextOrDefault(ctx, client) 180 | if err != nil { 181 | return mcpgo.NewToolResultError(err.Error()), nil 182 | } 183 | 184 | payload := make(map[string]interface{}) 185 | 186 | validator := NewValidator(&r). 187 | ValidateAndAddRequiredString(payload, "order_id") 188 | 189 | if result, err := validator.HandleErrorsIfAny(); result != nil { 190 | return result, err 191 | } 192 | 193 | order, err := client.Order.Fetch(payload["order_id"].(string), nil, nil) 194 | if err != nil { 195 | return mcpgo.NewToolResultError( 196 | fmt.Sprintf("fetching order failed: %s", err.Error()), 197 | ), nil 198 | } 199 | 200 | return mcpgo.NewToolResultJSON(order) 201 | } 202 | 203 | return mcpgo.NewTool( 204 | "fetch_order", 205 | "Fetch an order's details using its ID", 206 | parameters, 207 | handler, 208 | ) 209 | } 210 | 211 | // FetchAllOrders returns a tool to fetch all orders with optional filtering 212 | func FetchAllOrders( 213 | obs *observability.Observability, 214 | client *rzpsdk.Client, 215 | ) mcpgo.Tool { 216 | parameters := []mcpgo.ToolParameter{ 217 | mcpgo.WithNumber( 218 | "count", 219 | mcpgo.Description("Number of orders to be fetched "+ 220 | "(default: 10, max: 100)"), 221 | mcpgo.Min(1), 222 | mcpgo.Max(100), 223 | ), 224 | mcpgo.WithNumber( 225 | "skip", 226 | mcpgo.Description("Number of orders to be skipped (default: 0)"), 227 | mcpgo.Min(0), 228 | ), 229 | mcpgo.WithNumber( 230 | "from", 231 | mcpgo.Description("Timestamp (in Unix format) from when "+ 232 | "the orders should be fetched"), 233 | mcpgo.Min(0), 234 | ), 235 | mcpgo.WithNumber( 236 | "to", 237 | mcpgo.Description("Timestamp (in Unix format) up till "+ 238 | "when orders are to be fetched"), 239 | mcpgo.Min(0), 240 | ), 241 | mcpgo.WithNumber( 242 | "authorized", 243 | mcpgo.Description("Filter orders based on payment authorization status. "+ 244 | "Values: 0 (orders with unauthorized payments), "+ 245 | "1 (orders with authorized payments)"), 246 | mcpgo.Min(0), 247 | mcpgo.Max(1), 248 | ), 249 | mcpgo.WithString( 250 | "receipt", 251 | mcpgo.Description("Filter orders that contain the "+ 252 | "provided value for receipt"), 253 | ), 254 | mcpgo.WithArray( 255 | "expand", 256 | mcpgo.Description("Used to retrieve additional information. "+ 257 | "Supported values: payments, payments.card, transfers, virtual_account"), 258 | ), 259 | } 260 | 261 | handler := func( 262 | ctx context.Context, 263 | r mcpgo.CallToolRequest, 264 | ) (*mcpgo.ToolResult, error) { 265 | // Get client from context or use default 266 | client, err := getClientFromContextOrDefault(ctx, client) 267 | if err != nil { 268 | return mcpgo.NewToolResultError(err.Error()), nil 269 | } 270 | 271 | queryParams := make(map[string]interface{}) 272 | 273 | validator := NewValidator(&r). 274 | ValidateAndAddPagination(queryParams). 275 | ValidateAndAddOptionalInt(queryParams, "from"). 276 | ValidateAndAddOptionalInt(queryParams, "to"). 277 | ValidateAndAddOptionalInt(queryParams, "authorized"). 278 | ValidateAndAddOptionalString(queryParams, "receipt"). 279 | ValidateAndAddExpand(queryParams) 280 | 281 | if result, err := validator.HandleErrorsIfAny(); result != nil { 282 | return result, err 283 | } 284 | 285 | orders, err := client.Order.All(queryParams, nil) 286 | if err != nil { 287 | return mcpgo.NewToolResultError( 288 | fmt.Sprintf("fetching orders failed: %s", err.Error()), 289 | ), nil 290 | } 291 | 292 | return mcpgo.NewToolResultJSON(orders) 293 | } 294 | 295 | return mcpgo.NewTool( 296 | "fetch_all_orders", 297 | "Fetch all orders with optional filtering and pagination", 298 | parameters, 299 | handler, 300 | ) 301 | } 302 | 303 | // FetchOrderPayments returns a tool to fetch all payments for a specific order 304 | func FetchOrderPayments( 305 | obs *observability.Observability, 306 | client *rzpsdk.Client, 307 | ) mcpgo.Tool { 308 | parameters := []mcpgo.ToolParameter{ 309 | mcpgo.WithString( 310 | "order_id", 311 | mcpgo.Description( 312 | "Unique identifier of the order for which payments should"+ 313 | " be retrieved. Order id should start with `order_`"), 314 | mcpgo.Required(), 315 | ), 316 | } 317 | 318 | handler := func( 319 | ctx context.Context, 320 | r mcpgo.CallToolRequest, 321 | ) (*mcpgo.ToolResult, error) { 322 | // Get client from context or use default 323 | client, err := getClientFromContextOrDefault(ctx, client) 324 | if err != nil { 325 | return mcpgo.NewToolResultError(err.Error()), nil 326 | } 327 | 328 | orderPaymentsReq := make(map[string]interface{}) 329 | 330 | validator := NewValidator(&r). 331 | ValidateAndAddRequiredString(orderPaymentsReq, "order_id") 332 | 333 | if result, err := validator.HandleErrorsIfAny(); result != nil { 334 | return result, err 335 | } 336 | 337 | // Fetch payments for the order using Razorpay SDK 338 | // Note: Using the Order.Payments method from SDK 339 | orderID := orderPaymentsReq["order_id"].(string) 340 | payments, err := client.Order.Payments(orderID, nil, nil) 341 | if err != nil { 342 | return mcpgo.NewToolResultError( 343 | fmt.Sprintf( 344 | "fetching payments for order failed: %s", 345 | err.Error(), 346 | ), 347 | ), nil 348 | } 349 | 350 | // Return the result as JSON 351 | return mcpgo.NewToolResultJSON(payments) 352 | } 353 | 354 | return mcpgo.NewTool( 355 | "fetch_order_payments", 356 | "Fetch all payments made for a specific order in Razorpay", 357 | parameters, 358 | handler, 359 | ) 360 | } 361 | 362 | // UpdateOrder returns a tool to update an order 363 | // only the order's notes can be updated 364 | func UpdateOrder( 365 | obs *observability.Observability, 366 | client *rzpsdk.Client, 367 | ) mcpgo.Tool { 368 | parameters := []mcpgo.ToolParameter{ 369 | mcpgo.WithString( 370 | "order_id", 371 | mcpgo.Description("Unique identifier of the order which "+ 372 | "needs to be updated. ID should have an order_ prefix."), 373 | mcpgo.Required(), 374 | ), 375 | mcpgo.WithObject( 376 | "notes", 377 | mcpgo.Description("Key-value pairs used to store additional "+ 378 | "information about the order. A maximum of 15 key-value pairs "+ 379 | "can be included, with each value not exceeding 256 characters."), 380 | mcpgo.Required(), 381 | ), 382 | } 383 | 384 | handler := func( 385 | ctx context.Context, 386 | r mcpgo.CallToolRequest, 387 | ) (*mcpgo.ToolResult, error) { 388 | orderUpdateReq := make(map[string]interface{}) 389 | data := make(map[string]interface{}) 390 | 391 | client, err := getClientFromContextOrDefault(ctx, client) 392 | if err != nil { 393 | return mcpgo.NewToolResultError(err.Error()), nil 394 | } 395 | 396 | validator := NewValidator(&r). 397 | ValidateAndAddRequiredString(orderUpdateReq, "order_id"). 398 | ValidateAndAddRequiredMap(orderUpdateReq, "notes") 399 | 400 | if result, err := validator.HandleErrorsIfAny(); result != nil { 401 | return result, err 402 | } 403 | 404 | data["notes"] = orderUpdateReq["notes"] 405 | orderID := orderUpdateReq["order_id"].(string) 406 | 407 | order, err := client.Order.Update(orderID, data, nil) 408 | if err != nil { 409 | return mcpgo.NewToolResultError( 410 | fmt.Sprintf("updating order failed: %s", err.Error())), nil 411 | } 412 | 413 | return mcpgo.NewToolResultJSON(order) 414 | } 415 | 416 | return mcpgo.NewTool( 417 | "update_order", 418 | "Use this tool to update the notes for a specific order. "+ 419 | "Only the notes field can be modified.", 420 | parameters, 421 | handler, 422 | ) 423 | } 424 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/tools_params.go: -------------------------------------------------------------------------------- ```go 1 | package razorpay 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "strings" 7 | "time" 8 | 9 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" 10 | ) 11 | 12 | // Validator provides a fluent interface for validating parameters 13 | // and collecting errors 14 | type Validator struct { 15 | request *mcpgo.CallToolRequest 16 | errors []error 17 | } 18 | 19 | // NewValidator creates a new validator for the given request 20 | func NewValidator(r *mcpgo.CallToolRequest) *Validator { 21 | return &Validator{ 22 | request: r, 23 | errors: []error{}, 24 | } 25 | } 26 | 27 | // addError adds a non-nil error to the collection 28 | func (v *Validator) addError(err error) *Validator { 29 | if err != nil { 30 | v.errors = append(v.errors, err) 31 | } 32 | return v 33 | } 34 | 35 | // HasErrors returns true if there are any validation errors 36 | func (v *Validator) HasErrors() bool { 37 | return len(v.errors) > 0 38 | } 39 | 40 | // HandleErrorsIfAny formats all errors and returns an appropriate tool result 41 | func (v *Validator) HandleErrorsIfAny() (*mcpgo.ToolResult, error) { 42 | if v.HasErrors() { 43 | messages := make([]string, 0, len(v.errors)) 44 | for _, err := range v.errors { 45 | messages = append(messages, err.Error()) 46 | } 47 | errorMsg := "Validation errors:\n- " + strings.Join(messages, "\n- ") 48 | return mcpgo.NewToolResultError(errorMsg), nil 49 | } 50 | return nil, nil 51 | } 52 | 53 | // extractValueGeneric is a standalone generic function to extract a parameter 54 | // of type T 55 | func extractValueGeneric[T any]( 56 | request *mcpgo.CallToolRequest, 57 | name string, 58 | required bool, 59 | ) (*T, error) { 60 | // Type assert Arguments from any to map[string]interface{} 61 | args, ok := request.Arguments.(map[string]interface{}) 62 | if !ok { 63 | return nil, errors.New("invalid arguments type") 64 | } 65 | 66 | val, ok := args[name] 67 | if !ok || val == nil { 68 | if required { 69 | return nil, errors.New("missing required parameter: " + name) 70 | } 71 | return nil, nil // Not an error for optional params 72 | } 73 | 74 | var result T 75 | data, err := json.Marshal(val) 76 | if err != nil { 77 | return nil, errors.New("invalid parameter type: " + name) 78 | } 79 | 80 | err = json.Unmarshal(data, &result) 81 | if err != nil { 82 | return nil, errors.New("invalid parameter type: " + name) 83 | } 84 | 85 | return &result, nil 86 | } 87 | 88 | // Generic validation functions 89 | 90 | // validateAndAddRequired validates and adds a required parameter of any type 91 | func validateAndAddRequired[T any]( 92 | v *Validator, 93 | params map[string]interface{}, 94 | name string, 95 | ) *Validator { 96 | value, err := extractValueGeneric[T](v.request, name, true) 97 | if err != nil { 98 | return v.addError(err) 99 | } 100 | 101 | if value == nil { 102 | return v 103 | } 104 | 105 | params[name] = *value 106 | return v 107 | } 108 | 109 | // validateAndAddOptional validates and adds an optional parameter of any type 110 | // if not empty 111 | func validateAndAddOptional[T any]( 112 | v *Validator, 113 | params map[string]interface{}, 114 | name string, 115 | ) *Validator { 116 | value, err := extractValueGeneric[T](v.request, name, false) 117 | if err != nil { 118 | return v.addError(err) 119 | } 120 | 121 | if value == nil { 122 | return v 123 | } 124 | 125 | params[name] = *value 126 | 127 | return v 128 | } 129 | 130 | // validateAndAddToPath is a generic helper to extract a value and write it into 131 | // `target[targetKey]` if non-empty 132 | func validateAndAddToPath[T any]( 133 | v *Validator, 134 | target map[string]interface{}, 135 | paramName string, 136 | targetKey string, 137 | ) *Validator { 138 | value, err := extractValueGeneric[T](v.request, paramName, false) 139 | if err != nil { 140 | return v.addError(err) 141 | } 142 | 143 | if value == nil { 144 | return v 145 | } 146 | 147 | target[targetKey] = *value 148 | 149 | return v 150 | } 151 | 152 | // ValidateAndAddOptionalStringToPath validates an optional string 153 | // and writes it into target[targetKey] 154 | func (v *Validator) ValidateAndAddOptionalStringToPath( 155 | target map[string]interface{}, 156 | paramName, targetKey string, 157 | ) *Validator { 158 | return validateAndAddToPath[string](v, target, paramName, targetKey) // nolint:lll 159 | } 160 | 161 | // ValidateAndAddOptionalBoolToPath validates an optional bool 162 | // and writes it into target[targetKey] 163 | // only if it was explicitly provided in the request 164 | func (v *Validator) ValidateAndAddOptionalBoolToPath( 165 | target map[string]interface{}, 166 | paramName, targetKey string, 167 | ) *Validator { 168 | // Now validate and add the parameter 169 | value, err := extractValueGeneric[bool](v.request, paramName, false) 170 | if err != nil { 171 | return v.addError(err) 172 | } 173 | 174 | if value == nil { 175 | return v 176 | } 177 | 178 | target[targetKey] = *value 179 | return v 180 | } 181 | 182 | // ValidateAndAddOptionalIntToPath validates an optional integer 183 | // and writes it into target[targetKey] 184 | func (v *Validator) ValidateAndAddOptionalIntToPath( 185 | target map[string]interface{}, 186 | paramName, targetKey string, 187 | ) *Validator { 188 | return validateAndAddToPath[int64](v, target, paramName, targetKey) 189 | } 190 | 191 | // Type-specific validator methods 192 | 193 | // ValidateAndAddRequiredString validates and adds a required string parameter 194 | func (v *Validator) ValidateAndAddRequiredString( 195 | params map[string]interface{}, 196 | name string, 197 | ) *Validator { 198 | return validateAndAddRequired[string](v, params, name) 199 | } 200 | 201 | // ValidateAndAddOptionalString validates and adds an optional string parameter 202 | func (v *Validator) ValidateAndAddOptionalString( 203 | params map[string]interface{}, 204 | name string, 205 | ) *Validator { 206 | return validateAndAddOptional[string](v, params, name) 207 | } 208 | 209 | // ValidateAndAddRequiredMap validates and adds a required map parameter 210 | func (v *Validator) ValidateAndAddRequiredMap( 211 | params map[string]interface{}, 212 | name string, 213 | ) *Validator { 214 | return validateAndAddRequired[map[string]interface{}](v, params, name) 215 | } 216 | 217 | // ValidateAndAddOptionalMap validates and adds an optional map parameter 218 | func (v *Validator) ValidateAndAddOptionalMap( 219 | params map[string]interface{}, 220 | name string, 221 | ) *Validator { 222 | return validateAndAddOptional[map[string]interface{}](v, params, name) 223 | } 224 | 225 | // ValidateAndAddRequiredArray validates and adds a required array parameter 226 | func (v *Validator) ValidateAndAddRequiredArray( 227 | params map[string]interface{}, 228 | name string, 229 | ) *Validator { 230 | return validateAndAddRequired[[]interface{}](v, params, name) 231 | } 232 | 233 | // ValidateAndAddOptionalArray validates and adds an optional array parameter 234 | func (v *Validator) ValidateAndAddOptionalArray( 235 | params map[string]interface{}, 236 | name string, 237 | ) *Validator { 238 | return validateAndAddOptional[[]interface{}](v, params, name) 239 | } 240 | 241 | // ValidateAndAddPagination validates and adds pagination parameters 242 | // (count and skip) 243 | func (v *Validator) ValidateAndAddPagination( 244 | params map[string]interface{}, 245 | ) *Validator { 246 | return v.ValidateAndAddOptionalInt(params, "count"). 247 | ValidateAndAddOptionalInt(params, "skip") 248 | } 249 | 250 | // ValidateAndAddExpand validates and adds expand parameters 251 | func (v *Validator) ValidateAndAddExpand( 252 | params map[string]interface{}, 253 | ) *Validator { 254 | expand, err := extractValueGeneric[[]string](v.request, "expand", false) 255 | if err != nil { 256 | return v.addError(err) 257 | } 258 | 259 | if expand == nil { 260 | return v 261 | } 262 | 263 | if len(*expand) > 0 { 264 | for _, val := range *expand { 265 | params["expand[]"] = val 266 | } 267 | } 268 | return v 269 | } 270 | 271 | // ValidateAndAddRequiredInt validates and adds a required integer parameter 272 | func (v *Validator) ValidateAndAddRequiredInt( 273 | params map[string]interface{}, 274 | name string, 275 | ) *Validator { 276 | return validateAndAddRequired[int64](v, params, name) 277 | } 278 | 279 | // ValidateAndAddOptionalInt validates and adds an optional integer parameter 280 | func (v *Validator) ValidateAndAddOptionalInt( 281 | params map[string]interface{}, 282 | name string, 283 | ) *Validator { 284 | return validateAndAddOptional[int64](v, params, name) 285 | } 286 | 287 | // ValidateAndAddRequiredFloat validates and adds a required float parameter 288 | func (v *Validator) ValidateAndAddRequiredFloat( 289 | params map[string]interface{}, 290 | name string, 291 | ) *Validator { 292 | return validateAndAddRequired[float64](v, params, name) 293 | } 294 | 295 | // ValidateAndAddOptionalFloat validates and adds an optional float parameter 296 | func (v *Validator) ValidateAndAddOptionalFloat( 297 | params map[string]interface{}, 298 | name string, 299 | ) *Validator { 300 | return validateAndAddOptional[float64](v, params, name) 301 | } 302 | 303 | // ValidateAndAddRequiredBool validates and adds a required boolean parameter 304 | func (v *Validator) ValidateAndAddRequiredBool( 305 | params map[string]interface{}, 306 | name string, 307 | ) *Validator { 308 | return validateAndAddRequired[bool](v, params, name) 309 | } 310 | 311 | // ValidateAndAddOptionalBool validates and adds an optional boolean parameter 312 | // Note: This adds the boolean value only 313 | // if it was explicitly provided in the request 314 | func (v *Validator) ValidateAndAddOptionalBool( 315 | params map[string]interface{}, 316 | name string, 317 | ) *Validator { 318 | // Now validate and add the parameter 319 | value, err := extractValueGeneric[bool](v.request, name, false) 320 | if err != nil { 321 | return v.addError(err) 322 | } 323 | 324 | if value == nil { 325 | return v 326 | } 327 | 328 | params[name] = *value 329 | return v 330 | } 331 | 332 | // validateTokenMaxAmount validates the max_amount field in token. 333 | // max_amount is required and must be a positive number representing 334 | // the maximum amount that can be debited from the customer's account. 335 | func (v *Validator) validateTokenMaxAmount( 336 | token map[string]interface{}) *Validator { 337 | if maxAmount, exists := token["max_amount"]; exists { 338 | switch amt := maxAmount.(type) { 339 | case float64: 340 | if amt <= 0 { 341 | return v.addError(errors.New("token.max_amount must be greater than 0")) 342 | } 343 | case int: 344 | if amt <= 0 { 345 | return v.addError(errors.New("token.max_amount must be greater than 0")) 346 | } 347 | token["max_amount"] = float64(amt) // Convert int to float64 348 | default: 349 | return v.addError(errors.New("token.max_amount must be a number")) 350 | } 351 | } else { 352 | return v.addError(errors.New("token.max_amount is required")) 353 | } 354 | return v 355 | } 356 | 357 | // validateTokenExpireAt validates the expire_at field in token. 358 | // expire_at is optional and defaults to today + 60 days if not provided. 359 | // If provided, it must be a positive Unix timestamp indicating when the 360 | // mandate/token should expire. 361 | func (v *Validator) validateTokenExpireAt( 362 | token map[string]interface{}) *Validator { 363 | if expireAt, exists := token["expire_at"]; exists { 364 | switch exp := expireAt.(type) { 365 | case float64: 366 | if exp <= 0 { 367 | return v.addError(errors.New("token.expire_at must be greater than 0")) 368 | } 369 | case int: 370 | if exp <= 0 { 371 | return v.addError(errors.New("token.expire_at must be greater than 0")) 372 | } 373 | token["expire_at"] = float64(exp) // Convert int to float64 374 | default: 375 | return v.addError(errors.New("token.expire_at must be a number")) 376 | } 377 | } else { 378 | // Set default value to today + 60 days 379 | defaultExpireAt := time.Now().AddDate(0, 0, 60).Unix() 380 | token["expire_at"] = float64(defaultExpireAt) 381 | } 382 | return v 383 | } 384 | 385 | // validateTokenFrequency validates the frequency field in token. 386 | // frequency is required and must be one of the allowed values: 387 | // "as_presented", "monthly", "one_time", "yearly", "weekly", "daily". 388 | func (v *Validator) validateTokenFrequency( 389 | token map[string]interface{}) *Validator { 390 | if frequency, exists := token["frequency"]; exists { 391 | if freqStr, ok := frequency.(string); ok { 392 | validFrequencies := []string{ 393 | "as_presented", "monthly", "one_time", "yearly", "weekly", "daily"} 394 | for _, validFreq := range validFrequencies { 395 | if freqStr == validFreq { 396 | return v 397 | } 398 | } 399 | return v.addError(errors.New( 400 | "token.frequency must be one of: as_presented, " + 401 | "monthly, one_time, yearly, weekly, daily")) 402 | } 403 | return v.addError(errors.New("token.frequency must be a string")) 404 | } 405 | return v.addError(errors.New("token.frequency is required")) 406 | } 407 | 408 | // validateTokenType validates the type field in token. 409 | // type is required and must be "single_block_multiple_debit" for SBMD mandates. 410 | func (v *Validator) validateTokenType(token map[string]interface{}) *Validator { 411 | if tokenType, exists := token["type"]; exists { 412 | if typeStr, ok := tokenType.(string); ok { 413 | validTypes := []string{"single_block_multiple_debit"} 414 | for _, validType := range validTypes { 415 | if typeStr == validType { 416 | return v 417 | } 418 | } 419 | return v.addError(errors.New( 420 | "token.type must be one of: single_block_multiple_debit")) 421 | } 422 | return v.addError(errors.New("token.type must be a string")) 423 | } 424 | return v.addError(errors.New("token.type is required")) 425 | } 426 | 427 | // ValidateAndAddToken validates and adds a token object with proper structure. 428 | // The token object is used for mandate orders and must contain: 429 | // - max_amount: positive number (maximum debit amount) 430 | // - expire_at: optional Unix timestamp (mandate expiry, 431 | // defaults to today + 60 days) 432 | // - frequency: string (debit frequency: as_presented, monthly, one_time, 433 | // yearly, weekly, daily) 434 | // - type: string (mandate type: single_block_multiple_debit) 435 | func (v *Validator) ValidateAndAddToken( 436 | params map[string]interface{}, name string) *Validator { 437 | value, err := extractValueGeneric[map[string]interface{}]( 438 | v.request, name, false) 439 | if err != nil { 440 | return v.addError(err) 441 | } 442 | 443 | if value == nil { 444 | return v 445 | } 446 | 447 | token := *value 448 | 449 | // Validate all token fields 450 | v.validateTokenMaxAmount(token). 451 | validateTokenExpireAt(token). 452 | validateTokenFrequency(token). 453 | validateTokenType(token) 454 | 455 | if v.HasErrors() { 456 | return v 457 | } 458 | 459 | params[name] = token 460 | return v 461 | } 462 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/qr_codes.go: -------------------------------------------------------------------------------- ```go 1 | package razorpay 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | rzpsdk "github.com/razorpay/razorpay-go" 8 | 9 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" 10 | "github.com/razorpay/razorpay-mcp-server/pkg/observability" 11 | ) 12 | 13 | // CreateQRCode returns a tool that creates QR codes in Razorpay 14 | func CreateQRCode( 15 | obs *observability.Observability, 16 | client *rzpsdk.Client, 17 | ) mcpgo.Tool { 18 | parameters := []mcpgo.ToolParameter{ 19 | mcpgo.WithString( 20 | "type", 21 | mcpgo.Description( 22 | "The type of the QR Code. Currently only supports 'upi_qr'", 23 | ), 24 | mcpgo.Required(), 25 | mcpgo.Pattern("^upi_qr$"), 26 | ), 27 | mcpgo.WithString( 28 | "name", 29 | mcpgo.Description( 30 | "Label to identify the QR Code (e.g., 'Store Front Display')", 31 | ), 32 | ), 33 | mcpgo.WithString( 34 | "usage", 35 | mcpgo.Description( 36 | "Whether QR should accept single or multiple payments. "+ 37 | "Possible values: 'single_use', 'multiple_use'", 38 | ), 39 | mcpgo.Required(), 40 | mcpgo.Enum("single_use", "multiple_use"), 41 | ), 42 | mcpgo.WithBoolean( 43 | "fixed_amount", 44 | mcpgo.Description( 45 | "Whether QR should accept only specific amount (true) or any "+ 46 | "amount (false)", 47 | ), 48 | mcpgo.DefaultValue(false), 49 | ), 50 | mcpgo.WithNumber( 51 | "payment_amount", 52 | mcpgo.Description( 53 | "The specific amount allowed for transaction in smallest "+ 54 | "currency unit", 55 | ), 56 | mcpgo.Min(1), 57 | ), 58 | mcpgo.WithString( 59 | "description", 60 | mcpgo.Description("A brief description about the QR Code"), 61 | ), 62 | mcpgo.WithString( 63 | "customer_id", 64 | mcpgo.Description( 65 | "The unique identifier of the customer to link with the QR Code", 66 | ), 67 | ), 68 | mcpgo.WithNumber( 69 | "close_by", 70 | mcpgo.Description( 71 | "Unix timestamp at which QR Code should be automatically "+ 72 | "closed (min 2 mins after current time)", 73 | ), 74 | ), 75 | mcpgo.WithObject( 76 | "notes", 77 | mcpgo.Description( 78 | "Key-value pairs for additional information "+ 79 | "(max 15 pairs, 256 chars each)", 80 | ), 81 | mcpgo.MaxProperties(15), 82 | ), 83 | } 84 | 85 | handler := func( 86 | ctx context.Context, 87 | r mcpgo.CallToolRequest, 88 | ) (*mcpgo.ToolResult, error) { 89 | client, err := getClientFromContextOrDefault(ctx, client) 90 | if err != nil { 91 | return mcpgo.NewToolResultError(err.Error()), nil 92 | } 93 | 94 | qrData := make(map[string]interface{}) 95 | 96 | validator := NewValidator(&r). 97 | ValidateAndAddRequiredString(qrData, "type"). 98 | ValidateAndAddRequiredString(qrData, "usage"). 99 | ValidateAndAddOptionalString(qrData, "name"). 100 | ValidateAndAddOptionalBool(qrData, "fixed_amount"). 101 | ValidateAndAddOptionalFloat(qrData, "payment_amount"). 102 | ValidateAndAddOptionalString(qrData, "description"). 103 | ValidateAndAddOptionalString(qrData, "customer_id"). 104 | ValidateAndAddOptionalFloat(qrData, "close_by"). 105 | ValidateAndAddOptionalMap(qrData, "notes") 106 | 107 | if result, err := validator.HandleErrorsIfAny(); result != nil { 108 | return result, err 109 | } 110 | 111 | // Check if fixed_amount is true, then payment_amount is required 112 | if fixedAmount, exists := qrData["fixed_amount"]; exists && 113 | fixedAmount.(bool) { 114 | if _, exists := qrData["payment_amount"]; !exists { 115 | return mcpgo.NewToolResultError( 116 | "payment_amount is required when fixed_amount is true"), nil 117 | } 118 | } 119 | 120 | // Create QR code using Razorpay SDK 121 | qrCode, err := client.QrCode.Create(qrData, nil) 122 | if err != nil { 123 | return mcpgo.NewToolResultError( 124 | fmt.Sprintf("creating QR code failed: %s", err.Error())), nil 125 | } 126 | 127 | return mcpgo.NewToolResultJSON(qrCode) 128 | } 129 | 130 | return mcpgo.NewTool( 131 | "create_qr_code", 132 | "Create a new QR code in Razorpay that can be used to accept UPI payments", 133 | parameters, 134 | handler, 135 | ) 136 | } 137 | 138 | // FetchQRCode returns a tool that fetches a specific QR code by ID 139 | func FetchQRCode( 140 | obs *observability.Observability, 141 | client *rzpsdk.Client, 142 | ) mcpgo.Tool { 143 | parameters := []mcpgo.ToolParameter{ 144 | mcpgo.WithString( 145 | "qr_code_id", 146 | mcpgo.Description( 147 | "Unique identifier of the QR Code to be retrieved"+ 148 | "The QR code id should start with 'qr_'", 149 | ), 150 | mcpgo.Required(), 151 | ), 152 | } 153 | 154 | handler := func( 155 | ctx context.Context, 156 | r mcpgo.CallToolRequest, 157 | ) (*mcpgo.ToolResult, error) { 158 | client, err := getClientFromContextOrDefault(ctx, client) 159 | if err != nil { 160 | return mcpgo.NewToolResultError(err.Error()), nil 161 | } 162 | 163 | params := make(map[string]interface{}) 164 | validator := NewValidator(&r). 165 | ValidateAndAddRequiredString(params, "qr_code_id") 166 | if result, err := validator.HandleErrorsIfAny(); result != nil { 167 | return result, err 168 | } 169 | qrCodeID := params["qr_code_id"].(string) 170 | 171 | // Fetch QR code by ID using Razorpay SDK 172 | qrCode, err := client.QrCode.Fetch(qrCodeID, nil, nil) 173 | if err != nil { 174 | return mcpgo.NewToolResultError( 175 | fmt.Sprintf("fetching QR code failed: %s", err.Error())), nil 176 | } 177 | 178 | return mcpgo.NewToolResultJSON(qrCode) 179 | } 180 | 181 | return mcpgo.NewTool( 182 | "fetch_qr_code", 183 | "Fetch a QR code's details using it's ID", 184 | parameters, 185 | handler, 186 | ) 187 | } 188 | 189 | // FetchAllQRCodes returns a tool that fetches all QR codes 190 | // with pagination support 191 | func FetchAllQRCodes( 192 | obs *observability.Observability, 193 | client *rzpsdk.Client, 194 | ) mcpgo.Tool { 195 | parameters := []mcpgo.ToolParameter{ 196 | mcpgo.WithNumber( 197 | "from", 198 | mcpgo.Description( 199 | "Unix timestamp, in seconds, from when QR Codes are to be retrieved", 200 | ), 201 | mcpgo.Min(0), 202 | ), 203 | mcpgo.WithNumber( 204 | "to", 205 | mcpgo.Description( 206 | "Unix timestamp, in seconds, till when QR Codes are to be retrieved", 207 | ), 208 | mcpgo.Min(0), 209 | ), 210 | mcpgo.WithNumber( 211 | "count", 212 | mcpgo.Description( 213 | "Number of QR Codes to be retrieved (default: 10, max: 100)", 214 | ), 215 | mcpgo.Min(1), 216 | mcpgo.Max(100), 217 | ), 218 | mcpgo.WithNumber( 219 | "skip", 220 | mcpgo.Description( 221 | "Number of QR Codes to be skipped (default: 0)", 222 | ), 223 | mcpgo.Min(0), 224 | ), 225 | } 226 | 227 | handler := func( 228 | ctx context.Context, 229 | r mcpgo.CallToolRequest, 230 | ) (*mcpgo.ToolResult, error) { 231 | client, err := getClientFromContextOrDefault(ctx, client) 232 | if err != nil { 233 | return mcpgo.NewToolResultError(err.Error()), nil 234 | } 235 | 236 | fetchQROptions := make(map[string]interface{}) 237 | 238 | validator := NewValidator(&r). 239 | ValidateAndAddOptionalInt(fetchQROptions, "from"). 240 | ValidateAndAddOptionalInt(fetchQROptions, "to"). 241 | ValidateAndAddPagination(fetchQROptions) 242 | 243 | if result, err := validator.HandleErrorsIfAny(); result != nil { 244 | return result, err 245 | } 246 | 247 | // Fetch QR codes using Razorpay SDK 248 | qrCodes, err := client.QrCode.All(fetchQROptions, nil) 249 | if err != nil { 250 | return mcpgo.NewToolResultError( 251 | fmt.Sprintf("fetching QR codes failed: %s", err.Error())), nil 252 | } 253 | 254 | return mcpgo.NewToolResultJSON(qrCodes) 255 | } 256 | 257 | return mcpgo.NewTool( 258 | "fetch_all_qr_codes", 259 | "Fetch all QR codes with optional filtering and pagination", 260 | parameters, 261 | handler, 262 | ) 263 | } 264 | 265 | // FetchQRCodesByCustomerID returns a tool that fetches QR codes 266 | // for a specific customer ID 267 | func FetchQRCodesByCustomerID( 268 | obs *observability.Observability, 269 | client *rzpsdk.Client, 270 | ) mcpgo.Tool { 271 | parameters := []mcpgo.ToolParameter{ 272 | mcpgo.WithString( 273 | "customer_id", 274 | mcpgo.Description( 275 | "The unique identifier of the customer", 276 | ), 277 | mcpgo.Required(), 278 | ), 279 | } 280 | 281 | handler := func( 282 | ctx context.Context, 283 | r mcpgo.CallToolRequest, 284 | ) (*mcpgo.ToolResult, error) { 285 | client, err := getClientFromContextOrDefault(ctx, client) 286 | if err != nil { 287 | return mcpgo.NewToolResultError(err.Error()), nil 288 | } 289 | 290 | fetchQROptions := make(map[string]interface{}) 291 | 292 | validator := NewValidator(&r). 293 | ValidateAndAddRequiredString(fetchQROptions, "customer_id") 294 | 295 | if result, err := validator.HandleErrorsIfAny(); result != nil { 296 | return result, err 297 | } 298 | 299 | // Fetch QR codes by customer ID using Razorpay SDK 300 | qrCodes, err := client.QrCode.All(fetchQROptions, nil) 301 | if err != nil { 302 | return mcpgo.NewToolResultError( 303 | fmt.Sprintf("fetching QR codes failed: %s", err.Error())), nil 304 | } 305 | 306 | return mcpgo.NewToolResultJSON(qrCodes) 307 | } 308 | 309 | return mcpgo.NewTool( 310 | "fetch_qr_codes_by_customer_id", 311 | "Fetch all QR codes for a specific customer", 312 | parameters, 313 | handler, 314 | ) 315 | } 316 | 317 | // FetchQRCodesByPaymentID returns a tool that fetches QR codes 318 | // for a specific payment ID 319 | func FetchQRCodesByPaymentID( 320 | obs *observability.Observability, 321 | client *rzpsdk.Client, 322 | ) mcpgo.Tool { 323 | parameters := []mcpgo.ToolParameter{ 324 | mcpgo.WithString( 325 | "payment_id", 326 | mcpgo.Description( 327 | "The unique identifier of the payment"+ 328 | "The payment id always should start with 'pay_'", 329 | ), 330 | mcpgo.Required(), 331 | ), 332 | } 333 | 334 | handler := func( 335 | ctx context.Context, 336 | r mcpgo.CallToolRequest, 337 | ) (*mcpgo.ToolResult, error) { 338 | client, err := getClientFromContextOrDefault(ctx, client) 339 | if err != nil { 340 | return mcpgo.NewToolResultError(err.Error()), nil 341 | } 342 | 343 | fetchQROptions := make(map[string]interface{}) 344 | 345 | validator := NewValidator(&r). 346 | ValidateAndAddRequiredString(fetchQROptions, "payment_id") 347 | 348 | if result, err := validator.HandleErrorsIfAny(); result != nil { 349 | return result, err 350 | } 351 | 352 | // Fetch QR codes by payment ID using Razorpay SDK 353 | qrCodes, err := client.QrCode.All(fetchQROptions, nil) 354 | if err != nil { 355 | return mcpgo.NewToolResultError( 356 | fmt.Sprintf("fetching QR codes failed: %s", err.Error())), nil 357 | } 358 | 359 | return mcpgo.NewToolResultJSON(qrCodes) 360 | } 361 | 362 | return mcpgo.NewTool( 363 | "fetch_qr_codes_by_payment_id", 364 | "Fetch all QR codes for a specific payment", 365 | parameters, 366 | handler, 367 | ) 368 | } 369 | 370 | // FetchPaymentsForQRCode returns a tool that fetches payments made on a QR code 371 | func FetchPaymentsForQRCode( 372 | obs *observability.Observability, 373 | client *rzpsdk.Client, 374 | ) mcpgo.Tool { 375 | parameters := []mcpgo.ToolParameter{ 376 | mcpgo.WithString( 377 | "qr_code_id", 378 | mcpgo.Description( 379 | "The unique identifier of the QR Code to fetch payments for"+ 380 | "The QR code id should start with 'qr_'", 381 | ), 382 | mcpgo.Required(), 383 | ), 384 | mcpgo.WithNumber( 385 | "from", 386 | mcpgo.Description( 387 | "Unix timestamp, in seconds, from when payments are to be retrieved", 388 | ), 389 | mcpgo.Min(0), 390 | ), 391 | mcpgo.WithNumber( 392 | "to", 393 | mcpgo.Description( 394 | "Unix timestamp, in seconds, till when payments are to be fetched", 395 | ), 396 | mcpgo.Min(0), 397 | ), 398 | mcpgo.WithNumber( 399 | "count", 400 | mcpgo.Description( 401 | "Number of payments to be fetched (default: 10, max: 100)", 402 | ), 403 | mcpgo.Min(1), 404 | mcpgo.Max(100), 405 | ), 406 | mcpgo.WithNumber( 407 | "skip", 408 | mcpgo.Description( 409 | "Number of records to be skipped while fetching the payments", 410 | ), 411 | mcpgo.Min(0), 412 | ), 413 | } 414 | 415 | handler := func( 416 | ctx context.Context, 417 | r mcpgo.CallToolRequest, 418 | ) (*mcpgo.ToolResult, error) { 419 | client, err := getClientFromContextOrDefault(ctx, client) 420 | if err != nil { 421 | return mcpgo.NewToolResultError(err.Error()), nil 422 | } 423 | 424 | params := make(map[string]interface{}) 425 | fetchQROptions := make(map[string]interface{}) 426 | 427 | validator := NewValidator(&r). 428 | ValidateAndAddRequiredString(params, "qr_code_id"). 429 | ValidateAndAddOptionalInt(fetchQROptions, "from"). 430 | ValidateAndAddOptionalInt(fetchQROptions, "to"). 431 | ValidateAndAddOptionalInt(fetchQROptions, "count"). 432 | ValidateAndAddOptionalInt(fetchQROptions, "skip") 433 | 434 | if result, err := validator.HandleErrorsIfAny(); result != nil { 435 | return result, err 436 | } 437 | 438 | qrCodeID := params["qr_code_id"].(string) 439 | 440 | // Fetch payments for QR code using Razorpay SDK 441 | payments, err := client.QrCode.FetchPayments(qrCodeID, fetchQROptions, nil) 442 | if err != nil { 443 | return mcpgo.NewToolResultError( 444 | fmt.Sprintf("fetching payments for QR code failed: %s", err.Error())), nil 445 | } 446 | 447 | return mcpgo.NewToolResultJSON(payments) 448 | } 449 | 450 | return mcpgo.NewTool( 451 | "fetch_payments_for_qr_code", 452 | "Fetch all payments made on a QR code", 453 | parameters, 454 | handler, 455 | ) 456 | } 457 | 458 | // CloseQRCode returns a tool that closes a specific QR code 459 | func CloseQRCode( 460 | obs *observability.Observability, 461 | client *rzpsdk.Client, 462 | ) mcpgo.Tool { 463 | parameters := []mcpgo.ToolParameter{ 464 | mcpgo.WithString( 465 | "qr_code_id", 466 | mcpgo.Description( 467 | "Unique identifier of the QR Code to be closed"+ 468 | "The QR code id should start with 'qr_'", 469 | ), 470 | mcpgo.Required(), 471 | ), 472 | } 473 | 474 | handler := func( 475 | ctx context.Context, 476 | r mcpgo.CallToolRequest, 477 | ) (*mcpgo.ToolResult, error) { 478 | client, err := getClientFromContextOrDefault(ctx, client) 479 | if err != nil { 480 | return mcpgo.NewToolResultError(err.Error()), nil 481 | } 482 | 483 | params := make(map[string]interface{}) 484 | validator := NewValidator(&r). 485 | ValidateAndAddRequiredString(params, "qr_code_id") 486 | if result, err := validator.HandleErrorsIfAny(); result != nil { 487 | return result, err 488 | } 489 | qrCodeID := params["qr_code_id"].(string) 490 | 491 | // Close QR code by ID using Razorpay SDK 492 | qrCode, err := client.QrCode.Close(qrCodeID, nil, nil) 493 | if err != nil { 494 | return mcpgo.NewToolResultError( 495 | fmt.Sprintf("closing QR code failed: %s", err.Error())), nil 496 | } 497 | 498 | return mcpgo.NewToolResultJSON(qrCode) 499 | } 500 | 501 | return mcpgo.NewTool( 502 | "close_qr_code", 503 | "Close a QR Code that's no longer needed", 504 | parameters, 505 | handler, 506 | ) 507 | } 508 | ``` -------------------------------------------------------------------------------- /pkg/mcpgo/tool.go: -------------------------------------------------------------------------------- ```go 1 | package mcpgo 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/mark3labs/mcp-go/mcp" 8 | "github.com/mark3labs/mcp-go/server" 9 | ) 10 | 11 | // ToolHandler handles tool calls 12 | type ToolHandler func( 13 | ctx context.Context, 14 | request CallToolRequest) (*ToolResult, error) 15 | 16 | // CallToolRequest represents a request to call a tool 17 | type CallToolRequest struct { 18 | Name string 19 | Arguments any 20 | } 21 | 22 | // ToolResult represents the result of a tool call 23 | type ToolResult struct { 24 | Text string 25 | IsError bool 26 | Content []interface{} 27 | } 28 | 29 | // Tool represents a tool that can be added to the server 30 | type Tool interface { 31 | // internal method to convert to mcp's ServerTool 32 | toMCPServerTool() server.ServerTool 33 | 34 | // GetHandler internal method for fetching the underlying handler 35 | GetHandler() ToolHandler 36 | } 37 | 38 | // PropertyOption represents a customization option for 39 | // a parameter's schema 40 | type PropertyOption func(schema map[string]interface{}) 41 | 42 | // Min sets the minimum value for a number parameter or 43 | // minimum length for a string 44 | func Min(value float64) PropertyOption { 45 | return func(schema map[string]interface{}) { 46 | propType, ok := schema["type"].(string) 47 | if !ok { 48 | return 49 | } 50 | 51 | switch propType { 52 | case "number", "integer": 53 | schema["minimum"] = value 54 | case "string": 55 | schema["minLength"] = int(value) 56 | case "array": 57 | schema["minItems"] = int(value) 58 | } 59 | } 60 | } 61 | 62 | // Max sets the maximum value for a number parameter or 63 | // maximum length for a string 64 | func Max(value float64) PropertyOption { 65 | return func(schema map[string]interface{}) { 66 | propType, ok := schema["type"].(string) 67 | if !ok { 68 | return 69 | } 70 | 71 | switch propType { 72 | case "number", "integer": 73 | schema["maximum"] = value 74 | case "string": 75 | schema["maxLength"] = int(value) 76 | case "array": 77 | schema["maxItems"] = int(value) 78 | } 79 | } 80 | } 81 | 82 | // Pattern sets a regex pattern for string validation 83 | func Pattern(pattern string) PropertyOption { 84 | return func(schema map[string]interface{}) { 85 | propType, ok := schema["type"].(string) 86 | if !ok || propType != "string" { 87 | return 88 | } 89 | schema["pattern"] = pattern 90 | } 91 | } 92 | 93 | // Enum sets allowed values for a parameter 94 | func Enum(values ...interface{}) PropertyOption { 95 | return func(schema map[string]interface{}) { 96 | schema["enum"] = values 97 | } 98 | } 99 | 100 | // DefaultValue sets a default value for a parameter 101 | func DefaultValue(value interface{}) PropertyOption { 102 | return func(schema map[string]interface{}) { 103 | schema["default"] = value 104 | } 105 | } 106 | 107 | // MaxProperties sets the maximum number of properties for an object 108 | func MaxProperties(max int) PropertyOption { 109 | return func(schema map[string]interface{}) { 110 | propType, ok := schema["type"].(string) 111 | if !ok || propType != "object" { 112 | return 113 | } 114 | schema["maxProperties"] = max 115 | } 116 | } 117 | 118 | // MinProperties sets the minimum number of properties for an object 119 | func MinProperties(min int) PropertyOption { 120 | return func(schema map[string]interface{}) { 121 | propType, ok := schema["type"].(string) 122 | if !ok || propType != "object" { 123 | return 124 | } 125 | schema["minProperties"] = min 126 | } 127 | } 128 | 129 | // Required sets the tool parameter as required. 130 | // When a parameter is marked as required, the client must provide a value 131 | // for this parameter or the tool call will fail with an error. 132 | func Required() PropertyOption { 133 | return func(schema map[string]interface{}) { 134 | schema["required"] = true 135 | } 136 | } 137 | 138 | // Description sets the description for the tool parameter. 139 | // The description should explain the purpose of the parameter, expected format, 140 | // and any relevant constraints. 141 | func Description(desc string) PropertyOption { 142 | return func(schema map[string]interface{}) { 143 | schema["description"] = desc 144 | } 145 | } 146 | 147 | // ToolParameter represents a parameter for a tool 148 | type ToolParameter struct { 149 | Name string 150 | Schema map[string]interface{} 151 | } 152 | 153 | // applyPropertyOptions applies the given property options to 154 | // the parameter schema 155 | func (p *ToolParameter) applyPropertyOptions(opts ...PropertyOption) { 156 | for _, opt := range opts { 157 | opt(p.Schema) 158 | } 159 | } 160 | 161 | // WithString creates a string parameter with optional property options 162 | func WithString(name string, opts ...PropertyOption) ToolParameter { 163 | param := ToolParameter{ 164 | Name: name, 165 | Schema: map[string]interface{}{"type": "string"}, 166 | } 167 | param.applyPropertyOptions(opts...) 168 | return param 169 | } 170 | 171 | // WithNumber creates a number parameter with optional property options 172 | func WithNumber(name string, opts ...PropertyOption) ToolParameter { 173 | param := ToolParameter{ 174 | Name: name, 175 | Schema: map[string]interface{}{"type": "number"}, 176 | } 177 | param.applyPropertyOptions(opts...) 178 | return param 179 | } 180 | 181 | // WithBoolean creates a boolean parameter with optional property options 182 | func WithBoolean(name string, opts ...PropertyOption) ToolParameter { 183 | param := ToolParameter{ 184 | Name: name, 185 | Schema: map[string]interface{}{"type": "boolean"}, 186 | } 187 | param.applyPropertyOptions(opts...) 188 | return param 189 | } 190 | 191 | // WithObject creates an object parameter with optional property options 192 | func WithObject(name string, opts ...PropertyOption) ToolParameter { 193 | param := ToolParameter{ 194 | Name: name, 195 | Schema: map[string]interface{}{"type": "object"}, 196 | } 197 | param.applyPropertyOptions(opts...) 198 | return param 199 | } 200 | 201 | // WithArray creates an array parameter with optional property options 202 | func WithArray(name string, opts ...PropertyOption) ToolParameter { 203 | param := ToolParameter{ 204 | Name: name, 205 | Schema: map[string]interface{}{"type": "array"}, 206 | } 207 | param.applyPropertyOptions(opts...) 208 | return param 209 | } 210 | 211 | // mark3labsToolImpl implements the Tool interface 212 | type mark3labsToolImpl struct { 213 | name string 214 | description string 215 | handler ToolHandler 216 | parameters []ToolParameter 217 | } 218 | 219 | // NewTool creates a new tool with the given 220 | // Name, description, parameters and handler 221 | func NewTool( 222 | name, 223 | description string, 224 | parameters []ToolParameter, 225 | handler ToolHandler) *mark3labsToolImpl { 226 | return &mark3labsToolImpl{ 227 | name: name, 228 | description: description, 229 | handler: handler, 230 | parameters: parameters, 231 | } 232 | } 233 | 234 | // addNumberPropertyOptions adds number-specific options to the property options 235 | func addNumberPropertyOptions( 236 | propOpts []mcp.PropertyOption, 237 | schema map[string]interface{}) []mcp.PropertyOption { 238 | // Add minimum if present 239 | if min, ok := schema["minimum"].(float64); ok { 240 | propOpts = append(propOpts, mcp.Min(min)) 241 | } 242 | 243 | // Add maximum if present 244 | if max, ok := schema["maximum"].(float64); ok { 245 | propOpts = append(propOpts, mcp.Max(max)) 246 | } 247 | 248 | return propOpts 249 | } 250 | 251 | // addStringPropertyOptions adds string-specific options to the property options 252 | func addStringPropertyOptions( 253 | propOpts []mcp.PropertyOption, 254 | schema map[string]interface{}) []mcp.PropertyOption { 255 | // Add minLength if present 256 | if minLength, ok := schema["minLength"].(int); ok { 257 | propOpts = append(propOpts, mcp.MinLength(minLength)) 258 | } 259 | 260 | // Add maxLength if present 261 | if maxLength, ok := schema["maxLength"].(int); ok { 262 | propOpts = append(propOpts, mcp.MaxLength(maxLength)) 263 | } 264 | 265 | // Add pattern if present 266 | if pattern, ok := schema["pattern"].(string); ok { 267 | propOpts = append(propOpts, mcp.Pattern(pattern)) 268 | } 269 | 270 | return propOpts 271 | } 272 | 273 | // addDefaultValueOptions adds default value options based on type 274 | func addDefaultValueOptions( 275 | propOpts []mcp.PropertyOption, 276 | defaultValue interface{}) []mcp.PropertyOption { 277 | switch val := defaultValue.(type) { 278 | case string: 279 | propOpts = append(propOpts, mcp.DefaultString(val)) 280 | case float64: 281 | propOpts = append(propOpts, mcp.DefaultNumber(val)) 282 | case bool: 283 | propOpts = append(propOpts, mcp.DefaultBool(val)) 284 | } 285 | return propOpts 286 | } 287 | 288 | // addEnumOptions adds enum options if present 289 | func addEnumOptions( 290 | propOpts []mcp.PropertyOption, 291 | enumValues interface{}) []mcp.PropertyOption { 292 | values, ok := enumValues.([]interface{}) 293 | if !ok { 294 | return propOpts 295 | } 296 | 297 | // Convert values to strings for now 298 | strValues := make([]string, 0, len(values)) 299 | for _, ev := range values { 300 | if str, ok := ev.(string); ok { 301 | strValues = append(strValues, str) 302 | } 303 | } 304 | 305 | if len(strValues) > 0 { 306 | propOpts = append(propOpts, mcp.Enum(strValues...)) 307 | } 308 | 309 | return propOpts 310 | } 311 | 312 | // addObjectPropertyOptions adds object-specific options 313 | func addObjectPropertyOptions( 314 | propOpts []mcp.PropertyOption, 315 | schema map[string]interface{}) []mcp.PropertyOption { 316 | // Add maxProperties if present 317 | if maxProps, ok := schema["maxProperties"].(int); ok { 318 | propOpts = append(propOpts, mcp.MaxProperties(maxProps)) 319 | } 320 | 321 | // Add minProperties if present 322 | if minProps, ok := schema["minProperties"].(int); ok { 323 | propOpts = append(propOpts, mcp.MinProperties(minProps)) 324 | } 325 | 326 | return propOpts 327 | } 328 | 329 | // addArrayPropertyOptions adds array-specific options 330 | func addArrayPropertyOptions( 331 | propOpts []mcp.PropertyOption, 332 | schema map[string]interface{}) []mcp.PropertyOption { 333 | // Add minItems if present 334 | if minItems, ok := schema["minItems"].(int); ok { 335 | propOpts = append(propOpts, mcp.MinItems(minItems)) 336 | } 337 | 338 | // Add maxItems if present 339 | if maxItems, ok := schema["maxItems"].(int); ok { 340 | propOpts = append(propOpts, mcp.MaxItems(maxItems)) 341 | } 342 | 343 | return propOpts 344 | } 345 | 346 | // convertSchemaToPropertyOptions converts our schema to mcp property options 347 | func convertSchemaToPropertyOptions( 348 | schema map[string]interface{}) []mcp.PropertyOption { 349 | var propOpts []mcp.PropertyOption 350 | 351 | // Add description if present 352 | if description, ok := schema["description"].(string); ok && description != "" { 353 | propOpts = append(propOpts, mcp.Description(description)) 354 | } 355 | 356 | // Add required flag if present 357 | if required, ok := schema["required"].(bool); ok && required { 358 | propOpts = append(propOpts, mcp.Required()) 359 | } 360 | 361 | // Skip type, description and required as they're handled separately 362 | for k, v := range schema { 363 | if k == "type" || k == "description" || k == "required" { 364 | continue 365 | } 366 | 367 | // Process property based on key 368 | switch k { 369 | case "minimum", "maximum": 370 | propOpts = addNumberPropertyOptions(propOpts, schema) 371 | case "minLength", "maxLength", "pattern": 372 | propOpts = addStringPropertyOptions(propOpts, schema) 373 | case "default": 374 | propOpts = addDefaultValueOptions(propOpts, v) 375 | case "enum": 376 | propOpts = addEnumOptions(propOpts, v) 377 | case "maxProperties", "minProperties": 378 | propOpts = addObjectPropertyOptions(propOpts, schema) 379 | case "minItems", "maxItems": 380 | propOpts = addArrayPropertyOptions(propOpts, schema) 381 | } 382 | } 383 | 384 | return propOpts 385 | } 386 | 387 | // GetHandler returns the handler for the tool 388 | func (t *mark3labsToolImpl) GetHandler() ToolHandler { 389 | return t.handler 390 | } 391 | 392 | // toMCPServerTool converts our Tool to mcp's ServerTool 393 | func (t *mark3labsToolImpl) toMCPServerTool() server.ServerTool { 394 | // Create the mcp tool with appropriate options 395 | var toolOpts []mcp.ToolOption 396 | 397 | // Add description 398 | toolOpts = append(toolOpts, mcp.WithDescription(t.description)) 399 | 400 | // Add parameters with their schemas 401 | for _, param := range t.parameters { 402 | // Get property options from schema 403 | propOpts := convertSchemaToPropertyOptions(param.Schema) 404 | 405 | // Get the type from the schema 406 | schemaType, ok := param.Schema["type"].(string) 407 | if !ok { 408 | // Default to string if type is missing or not a string 409 | schemaType = "string" 410 | } 411 | 412 | // Use the appropriate function based on schema type 413 | switch schemaType { 414 | case "string": 415 | toolOpts = append(toolOpts, mcp.WithString(param.Name, propOpts...)) 416 | case "number", "integer": 417 | toolOpts = append(toolOpts, mcp.WithNumber(param.Name, propOpts...)) 418 | case "boolean": 419 | toolOpts = append(toolOpts, mcp.WithBoolean(param.Name, propOpts...)) 420 | case "object": 421 | toolOpts = append(toolOpts, mcp.WithObject(param.Name, propOpts...)) 422 | case "array": 423 | toolOpts = append(toolOpts, mcp.WithArray(param.Name, propOpts...)) 424 | default: 425 | // Unknown type, default to string 426 | toolOpts = append(toolOpts, mcp.WithString(param.Name, propOpts...)) 427 | } 428 | } 429 | 430 | // Create the tool with all options 431 | tool := mcp.NewTool(t.name, toolOpts...) 432 | 433 | // Create the handler 434 | handlerFunc := func( 435 | ctx context.Context, 436 | req mcp.CallToolRequest, 437 | ) (*mcp.CallToolResult, error) { 438 | // Convert mcp request to our request 439 | ourReq := CallToolRequest{ 440 | Name: req.Params.Name, 441 | Arguments: req.Params.Arguments, 442 | } 443 | 444 | // Call our handler 445 | result, err := t.handler(ctx, ourReq) 446 | if err != nil { 447 | return nil, err 448 | } 449 | 450 | // Convert our result to mcp result 451 | var mcpResult *mcp.CallToolResult 452 | if result.IsError { 453 | mcpResult = mcp.NewToolResultError(result.Text) 454 | } else { 455 | mcpResult = mcp.NewToolResultText(result.Text) 456 | } 457 | 458 | return mcpResult, nil 459 | } 460 | 461 | return server.ServerTool{ 462 | Tool: tool, 463 | Handler: handlerFunc, 464 | } 465 | } 466 | 467 | // NewToolResultJSON creates a new tool result with JSON content 468 | func NewToolResultJSON(data interface{}) (*ToolResult, error) { 469 | jsonBytes, err := json.Marshal(data) 470 | if err != nil { 471 | return nil, err 472 | } 473 | 474 | return &ToolResult{ 475 | Text: string(jsonBytes), 476 | IsError: false, 477 | Content: nil, 478 | }, nil 479 | } 480 | 481 | // NewToolResultText creates a new tool result with text content 482 | func NewToolResultText(text string) *ToolResult { 483 | return &ToolResult{ 484 | Text: text, 485 | IsError: false, 486 | Content: nil, 487 | } 488 | } 489 | 490 | // NewToolResultError creates a new tool result with an error 491 | func NewToolResultError(text string) *ToolResult { 492 | return &ToolResult{ 493 | Text: text, 494 | IsError: true, 495 | Content: nil, 496 | } 497 | } 498 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/tokens_test.go: -------------------------------------------------------------------------------- ```go 1 | package razorpay 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/razorpay/razorpay-go/constants" 12 | 13 | "github.com/razorpay/razorpay-mcp-server/pkg/contextkey" 14 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" 15 | "github.com/razorpay/razorpay-mcp-server/pkg/razorpay/mock" 16 | ) 17 | 18 | func Test_FetchSavedPaymentMethods(t *testing.T) { 19 | // URL patterns for mocking 20 | createCustomerPath := fmt.Sprintf( 21 | "/%s%s", 22 | constants.VERSION_V1, 23 | constants.CUSTOMER_URL, 24 | ) 25 | 26 | fetchTokensPathFmt := fmt.Sprintf( 27 | "/%s/customers/%%s/tokens", 28 | constants.VERSION_V1, 29 | ) 30 | 31 | // Sample successful customer creation/fetch response 32 | customerResp := map[string]interface{}{ 33 | "id": "cust_1Aa00000000003", 34 | "entity": "customer", 35 | "name": "", 36 | "email": "", 37 | "contact": "9876543210", 38 | "gstin": nil, 39 | "notes": map[string]interface{}{}, 40 | "created_at": float64(1234567890), 41 | } 42 | 43 | // Sample successful tokens response 44 | tokensResp := map[string]interface{}{ 45 | "entity": "collection", 46 | "count": float64(2), 47 | "items": []interface{}{ 48 | map[string]interface{}{ 49 | "id": "token_ABCDEFGH", 50 | "entity": "token", 51 | "token": "EhYXHrLsJdwRhM", 52 | "bank": nil, 53 | "wallet": nil, 54 | "method": "card", 55 | "card": map[string]interface{}{ 56 | "entity": "card", 57 | "name": "Gaurav Kumar", 58 | "last4": "1111", 59 | "network": "Visa", 60 | "type": "debit", 61 | "issuer": "HDFC", 62 | "international": false, 63 | "emi": false, 64 | "sub_type": "consumer", 65 | }, 66 | "vpa": nil, 67 | "recurring": true, 68 | "recurring_details": map[string]interface{}{ 69 | "status": "confirmed", 70 | "failure_reason": nil, 71 | }, 72 | "auth_type": nil, 73 | "mrn": nil, 74 | "used_at": float64(1629779657), 75 | "created_at": float64(1629779657), 76 | "expired_at": float64(1640918400), 77 | "dcc_enabled": false, 78 | }, 79 | map[string]interface{}{ 80 | "id": "token_EhYXHrLsJdwRhN", 81 | "entity": "token", 82 | "token": "EhYXHrLsJdwRhN", 83 | "bank": nil, 84 | "wallet": nil, 85 | "method": "upi", 86 | "card": nil, 87 | "vpa": map[string]interface{}{ 88 | "username": "gauravkumar", 89 | "handle": "okhdfcbank", 90 | "name": "Gaurav Kumar", 91 | }, 92 | "recurring": true, 93 | "recurring_details": map[string]interface{}{ 94 | "status": "confirmed", 95 | "failure_reason": nil, 96 | }, 97 | "auth_type": nil, 98 | "mrn": nil, 99 | "used_at": float64(1629779657), 100 | "created_at": float64(1629779657), 101 | "expired_at": float64(1640918400), 102 | "dcc_enabled": false, 103 | }, 104 | }, 105 | } 106 | 107 | // Expected combined response 108 | expectedSuccessResp := map[string]interface{}{ 109 | "customer": customerResp, 110 | "saved_payment_methods": tokensResp, 111 | } 112 | 113 | // Error responses 114 | customerCreationFailedResp := map[string]interface{}{ 115 | "error": map[string]interface{}{ 116 | "code": "BAD_REQUEST_ERROR", 117 | "description": "Contact number is invalid", 118 | }, 119 | } 120 | 121 | tokensAPIFailedResp := map[string]interface{}{ 122 | "error": map[string]interface{}{ 123 | "code": "BAD_REQUEST_ERROR", 124 | "description": "Customer not found", 125 | }, 126 | } 127 | 128 | // Customer response without ID (invalid) 129 | invalidCustomerResp := map[string]interface{}{ 130 | "entity": "customer", 131 | "name": "", 132 | "email": "", 133 | "contact": "9876543210", 134 | "gstin": nil, 135 | "notes": map[string]interface{}{}, 136 | "created_at": float64(1234567890), 137 | // Missing "id" field 138 | } 139 | 140 | tests := []RazorpayToolTestCase{ 141 | { 142 | Name: "successful fetch of saved cards with valid contact", 143 | Request: map[string]interface{}{ 144 | "contact": "9876543210", 145 | }, 146 | MockHttpClient: func() (*http.Client, *httptest.Server) { 147 | return mock.NewHTTPClient( 148 | mock.Endpoint{ 149 | Path: createCustomerPath, 150 | Method: "POST", 151 | Response: customerResp, 152 | }, 153 | mock.Endpoint{ 154 | Path: fmt.Sprintf(fetchTokensPathFmt, "cust_1Aa00000000003"), 155 | Method: "GET", 156 | Response: tokensResp, 157 | }, 158 | ) 159 | }, 160 | ExpectError: false, 161 | ExpectedResult: expectedSuccessResp, 162 | }, 163 | { 164 | Name: "successful fetch with international contact format", 165 | Request: map[string]interface{}{ 166 | "contact": "+919876543210", 167 | }, 168 | MockHttpClient: func() (*http.Client, *httptest.Server) { 169 | customerRespIntl := map[string]interface{}{ 170 | "id": "cust_1Aa00000000004", 171 | "entity": "customer", 172 | "name": "", 173 | "email": "", 174 | "contact": "+919876543210", 175 | "gstin": nil, 176 | "notes": map[string]interface{}{}, 177 | "created_at": float64(1234567890), 178 | } 179 | return mock.NewHTTPClient( 180 | mock.Endpoint{ 181 | Path: createCustomerPath, 182 | Method: "POST", 183 | Response: customerRespIntl, 184 | }, 185 | mock.Endpoint{ 186 | Path: fmt.Sprintf(fetchTokensPathFmt, "cust_1Aa00000000004"), 187 | Method: "GET", 188 | Response: tokensResp, 189 | }, 190 | ) 191 | }, 192 | ExpectError: false, 193 | ExpectedResult: map[string]interface{}{ 194 | "customer": map[string]interface{}{ 195 | "id": "cust_1Aa00000000004", 196 | "entity": "customer", 197 | "name": "", 198 | "email": "", 199 | "contact": "+919876543210", 200 | "gstin": nil, 201 | "notes": map[string]interface{}{}, 202 | "created_at": float64(1234567890), 203 | }, 204 | "saved_payment_methods": tokensResp, 205 | }, 206 | }, 207 | { 208 | Name: "customer creation/fetch failure", 209 | Request: map[string]interface{}{ 210 | "contact": "invalid_contact", 211 | }, 212 | MockHttpClient: func() (*http.Client, *httptest.Server) { 213 | return mock.NewHTTPClient( 214 | mock.Endpoint{ 215 | Path: createCustomerPath, 216 | Method: "POST", 217 | Response: customerCreationFailedResp, 218 | }, 219 | ) 220 | }, 221 | ExpectError: true, 222 | ExpectedErrMsg: "Failed to create/fetch customer with " + 223 | "contact invalid_contact: Contact number is invalid", 224 | }, 225 | { 226 | Name: "tokens API failure after successful customer creation", 227 | Request: map[string]interface{}{ 228 | "contact": "9876543210", 229 | }, 230 | MockHttpClient: func() (*http.Client, *httptest.Server) { 231 | return mock.NewHTTPClient( 232 | mock.Endpoint{ 233 | Path: createCustomerPath, 234 | Method: "POST", 235 | Response: customerResp, 236 | }, 237 | mock.Endpoint{ 238 | Path: fmt.Sprintf(fetchTokensPathFmt, "cust_1Aa00000000003"), 239 | Method: "GET", 240 | Response: tokensAPIFailedResp, 241 | }, 242 | ) 243 | }, 244 | ExpectError: true, 245 | ExpectedErrMsg: "Failed to fetch saved payment methods for " + 246 | "customer cust_1Aa00000000003: Customer not found", 247 | }, 248 | { 249 | Name: "invalid customer response - missing customer ID", 250 | Request: map[string]interface{}{ 251 | "contact": "9876543210", 252 | }, 253 | MockHttpClient: func() (*http.Client, *httptest.Server) { 254 | return mock.NewHTTPClient( 255 | mock.Endpoint{ 256 | Path: createCustomerPath, 257 | Method: "POST", 258 | Response: invalidCustomerResp, 259 | }, 260 | ) 261 | }, 262 | ExpectError: true, 263 | ExpectedErrMsg: "Customer ID not found in response", 264 | }, 265 | { 266 | Name: "missing contact parameter", 267 | Request: map[string]interface{}{ 268 | // No contact parameter 269 | }, 270 | MockHttpClient: nil, // No HTTP client needed for validation error 271 | ExpectError: true, 272 | ExpectedErrMsg: "missing required parameter: contact", 273 | }, 274 | { 275 | Name: "empty contact parameter", 276 | Request: map[string]interface{}{ 277 | "contact": "", 278 | }, 279 | MockHttpClient: nil, // No HTTP client needed for validation error 280 | ExpectError: true, 281 | ExpectedErrMsg: "missing required parameter: contact", 282 | }, 283 | { 284 | Name: "null contact parameter", 285 | Request: map[string]interface{}{ 286 | "contact": nil, 287 | }, 288 | MockHttpClient: nil, // No HTTP client needed for validation error 289 | ExpectError: true, 290 | ExpectedErrMsg: "missing required parameter: contact", 291 | }, 292 | { 293 | Name: "successful fetch with empty tokens list", 294 | Request: map[string]interface{}{ 295 | "contact": "9876543210", 296 | }, 297 | MockHttpClient: func() (*http.Client, *httptest.Server) { 298 | emptyTokensResp := map[string]interface{}{ 299 | "entity": "collection", 300 | "count": float64(0), 301 | "items": []interface{}{}, 302 | } 303 | return mock.NewHTTPClient( 304 | mock.Endpoint{ 305 | Path: createCustomerPath, 306 | Method: "POST", 307 | Response: customerResp, 308 | }, 309 | mock.Endpoint{ 310 | Path: fmt.Sprintf(fetchTokensPathFmt, "cust_1Aa00000000003"), 311 | Method: "GET", 312 | Response: emptyTokensResp, 313 | }, 314 | ) 315 | }, 316 | ExpectError: false, 317 | ExpectedResult: map[string]interface{}{ 318 | "customer": customerResp, 319 | "saved_payment_methods": map[string]interface{}{ 320 | "entity": "collection", 321 | "count": float64(0), 322 | "items": []interface{}{}, 323 | }, 324 | }, 325 | }, 326 | } 327 | 328 | for _, tc := range tests { 329 | t.Run(tc.Name, func(t *testing.T) { 330 | runToolTest(t, tc, FetchSavedPaymentMethods, "Saved Cards") 331 | }) 332 | } 333 | } 334 | 335 | // Test_FetchSavedPaymentMethods_ClientContextScenarios tests scenarios 336 | // related to client context handling for 100% code coverage 337 | func Test_FetchSavedPaymentMethods_ClientContextScenarios(t *testing.T) { 338 | obs := CreateTestObservability() 339 | 340 | t.Run("no client in context and default is nil", func(t *testing.T) { 341 | // Create tool with nil client 342 | tool := FetchSavedPaymentMethods(obs, nil) 343 | 344 | // Create context without client 345 | ctx := context.Background() 346 | request := mcpgo.CallToolRequest{ 347 | Arguments: map[string]interface{}{ 348 | "contact": "9876543210", 349 | }, 350 | } 351 | 352 | result, err := tool.GetHandler()(ctx, request) 353 | 354 | if err != nil { 355 | t.Fatalf("Expected no error, got %v", err) 356 | } 357 | 358 | if result == nil { 359 | t.Fatal("Expected result, got nil") 360 | } 361 | 362 | if result.Text == "" { 363 | t.Fatal("Expected error message in result") 364 | } 365 | 366 | expectedErrMsg := "no client found in context" 367 | if !strings.Contains(result.Text, expectedErrMsg) { 368 | t.Errorf( 369 | "Expected error message to contain '%s', got '%s'", 370 | expectedErrMsg, 371 | result.Text, 372 | ) 373 | } 374 | }) 375 | 376 | t.Run("invalid client type in context", func(t *testing.T) { 377 | // Create tool with nil client 378 | tool := FetchSavedPaymentMethods(obs, nil) 379 | 380 | // Create context with invalid client type 381 | ctx := contextkey.WithClient(context.Background(), "invalid_client_type") 382 | request := mcpgo.CallToolRequest{ 383 | Arguments: map[string]interface{}{ 384 | "contact": "9876543210", 385 | }, 386 | } 387 | 388 | result, err := tool.GetHandler()(ctx, request) 389 | 390 | if err != nil { 391 | t.Fatalf("Expected no error, got %v", err) 392 | } 393 | 394 | if result == nil { 395 | t.Fatal("Expected result, got nil") 396 | } 397 | 398 | if result.Text == "" { 399 | t.Fatal("Expected error message in result") 400 | } 401 | 402 | expectedErrMsg := "invalid client type in context" 403 | if !strings.Contains(result.Text, expectedErrMsg) { 404 | t.Errorf( 405 | "Expected error message to contain '%s', got '%s'", 406 | expectedErrMsg, 407 | result.Text, 408 | ) 409 | } 410 | }) 411 | } 412 | 413 | func Test_RevokeToken(t *testing.T) { 414 | // URL patterns for mocking 415 | revokeTokenPathFmt := fmt.Sprintf( 416 | "/%s/customers/%%s/tokens/%%s/cancel", 417 | constants.VERSION_V1, 418 | ) 419 | 420 | // Sample successful token revocation response 421 | successResp := map[string]interface{}{ 422 | "deleted": true, 423 | } 424 | 425 | // Error responses 426 | tokenNotFoundResp := map[string]interface{}{ 427 | "error": map[string]interface{}{ 428 | "code": "BAD_REQUEST_ERROR", 429 | "description": "Token not found", 430 | }, 431 | } 432 | 433 | customerNotFoundResp := map[string]interface{}{ 434 | "error": map[string]interface{}{ 435 | "code": "BAD_REQUEST_ERROR", 436 | "description": "Customer not found", 437 | }, 438 | } 439 | 440 | tests := []RazorpayToolTestCase{ 441 | { 442 | Name: "successful token revocation with valid parameters", 443 | Request: map[string]interface{}{ 444 | "customer_id": "cust_1Aa00000000003", 445 | "token_id": "token_ABCDEFGH", 446 | }, 447 | MockHttpClient: func() (*http.Client, *httptest.Server) { 448 | return mock.NewHTTPClient( 449 | mock.Endpoint{ 450 | Path: fmt.Sprintf( 451 | revokeTokenPathFmt, 452 | "cust_1Aa00000000003", 453 | "token_ABCDEFGH", 454 | ), 455 | Method: "PUT", 456 | Response: successResp, 457 | }, 458 | ) 459 | }, 460 | ExpectError: false, 461 | ExpectedResult: successResp, 462 | }, 463 | { 464 | Name: "token not found error", 465 | Request: map[string]interface{}{ 466 | "customer_id": "cust_1Aa00000000003", 467 | "token_id": "token_nonexistent", 468 | }, 469 | MockHttpClient: func() (*http.Client, *httptest.Server) { 470 | return mock.NewHTTPClient( 471 | mock.Endpoint{ 472 | Path: fmt.Sprintf( 473 | revokeTokenPathFmt, 474 | "cust_1Aa00000000003", 475 | "token_nonexistent", 476 | ), 477 | Method: "PUT", 478 | Response: tokenNotFoundResp, 479 | }, 480 | ) 481 | }, 482 | ExpectError: true, 483 | ExpectedErrMsg: "Failed to revoke token token_nonexistent for " + 484 | "customer cust_1Aa00000000003: Token not found", 485 | }, 486 | { 487 | Name: "customer not found error", 488 | Request: map[string]interface{}{ 489 | "customer_id": "cust_nonexistent", 490 | "token_id": "token_ABCDEFGH", 491 | }, 492 | MockHttpClient: func() (*http.Client, *httptest.Server) { 493 | return mock.NewHTTPClient( 494 | mock.Endpoint{ 495 | Path: fmt.Sprintf( 496 | revokeTokenPathFmt, 497 | "cust_nonexistent", 498 | "token_ABCDEFGH", 499 | ), 500 | Method: "PUT", 501 | Response: customerNotFoundResp, 502 | }, 503 | ) 504 | }, 505 | ExpectError: true, 506 | ExpectedErrMsg: "Failed to revoke token token_ABCDEFGH for " + 507 | "customer cust_nonexistent: Customer not found", 508 | }, 509 | { 510 | Name: "missing customer_id parameter", 511 | Request: map[string]interface{}{ 512 | "token_id": "token_ABCDEFGH", 513 | }, 514 | MockHttpClient: nil, // No HTTP client needed for validation error 515 | ExpectError: true, 516 | ExpectedErrMsg: "missing required parameter: customer_id", 517 | }, 518 | { 519 | Name: "missing token_id parameter", 520 | Request: map[string]interface{}{ 521 | "customer_id": "cust_1Aa00000000003", 522 | }, 523 | MockHttpClient: nil, // No HTTP client needed for validation error 524 | ExpectError: true, 525 | ExpectedErrMsg: "missing required parameter: token_id", 526 | }, 527 | { 528 | Name: "empty customer_id parameter", 529 | Request: map[string]interface{}{ 530 | "customer_id": "", 531 | "token_id": "token_ABCDEFGH", 532 | }, 533 | MockHttpClient: nil, // No HTTP client needed for validation error 534 | ExpectError: true, 535 | ExpectedErrMsg: "missing required parameter: customer_id", 536 | }, 537 | { 538 | Name: "empty token_id parameter", 539 | Request: map[string]interface{}{ 540 | "customer_id": "cust_1Aa00000000003", 541 | "token_id": "", 542 | }, 543 | MockHttpClient: nil, // No HTTP client needed for validation error 544 | ExpectError: true, 545 | ExpectedErrMsg: "missing required parameter: token_id", 546 | }, 547 | { 548 | Name: "null customer_id parameter", 549 | Request: map[string]interface{}{ 550 | "customer_id": nil, 551 | "token_id": "token_ABCDEFGH", 552 | }, 553 | MockHttpClient: nil, // No HTTP client needed for validation error 554 | ExpectError: true, 555 | ExpectedErrMsg: "missing required parameter: customer_id", 556 | }, 557 | { 558 | Name: "null token_id parameter", 559 | Request: map[string]interface{}{ 560 | "customer_id": "cust_1Aa00000000003", 561 | "token_id": nil, 562 | }, 563 | MockHttpClient: nil, // No HTTP client needed for validation error 564 | ExpectError: true, 565 | ExpectedErrMsg: "missing required parameter: token_id", 566 | }, 567 | { 568 | Name: "both parameters missing", 569 | Request: map[string]interface{}{ 570 | // No parameters 571 | }, 572 | MockHttpClient: nil, // No HTTP client needed for validation error 573 | ExpectError: true, 574 | ExpectedErrMsg: "missing required parameter: customer_id", 575 | }, 576 | } 577 | 578 | for _, tc := range tests { 579 | t.Run(tc.Name, func(t *testing.T) { 580 | runToolTest(t, tc, RevokeToken, "Revoke Token") 581 | }) 582 | } 583 | } 584 | 585 | // Test_RevokeToken_ClientContextScenarios tests scenarios 586 | // related to client context handling for 100% code coverage 587 | func Test_RevokeToken_ClientContextScenarios(t *testing.T) { 588 | obs := CreateTestObservability() 589 | 590 | t.Run("no client in context and default is nil", func(t *testing.T) { 591 | // Create tool with nil client 592 | tool := RevokeToken(obs, nil) 593 | 594 | // Create context without client 595 | ctx := context.Background() 596 | request := mcpgo.CallToolRequest{ 597 | Arguments: map[string]interface{}{ 598 | "customer_id": "cust_1Aa00000000003", 599 | "token_id": "token_ABCDEFGH", 600 | }, 601 | } 602 | 603 | result, err := tool.GetHandler()(ctx, request) 604 | 605 | if err != nil { 606 | t.Fatalf("Expected no error, got %v", err) 607 | } 608 | 609 | if result == nil { 610 | t.Fatal("Expected result, got nil") 611 | } 612 | 613 | if result.Text == "" { 614 | t.Fatal("Expected error message in result") 615 | } 616 | 617 | expectedErrMsg := "no client found in context" 618 | if !strings.Contains(result.Text, expectedErrMsg) { 619 | t.Errorf( 620 | "Expected error message to contain '%s', got '%s'", 621 | expectedErrMsg, 622 | result.Text, 623 | ) 624 | } 625 | }) 626 | 627 | t.Run("invalid client type in context", func(t *testing.T) { 628 | // Create tool with nil client 629 | tool := RevokeToken(obs, nil) 630 | 631 | // Create context with invalid client type 632 | ctx := contextkey.WithClient(context.Background(), "invalid_client_type") 633 | request := mcpgo.CallToolRequest{ 634 | Arguments: map[string]interface{}{ 635 | "customer_id": "cust_1Aa00000000003", 636 | "token_id": "token_ABCDEFGH", 637 | }, 638 | } 639 | 640 | result, err := tool.GetHandler()(ctx, request) 641 | 642 | if err != nil { 643 | t.Fatalf("Expected no error, got %v", err) 644 | } 645 | 646 | if result == nil { 647 | t.Fatal("Expected result, got nil") 648 | } 649 | 650 | if result.Text == "" { 651 | t.Fatal("Expected error message in result") 652 | } 653 | 654 | expectedErrMsg := "invalid client type in context" 655 | if !strings.Contains(result.Text, expectedErrMsg) { 656 | t.Errorf( 657 | "Expected error message to contain '%s', got '%s'", 658 | expectedErrMsg, 659 | result.Text, 660 | ) 661 | } 662 | }) 663 | } 664 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/payment_links_test.go: -------------------------------------------------------------------------------- ```go 1 | package razorpay 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/razorpay/razorpay-go/constants" 10 | 11 | "github.com/razorpay/razorpay-mcp-server/pkg/razorpay/mock" 12 | ) 13 | 14 | func Test_CreatePaymentLink(t *testing.T) { 15 | createPaymentLinkPath := fmt.Sprintf( 16 | "/%s%s", 17 | constants.VERSION_V1, 18 | constants.PaymentLink_URL, 19 | ) 20 | 21 | successfulPaymentLinkResp := map[string]interface{}{ 22 | "id": "plink_ExjpAUN3gVHrPJ", 23 | "amount": float64(50000), 24 | "currency": "INR", 25 | "description": "Test payment", 26 | "status": "created", 27 | "short_url": "https://rzp.io/i/nxrHnLJ", 28 | } 29 | 30 | paymentLinkWithoutDescResp := map[string]interface{}{ 31 | "id": "plink_ExjpAUN3gVHrPJ", 32 | "amount": float64(50000), 33 | "currency": "INR", 34 | "status": "created", 35 | "short_url": "https://rzp.io/i/nxrHnLJ", 36 | } 37 | 38 | invalidCurrencyErrorResp := map[string]interface{}{ 39 | "error": map[string]interface{}{ 40 | "code": "BAD_REQUEST_ERROR", 41 | "description": "API error: Invalid currency", 42 | }, 43 | } 44 | 45 | tests := []RazorpayToolTestCase{ 46 | { 47 | Name: "successful payment link creation", 48 | Request: map[string]interface{}{ 49 | "amount": float64(50000), 50 | "currency": "INR", 51 | "description": "Test payment", 52 | }, 53 | MockHttpClient: func() (*http.Client, *httptest.Server) { 54 | return mock.NewHTTPClient( 55 | mock.Endpoint{ 56 | Path: createPaymentLinkPath, 57 | Method: "POST", 58 | Response: successfulPaymentLinkResp, 59 | }, 60 | ) 61 | }, 62 | ExpectError: false, 63 | ExpectedResult: successfulPaymentLinkResp, 64 | }, 65 | { 66 | Name: "payment link without description", 67 | Request: map[string]interface{}{ 68 | "amount": float64(50000), 69 | "currency": "INR", 70 | }, 71 | MockHttpClient: func() (*http.Client, *httptest.Server) { 72 | return mock.NewHTTPClient( 73 | mock.Endpoint{ 74 | Path: createPaymentLinkPath, 75 | Method: "POST", 76 | Response: paymentLinkWithoutDescResp, 77 | }, 78 | ) 79 | }, 80 | ExpectError: false, 81 | ExpectedResult: paymentLinkWithoutDescResp, 82 | }, 83 | { 84 | Name: "missing amount parameter", 85 | Request: map[string]interface{}{ 86 | "currency": "INR", 87 | }, 88 | MockHttpClient: nil, // No HTTP client needed for validation error 89 | ExpectError: true, 90 | ExpectedErrMsg: "missing required parameter: amount", 91 | }, 92 | { 93 | Name: "missing currency parameter", 94 | Request: map[string]interface{}{ 95 | "amount": float64(50000), 96 | }, 97 | MockHttpClient: nil, // No HTTP client needed for validation error 98 | ExpectError: true, 99 | ExpectedErrMsg: "missing required parameter: currency", 100 | }, 101 | { 102 | Name: "multiple validation errors", 103 | Request: map[string]interface{}{ 104 | // Missing both amount and currency (required parameters) 105 | "description": 12345, // Wrong type for description 106 | }, 107 | MockHttpClient: nil, // No HTTP client needed for validation error 108 | ExpectError: true, 109 | ExpectedErrMsg: "Validation errors:\n- " + 110 | "missing required parameter: amount\n- " + 111 | "missing required parameter: currency\n- " + 112 | "invalid parameter type: description", 113 | }, 114 | { 115 | Name: "payment link creation fails", 116 | Request: map[string]interface{}{ 117 | "amount": float64(50000), 118 | "currency": "XYZ", // Invalid currency 119 | }, 120 | MockHttpClient: func() (*http.Client, *httptest.Server) { 121 | return mock.NewHTTPClient( 122 | mock.Endpoint{ 123 | Path: createPaymentLinkPath, 124 | Method: "POST", 125 | Response: invalidCurrencyErrorResp, 126 | }, 127 | ) 128 | }, 129 | ExpectError: true, 130 | ExpectedErrMsg: "creating payment link failed: API error: Invalid currency", 131 | }, 132 | } 133 | 134 | for _, tc := range tests { 135 | t.Run(tc.Name, func(t *testing.T) { 136 | runToolTest(t, tc, CreatePaymentLink, "Payment Link") 137 | }) 138 | } 139 | } 140 | 141 | func Test_FetchPaymentLink(t *testing.T) { 142 | fetchPaymentLinkPathFmt := fmt.Sprintf( 143 | "/%s%s/%%s", 144 | constants.VERSION_V1, 145 | constants.PaymentLink_URL, 146 | ) 147 | 148 | // Define common response maps to be reused 149 | paymentLinkResp := map[string]interface{}{ 150 | "id": "plink_ExjpAUN3gVHrPJ", 151 | "amount": float64(50000), 152 | "currency": "INR", 153 | "description": "Test payment", 154 | "status": "paid", 155 | "short_url": "https://rzp.io/i/nxrHnLJ", 156 | } 157 | 158 | paymentLinkNotFoundResp := map[string]interface{}{ 159 | "error": map[string]interface{}{ 160 | "code": "BAD_REQUEST_ERROR", 161 | "description": "payment link not found", 162 | }, 163 | } 164 | 165 | tests := []RazorpayToolTestCase{ 166 | { 167 | Name: "successful payment link fetch", 168 | Request: map[string]interface{}{ 169 | "payment_link_id": "plink_ExjpAUN3gVHrPJ", 170 | }, 171 | MockHttpClient: func() (*http.Client, *httptest.Server) { 172 | return mock.NewHTTPClient( 173 | mock.Endpoint{ 174 | Path: fmt.Sprintf(fetchPaymentLinkPathFmt, "plink_ExjpAUN3gVHrPJ"), 175 | Method: "GET", 176 | Response: paymentLinkResp, 177 | }, 178 | ) 179 | }, 180 | ExpectError: false, 181 | ExpectedResult: paymentLinkResp, 182 | }, 183 | { 184 | Name: "payment link not found", 185 | Request: map[string]interface{}{ 186 | "payment_link_id": "plink_invalid", 187 | }, 188 | MockHttpClient: func() (*http.Client, *httptest.Server) { 189 | return mock.NewHTTPClient( 190 | mock.Endpoint{ 191 | Path: fmt.Sprintf(fetchPaymentLinkPathFmt, "plink_invalid"), 192 | Method: "GET", 193 | Response: paymentLinkNotFoundResp, 194 | }, 195 | ) 196 | }, 197 | ExpectError: true, 198 | ExpectedErrMsg: "fetching payment link failed: payment link not found", 199 | }, 200 | { 201 | Name: "missing payment_link_id parameter", 202 | Request: map[string]interface{}{}, 203 | MockHttpClient: nil, // No HTTP client needed for validation error 204 | ExpectError: true, 205 | ExpectedErrMsg: "missing required parameter: payment_link_id", 206 | }, 207 | { 208 | Name: "multiple validation errors", 209 | Request: map[string]interface{}{ 210 | // Missing payment_link_id parameter 211 | "non_existent_param": 12345, // Additional parameter that doesn't exist 212 | }, 213 | MockHttpClient: nil, // No HTTP client needed for validation error 214 | ExpectError: true, 215 | ExpectedErrMsg: "missing required parameter: payment_link_id", 216 | }, 217 | } 218 | 219 | for _, tc := range tests { 220 | t.Run(tc.Name, func(t *testing.T) { 221 | runToolTest(t, tc, FetchPaymentLink, "Payment Link") 222 | }) 223 | } 224 | } 225 | 226 | func Test_CreateUpiPaymentLink(t *testing.T) { 227 | createPaymentLinkPath := fmt.Sprintf( 228 | "/%s%s", 229 | constants.VERSION_V1, 230 | constants.PaymentLink_URL, 231 | ) 232 | 233 | upiPaymentLinkWithAllParamsResp := map[string]interface{}{ 234 | "id": "plink_UpiAllParamsExjpAUN3gVHrPJ", 235 | "amount": float64(50000), 236 | "currency": "INR", 237 | "description": "Test UPI payment with all params", 238 | "reference_id": "REF12345", 239 | "accept_partial": true, 240 | "expire_by": float64(1718196584), 241 | "reminder_enable": true, 242 | "status": "created", 243 | "short_url": "https://rzp.io/i/upiAllParams123", 244 | "upi_link": true, 245 | "customer": map[string]interface{}{ 246 | "name": "Test Customer", 247 | "email": "[email protected]", 248 | "contact": "+919876543210", 249 | }, 250 | "notes": map[string]interface{}{ 251 | "policy_name": "Test Policy", 252 | "user_id": "usr_123", 253 | }, 254 | } 255 | 256 | errorResp := map[string]interface{}{ 257 | "error": map[string]interface{}{ 258 | "code": "BAD_REQUEST_ERROR", 259 | "description": "API error: Something went wrong", 260 | }, 261 | } 262 | 263 | tests := []RazorpayToolTestCase{ 264 | { 265 | Name: "UPI payment link with all parameters", 266 | Request: map[string]interface{}{ 267 | "amount": float64(50000), 268 | "currency": "INR", 269 | "description": "Test UPI payment with all params", 270 | "reference_id": "REF12345", 271 | "accept_partial": true, 272 | "first_min_partial_amount": float64(10000), 273 | "expire_by": float64(1718196584), 274 | "customer_name": "Test Customer", 275 | "customer_email": "[email protected]", 276 | "customer_contact": "+919876543210", 277 | "notify_sms": true, 278 | "notify_email": true, 279 | "reminder_enable": true, 280 | "notes": map[string]interface{}{ 281 | "policy_name": "Test Policy", 282 | "user_id": "usr_123", 283 | }, 284 | "callback_url": "https://example.com/callback", 285 | "callback_method": "get", 286 | }, 287 | MockHttpClient: func() (*http.Client, *httptest.Server) { 288 | return mock.NewHTTPClient( 289 | mock.Endpoint{ 290 | Path: createPaymentLinkPath, 291 | Method: "POST", 292 | Response: upiPaymentLinkWithAllParamsResp, 293 | }, 294 | ) 295 | }, 296 | ExpectError: false, 297 | ExpectedResult: upiPaymentLinkWithAllParamsResp, 298 | }, 299 | { 300 | Name: "missing amount parameter", 301 | Request: map[string]interface{}{}, 302 | MockHttpClient: nil, // No HTTP client needed for validation error 303 | ExpectError: true, 304 | ExpectedErrMsg: "missing required parameter: amount", 305 | }, 306 | { 307 | Name: "UPI payment link creation fails", 308 | Request: map[string]interface{}{ 309 | "amount": float64(50000), 310 | }, 311 | MockHttpClient: func() (*http.Client, *httptest.Server) { 312 | return mock.NewHTTPClient( 313 | mock.Endpoint{ 314 | Path: createPaymentLinkPath, 315 | Method: "POST", 316 | Response: errorResp, 317 | }, 318 | ) 319 | }, 320 | ExpectError: true, 321 | ExpectedErrMsg: "missing required parameter: currency", 322 | }, 323 | } 324 | 325 | for _, tc := range tests { 326 | t.Run(tc.Name, func(t *testing.T) { 327 | runToolTest(t, tc, CreateUpiPaymentLink, "UPI Payment Link") 328 | }) 329 | } 330 | } 331 | 332 | func Test_ResendPaymentLinkNotification(t *testing.T) { 333 | notifyPaymentLinkPathFmt := fmt.Sprintf( 334 | "/%s%s/%%s/notify_by/%%s", 335 | constants.VERSION_V1, 336 | constants.PaymentLink_URL, 337 | ) 338 | 339 | successResponse := map[string]interface{}{ 340 | "success": true, 341 | } 342 | 343 | invalidMediumErrorResp := map[string]interface{}{ 344 | "error": map[string]interface{}{ 345 | "code": "BAD_REQUEST_ERROR", 346 | "description": "not a valid notification medium", 347 | }, 348 | } 349 | 350 | tests := []RazorpayToolTestCase{ 351 | { 352 | Name: "successful SMS notification", 353 | Request: map[string]interface{}{ 354 | "payment_link_id": "plink_ExjpAUN3gVHrPJ", 355 | "medium": "sms", 356 | }, 357 | MockHttpClient: func() (*http.Client, *httptest.Server) { 358 | return mock.NewHTTPClient( 359 | mock.Endpoint{ 360 | Path: fmt.Sprintf( 361 | notifyPaymentLinkPathFmt, 362 | "plink_ExjpAUN3gVHrPJ", 363 | "sms", 364 | ), 365 | Method: "POST", 366 | Response: successResponse, 367 | }, 368 | ) 369 | }, 370 | ExpectError: false, 371 | ExpectedResult: successResponse, 372 | }, 373 | { 374 | Name: "missing payment_link_id parameter", 375 | Request: map[string]interface{}{ 376 | "medium": "sms", 377 | }, 378 | MockHttpClient: nil, // No HTTP client needed for validation error 379 | ExpectError: true, 380 | ExpectedErrMsg: "missing required parameter: payment_link_id", 381 | }, 382 | { 383 | Name: "missing medium parameter", 384 | Request: map[string]interface{}{ 385 | "payment_link_id": "plink_ExjpAUN3gVHrPJ", 386 | }, 387 | MockHttpClient: nil, // No HTTP client needed for validation error 388 | ExpectError: true, 389 | ExpectedErrMsg: "missing required parameter: medium", 390 | }, 391 | { 392 | Name: "API error response", 393 | Request: map[string]interface{}{ 394 | "payment_link_id": "plink_Invalid", 395 | "medium": "sms", // Using valid medium so it passes validation 396 | }, 397 | MockHttpClient: func() (*http.Client, *httptest.Server) { 398 | return mock.NewHTTPClient( 399 | mock.Endpoint{ 400 | Path: fmt.Sprintf( 401 | notifyPaymentLinkPathFmt, 402 | "plink_Invalid", 403 | "sms", 404 | ), 405 | Method: "POST", 406 | Response: invalidMediumErrorResp, 407 | }, 408 | ) 409 | }, 410 | ExpectError: true, 411 | ExpectedErrMsg: "sending notification failed: " + 412 | "not a valid notification medium", 413 | }, 414 | } 415 | 416 | for _, tc := range tests { 417 | t.Run(tc.Name, func(t *testing.T) { 418 | toolFunc := ResendPaymentLinkNotification 419 | runToolTest(t, tc, toolFunc, "Payment Link Notification") 420 | }) 421 | } 422 | } 423 | 424 | func Test_UpdatePaymentLink(t *testing.T) { 425 | updatePaymentLinkPathFmt := fmt.Sprintf( 426 | "/%s%s/%%s", 427 | constants.VERSION_V1, 428 | constants.PaymentLink_URL, 429 | ) 430 | 431 | updatedPaymentLinkResp := map[string]interface{}{ 432 | "id": "plink_FL5HCrWEO112OW", 433 | "amount": float64(1000), 434 | "currency": "INR", 435 | "status": "created", 436 | "reference_id": "TS35", 437 | "expire_by": float64(1612092283), 438 | "reminder_enable": false, 439 | "notes": []interface{}{ 440 | map[string]interface{}{ 441 | "key": "policy_name", 442 | "value": "Jeevan Saral", 443 | }, 444 | }, 445 | } 446 | 447 | invalidStateResp := map[string]interface{}{ 448 | "error": map[string]interface{}{ 449 | "code": "BAD_REQUEST_ERROR", 450 | "description": "update can only be made in created or partially paid state", 451 | }, 452 | } 453 | 454 | tests := []RazorpayToolTestCase{ 455 | { 456 | Name: "successful update with multiple fields", 457 | Request: map[string]interface{}{ 458 | "payment_link_id": "plink_FL5HCrWEO112OW", 459 | "reference_id": "TS35", 460 | "expire_by": float64(1612092283), 461 | "reminder_enable": false, 462 | "accept_partial": true, 463 | "notes": map[string]interface{}{ 464 | "policy_name": "Jeevan Saral", 465 | }, 466 | }, 467 | MockHttpClient: func() (*http.Client, *httptest.Server) { 468 | return mock.NewHTTPClient( 469 | mock.Endpoint{ 470 | Path: fmt.Sprintf( 471 | updatePaymentLinkPathFmt, 472 | "plink_FL5HCrWEO112OW", 473 | ), 474 | Method: "PATCH", 475 | Response: updatedPaymentLinkResp, 476 | }, 477 | ) 478 | }, 479 | ExpectError: false, 480 | ExpectedResult: updatedPaymentLinkResp, 481 | }, 482 | { 483 | Name: "successful update with single field", 484 | Request: map[string]interface{}{ 485 | "payment_link_id": "plink_FL5HCrWEO112OW", 486 | "reference_id": "TS35", 487 | }, 488 | MockHttpClient: func() (*http.Client, *httptest.Server) { 489 | return mock.NewHTTPClient( 490 | mock.Endpoint{ 491 | Path: fmt.Sprintf( 492 | updatePaymentLinkPathFmt, 493 | "plink_FL5HCrWEO112OW", 494 | ), 495 | Method: "PATCH", 496 | Response: updatedPaymentLinkResp, 497 | }, 498 | ) 499 | }, 500 | ExpectError: false, 501 | ExpectedResult: updatedPaymentLinkResp, 502 | }, 503 | { 504 | Name: "missing payment_link_id parameter", 505 | Request: map[string]interface{}{ 506 | "reference_id": "TS35", 507 | }, 508 | MockHttpClient: nil, // No HTTP client needed for validation error 509 | ExpectError: true, 510 | ExpectedErrMsg: "missing required parameter: payment_link_id", 511 | }, 512 | { 513 | Name: "no update fields provided", 514 | Request: map[string]interface{}{ 515 | "payment_link_id": "plink_FL5HCrWEO112OW", 516 | }, 517 | MockHttpClient: nil, // No HTTP client needed for validation error 518 | ExpectError: true, 519 | ExpectedErrMsg: "at least one field to update must be provided", 520 | }, 521 | { 522 | Name: "payment link in invalid state", 523 | Request: map[string]interface{}{ 524 | "payment_link_id": "plink_Paid", 525 | "reference_id": "TS35", 526 | }, 527 | MockHttpClient: func() (*http.Client, *httptest.Server) { 528 | return mock.NewHTTPClient( 529 | mock.Endpoint{ 530 | Path: fmt.Sprintf( 531 | updatePaymentLinkPathFmt, 532 | "plink_Paid", 533 | ), 534 | Method: "PATCH", 535 | Response: invalidStateResp, 536 | }, 537 | ) 538 | }, 539 | ExpectError: true, 540 | ExpectedErrMsg: "updating payment link failed: update can only be made in " + 541 | "created or partially paid state", 542 | }, 543 | { 544 | Name: "update with explicit false value", 545 | Request: map[string]interface{}{ 546 | "payment_link_id": "plink_FL5HCrWEO112OW", 547 | "reminder_enable": false, // Explicitly set to false 548 | }, 549 | MockHttpClient: func() (*http.Client, *httptest.Server) { 550 | return mock.NewHTTPClient( 551 | mock.Endpoint{ 552 | Path: fmt.Sprintf( 553 | updatePaymentLinkPathFmt, 554 | "plink_FL5HCrWEO112OW", 555 | ), 556 | Method: "PATCH", 557 | Response: updatedPaymentLinkResp, 558 | }, 559 | ) 560 | }, 561 | ExpectError: false, 562 | ExpectedResult: updatedPaymentLinkResp, 563 | }, 564 | } 565 | 566 | for _, tc := range tests { 567 | t.Run(tc.Name, func(t *testing.T) { 568 | toolFunc := UpdatePaymentLink 569 | runToolTest(t, tc, toolFunc, "Payment Link Update") 570 | }) 571 | } 572 | } 573 | 574 | func Test_FetchAllPaymentLinks(t *testing.T) { 575 | fetchAllPaymentLinksPath := fmt.Sprintf( 576 | "/%s%s", 577 | constants.VERSION_V1, 578 | constants.PaymentLink_URL, 579 | ) 580 | 581 | allPaymentLinksResp := map[string]interface{}{ 582 | "payment_links": []interface{}{ 583 | map[string]interface{}{ 584 | "id": "plink_KBnb7I424Rc1R9", 585 | "amount": float64(10000), 586 | "currency": "INR", 587 | "status": "paid", 588 | "description": "Grocery", 589 | "reference_id": "111", 590 | "short_url": "https://rzp.io/i/alaBxs0i", 591 | "upi_link": false, 592 | }, 593 | map[string]interface{}{ 594 | "id": "plink_JP6yOUDCuHgcrl", 595 | "amount": float64(10000), 596 | "currency": "INR", 597 | "status": "paid", 598 | "description": "Online Tutoring - 1 Month", 599 | "reference_id": "11212", 600 | "short_url": "https://rzp.io/i/0ioYuawFu", 601 | "upi_link": false, 602 | }, 603 | }, 604 | } 605 | 606 | errorResp := map[string]interface{}{ 607 | "error": map[string]interface{}{ 608 | "code": "BAD_REQUEST_ERROR", 609 | "description": "The api key/secret provided is invalid", 610 | }, 611 | } 612 | 613 | tests := []RazorpayToolTestCase{ 614 | { 615 | Name: "fetch all payment links", 616 | Request: map[string]interface{}{}, 617 | MockHttpClient: func() (*http.Client, *httptest.Server) { 618 | return mock.NewHTTPClient( 619 | mock.Endpoint{ 620 | Path: fetchAllPaymentLinksPath, 621 | Method: "GET", 622 | Response: allPaymentLinksResp, 623 | }, 624 | ) 625 | }, 626 | ExpectError: false, 627 | ExpectedResult: allPaymentLinksResp, 628 | }, 629 | { 630 | Name: "api error", 631 | Request: map[string]interface{}{}, 632 | MockHttpClient: func() (*http.Client, *httptest.Server) { 633 | return mock.NewHTTPClient( 634 | mock.Endpoint{ 635 | Path: fetchAllPaymentLinksPath, 636 | Method: "GET", 637 | Response: errorResp, 638 | }, 639 | ) 640 | }, 641 | ExpectError: true, 642 | ExpectedErrMsg: "fetching payment links failed: The api key/secret provided is invalid", // nolint:lll 643 | }, 644 | } 645 | 646 | for _, tc := range tests { 647 | t.Run(tc.Name, func(t *testing.T) { 648 | toolFunc := FetchAllPaymentLinks 649 | runToolTest(t, tc, toolFunc, "Payment Links") 650 | }) 651 | } 652 | } 653 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/payment_links.go: -------------------------------------------------------------------------------- ```go 1 | package razorpay 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | rzpsdk "github.com/razorpay/razorpay-go" 8 | 9 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" 10 | "github.com/razorpay/razorpay-mcp-server/pkg/observability" 11 | ) 12 | 13 | // CreatePaymentLink returns a tool that creates payment links in Razorpay 14 | func CreatePaymentLink( 15 | obs *observability.Observability, 16 | client *rzpsdk.Client, 17 | ) mcpgo.Tool { 18 | parameters := []mcpgo.ToolParameter{ 19 | mcpgo.WithNumber( 20 | "amount", 21 | mcpgo.Description("Amount to be paid using the link in smallest "+ 22 | "currency unit(e.g., ₹300, use 30000)"), 23 | mcpgo.Required(), 24 | mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency) 25 | ), 26 | mcpgo.WithString( 27 | "currency", 28 | mcpgo.Description("Three-letter ISO code for the currency (e.g., INR)"), 29 | mcpgo.Required(), 30 | ), 31 | mcpgo.WithString( 32 | "description", 33 | mcpgo.Description("A brief description of the Payment Link explaining the intent of the payment."), // nolint:lll 34 | ), 35 | mcpgo.WithBoolean( 36 | "accept_partial", 37 | mcpgo.Description("Indicates whether customers can make partial payments using the Payment Link. Default: false"), // nolint:lll 38 | ), 39 | mcpgo.WithNumber( 40 | "first_min_partial_amount", 41 | mcpgo.Description("Minimum amount that must be paid by the customer as the first partial payment. Default value is 100."), // nolint:lll 42 | ), 43 | mcpgo.WithNumber( 44 | "expire_by", 45 | mcpgo.Description("Timestamp, in Unix, when the Payment Link will expire. By default, a Payment Link will be valid for six months."), // nolint:lll 46 | ), 47 | mcpgo.WithString( 48 | "reference_id", 49 | mcpgo.Description("Reference number tagged to a Payment Link. Must be unique for each Payment Link. Max 40 characters."), // nolint:lll 50 | ), 51 | mcpgo.WithString( 52 | "customer_name", 53 | mcpgo.Description("Name of the customer."), 54 | ), 55 | mcpgo.WithString( 56 | "customer_email", 57 | mcpgo.Description("Email address of the customer."), 58 | ), 59 | mcpgo.WithString( 60 | "customer_contact", 61 | mcpgo.Description("Contact number of the customer."), 62 | ), 63 | mcpgo.WithBoolean( 64 | "notify_sms", 65 | mcpgo.Description("Send SMS notifications for the Payment Link."), 66 | ), 67 | mcpgo.WithBoolean( 68 | "notify_email", 69 | mcpgo.Description("Send email notifications for the Payment Link."), 70 | ), 71 | mcpgo.WithBoolean( 72 | "reminder_enable", 73 | mcpgo.Description("Enable payment reminders for the Payment Link."), 74 | ), 75 | mcpgo.WithObject( 76 | "notes", 77 | mcpgo.Description("Key-value pairs that can be used to store additional information. Maximum 15 pairs, each value limited to 256 characters."), // nolint:lll 78 | ), 79 | mcpgo.WithString( 80 | "callback_url", 81 | mcpgo.Description("If specified, adds a redirect URL to the Payment Link. Customer will be redirected here after payment."), // nolint:lll 82 | ), 83 | mcpgo.WithString( 84 | "callback_method", 85 | mcpgo.Description("HTTP method for callback redirection. "+ 86 | "Must be 'get' if callback_url is set."), 87 | ), 88 | } 89 | 90 | handler := func( 91 | ctx context.Context, 92 | r mcpgo.CallToolRequest, 93 | ) (*mcpgo.ToolResult, error) { 94 | // Get client from context or use default 95 | client, err := getClientFromContextOrDefault(ctx, client) 96 | if err != nil { 97 | return mcpgo.NewToolResultError(err.Error()), nil 98 | } 99 | 100 | // Create a parameters map to collect validated parameters 101 | plCreateReq := make(map[string]interface{}) 102 | customer := make(map[string]interface{}) 103 | notify := make(map[string]interface{}) 104 | // Validate all parameters with fluent validator 105 | validator := NewValidator(&r). 106 | ValidateAndAddRequiredInt(plCreateReq, "amount"). 107 | ValidateAndAddRequiredString(plCreateReq, "currency"). 108 | ValidateAndAddOptionalString(plCreateReq, "description"). 109 | ValidateAndAddOptionalBool(plCreateReq, "accept_partial"). 110 | ValidateAndAddOptionalInt(plCreateReq, "first_min_partial_amount"). 111 | ValidateAndAddOptionalInt(plCreateReq, "expire_by"). 112 | ValidateAndAddOptionalString(plCreateReq, "reference_id"). 113 | ValidateAndAddOptionalStringToPath(customer, "customer_name", "name"). 114 | ValidateAndAddOptionalStringToPath(customer, "customer_email", "email"). 115 | ValidateAndAddOptionalStringToPath(customer, "customer_contact", "contact"). 116 | ValidateAndAddOptionalBoolToPath(notify, "notify_sms", "sms"). 117 | ValidateAndAddOptionalBoolToPath(notify, "notify_email", "email"). 118 | ValidateAndAddOptionalBool(plCreateReq, "reminder_enable"). 119 | ValidateAndAddOptionalMap(plCreateReq, "notes"). 120 | ValidateAndAddOptionalString(plCreateReq, "callback_url"). 121 | ValidateAndAddOptionalString(plCreateReq, "callback_method") 122 | 123 | if result, err := validator.HandleErrorsIfAny(); result != nil { 124 | return result, err 125 | } 126 | 127 | // Handle customer details 128 | if len(customer) > 0 { 129 | plCreateReq["customer"] = customer 130 | } 131 | 132 | // Handle notification settings 133 | if len(notify) > 0 { 134 | plCreateReq["notify"] = notify 135 | } 136 | 137 | // Create the payment link 138 | paymentLink, err := client.PaymentLink.Create(plCreateReq, nil) 139 | if err != nil { 140 | return mcpgo.NewToolResultError( 141 | fmt.Sprintf("creating payment link failed: %s", err.Error())), nil 142 | } 143 | 144 | return mcpgo.NewToolResultJSON(paymentLink) 145 | } 146 | 147 | return mcpgo.NewTool( 148 | "create_payment_link", 149 | "Create a new standard payment link in Razorpay with a specified amount", 150 | parameters, 151 | handler, 152 | ) 153 | } 154 | 155 | // CreateUpiPaymentLink returns a tool that creates payment links in Razorpay 156 | func CreateUpiPaymentLink( 157 | obs *observability.Observability, 158 | client *rzpsdk.Client, 159 | ) mcpgo.Tool { 160 | parameters := []mcpgo.ToolParameter{ 161 | mcpgo.WithNumber( 162 | "amount", 163 | mcpgo.Description("Amount to be paid using the link in smallest currency unit(e.g., ₹300, use 30000), Only accepted currency is INR"), // nolint:lll 164 | mcpgo.Required(), 165 | mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency) 166 | ), 167 | mcpgo.WithString( 168 | "currency", 169 | mcpgo.Description("Three-letter ISO code for the currency (e.g., INR). UPI links are only supported in INR"), // nolint:lll 170 | mcpgo.Required(), 171 | ), 172 | mcpgo.WithString( 173 | "description", 174 | mcpgo.Description("A brief description of the Payment Link explaining the intent of the payment."), // nolint:lll 175 | ), 176 | mcpgo.WithBoolean( 177 | "accept_partial", 178 | mcpgo.Description("Indicates whether customers can make partial payments using the Payment Link. Default: false"), // nolint:lll 179 | ), 180 | mcpgo.WithNumber( 181 | "first_min_partial_amount", 182 | mcpgo.Description("Minimum amount that must be paid by the customer as the first partial payment. Default value is 100."), // nolint:lll 183 | ), 184 | mcpgo.WithNumber( 185 | "expire_by", 186 | mcpgo.Description("Timestamp, in Unix, when the Payment Link will expire. By default, a Payment Link will be valid for six months."), // nolint:lll 187 | ), 188 | mcpgo.WithString( 189 | "reference_id", 190 | mcpgo.Description("Reference number tagged to a Payment Link. Must be unique for each Payment Link. Max 40 characters."), // nolint:lll 191 | ), 192 | mcpgo.WithString( 193 | "customer_name", 194 | mcpgo.Description("Name of the customer."), 195 | ), 196 | mcpgo.WithString( 197 | "customer_email", 198 | mcpgo.Description("Email address of the customer."), 199 | ), 200 | mcpgo.WithString( 201 | "customer_contact", 202 | mcpgo.Description("Contact number of the customer."), 203 | ), 204 | mcpgo.WithBoolean( 205 | "notify_sms", 206 | mcpgo.Description("Send SMS notifications for the Payment Link."), 207 | ), 208 | mcpgo.WithBoolean( 209 | "notify_email", 210 | mcpgo.Description("Send email notifications for the Payment Link."), 211 | ), 212 | mcpgo.WithBoolean( 213 | "reminder_enable", 214 | mcpgo.Description("Enable payment reminders for the Payment Link."), 215 | ), 216 | mcpgo.WithObject( 217 | "notes", 218 | mcpgo.Description("Key-value pairs that can be used to store additional information. Maximum 15 pairs, each value limited to 256 characters."), // nolint:lll 219 | ), 220 | mcpgo.WithString( 221 | "callback_url", 222 | mcpgo.Description("If specified, adds a redirect URL to the Payment Link. Customer will be redirected here after payment."), // nolint:lll 223 | ), 224 | mcpgo.WithString( 225 | "callback_method", 226 | mcpgo.Description("HTTP method for callback redirection. "+ 227 | "Must be 'get' if callback_url is set."), 228 | ), 229 | } 230 | 231 | handler := func( 232 | ctx context.Context, 233 | r mcpgo.CallToolRequest, 234 | ) (*mcpgo.ToolResult, error) { 235 | // Create a parameters map to collect validated parameters 236 | upiPlCreateReq := make(map[string]interface{}) 237 | customer := make(map[string]interface{}) 238 | notify := make(map[string]interface{}) 239 | // Validate all parameters with fluent validator 240 | validator := NewValidator(&r). 241 | ValidateAndAddRequiredInt(upiPlCreateReq, "amount"). 242 | ValidateAndAddRequiredString(upiPlCreateReq, "currency"). 243 | ValidateAndAddOptionalString(upiPlCreateReq, "description"). 244 | ValidateAndAddOptionalBool(upiPlCreateReq, "accept_partial"). 245 | ValidateAndAddOptionalInt(upiPlCreateReq, "first_min_partial_amount"). 246 | ValidateAndAddOptionalInt(upiPlCreateReq, "expire_by"). 247 | ValidateAndAddOptionalString(upiPlCreateReq, "reference_id"). 248 | ValidateAndAddOptionalStringToPath(customer, "customer_name", "name"). 249 | ValidateAndAddOptionalStringToPath(customer, "customer_email", "email"). 250 | ValidateAndAddOptionalStringToPath(customer, "customer_contact", "contact"). 251 | ValidateAndAddOptionalBoolToPath(notify, "notify_sms", "sms"). 252 | ValidateAndAddOptionalBoolToPath(notify, "notify_email", "email"). 253 | ValidateAndAddOptionalBool(upiPlCreateReq, "reminder_enable"). 254 | ValidateAndAddOptionalMap(upiPlCreateReq, "notes"). 255 | ValidateAndAddOptionalString(upiPlCreateReq, "callback_url"). 256 | ValidateAndAddOptionalString(upiPlCreateReq, "callback_method") 257 | 258 | if result, err := validator.HandleErrorsIfAny(); result != nil { 259 | return result, err 260 | } 261 | 262 | // Add the required UPI payment link parameters 263 | upiPlCreateReq["upi_link"] = "true" 264 | 265 | // Handle customer details 266 | if len(customer) > 0 { 267 | upiPlCreateReq["customer"] = customer 268 | } 269 | 270 | // Handle notification settings 271 | if len(notify) > 0 { 272 | upiPlCreateReq["notify"] = notify 273 | } 274 | 275 | client, err := getClientFromContextOrDefault(ctx, client) 276 | if err != nil { 277 | return mcpgo.NewToolResultError(err.Error()), nil 278 | } 279 | 280 | // Create the payment link 281 | paymentLink, err := client.PaymentLink.Create(upiPlCreateReq, nil) 282 | if err != nil { 283 | return mcpgo.NewToolResultError( 284 | fmt.Sprintf("upi pl create failed: %s", err.Error())), nil 285 | } 286 | 287 | return mcpgo.NewToolResultJSON(paymentLink) 288 | } 289 | 290 | return mcpgo.NewTool( 291 | "payment_link_upi_create", 292 | "Create a new UPI payment link in Razorpay with a specified amount and additional options.", // nolint:lll 293 | parameters, 294 | handler, 295 | ) 296 | } 297 | 298 | // FetchPaymentLink returns a tool that fetches payment link details using 299 | // payment_link_id 300 | func FetchPaymentLink( 301 | obs *observability.Observability, 302 | client *rzpsdk.Client, 303 | ) mcpgo.Tool { 304 | parameters := []mcpgo.ToolParameter{ 305 | mcpgo.WithString( 306 | "payment_link_id", 307 | mcpgo.Description("ID of the payment link to be fetched"+ 308 | "(ID should have a plink_ prefix)."), 309 | mcpgo.Required(), 310 | ), 311 | } 312 | 313 | handler := func( 314 | ctx context.Context, 315 | r mcpgo.CallToolRequest, 316 | ) (*mcpgo.ToolResult, error) { 317 | // Get client from context or use default 318 | client, err := getClientFromContextOrDefault(ctx, client) 319 | if err != nil { 320 | return mcpgo.NewToolResultError(err.Error()), nil 321 | } 322 | 323 | fields := make(map[string]interface{}) 324 | 325 | validator := NewValidator(&r). 326 | ValidateAndAddRequiredString(fields, "payment_link_id") 327 | 328 | if result, err := validator.HandleErrorsIfAny(); result != nil { 329 | return result, err 330 | } 331 | 332 | paymentLinkId := fields["payment_link_id"].(string) 333 | 334 | paymentLink, err := client.PaymentLink.Fetch(paymentLinkId, nil, nil) 335 | if err != nil { 336 | return mcpgo.NewToolResultError( 337 | fmt.Sprintf("fetching payment link failed: %s", err.Error())), nil 338 | } 339 | 340 | return mcpgo.NewToolResultJSON(paymentLink) 341 | } 342 | 343 | return mcpgo.NewTool( 344 | "fetch_payment_link", 345 | "Fetch payment link details using it's ID. "+ 346 | "Response contains the basic details like amount, status etc. "+ 347 | "The link could be of any type(standard or UPI)", 348 | parameters, 349 | handler, 350 | ) 351 | } 352 | 353 | // ResendPaymentLinkNotification returns a tool that sends/resends notifications 354 | // for a payment link via email or SMS 355 | func ResendPaymentLinkNotification( 356 | obs *observability.Observability, 357 | client *rzpsdk.Client, 358 | ) mcpgo.Tool { 359 | parameters := []mcpgo.ToolParameter{ 360 | mcpgo.WithString( 361 | "payment_link_id", 362 | mcpgo.Description("ID of the payment link for which to send notification "+ 363 | "(ID should have a plink_ prefix)."), // nolint:lll 364 | mcpgo.Required(), 365 | ), 366 | mcpgo.WithString( 367 | "medium", 368 | mcpgo.Description("Medium through which to send the notification. "+ 369 | "Must be either 'sms' or 'email'."), // nolint:lll 370 | mcpgo.Required(), 371 | mcpgo.Enum("sms", "email"), 372 | ), 373 | } 374 | 375 | handler := func( 376 | ctx context.Context, 377 | r mcpgo.CallToolRequest, 378 | ) (*mcpgo.ToolResult, error) { 379 | client, err := getClientFromContextOrDefault(ctx, client) 380 | if err != nil { 381 | return mcpgo.NewToolResultError(err.Error()), nil 382 | } 383 | 384 | fields := make(map[string]interface{}) 385 | 386 | validator := NewValidator(&r). 387 | ValidateAndAddRequiredString(fields, "payment_link_id"). 388 | ValidateAndAddRequiredString(fields, "medium") 389 | 390 | if result, err := validator.HandleErrorsIfAny(); result != nil { 391 | return result, err 392 | } 393 | 394 | paymentLinkId := fields["payment_link_id"].(string) 395 | medium := fields["medium"].(string) 396 | 397 | // Call the SDK function 398 | response, err := client.PaymentLink.NotifyBy(paymentLinkId, medium, nil, nil) 399 | if err != nil { 400 | return mcpgo.NewToolResultError( 401 | fmt.Sprintf("sending notification failed: %s", err.Error())), nil 402 | } 403 | 404 | return mcpgo.NewToolResultJSON(response) 405 | } 406 | 407 | return mcpgo.NewTool( 408 | "payment_link_notify", 409 | "Send or resend notification for a payment link via SMS or email.", // nolint:lll 410 | parameters, 411 | handler, 412 | ) 413 | } 414 | 415 | // UpdatePaymentLink returns a tool that updates an existing payment link 416 | func UpdatePaymentLink( 417 | obs *observability.Observability, 418 | client *rzpsdk.Client, 419 | ) mcpgo.Tool { 420 | parameters := []mcpgo.ToolParameter{ 421 | mcpgo.WithString( 422 | "payment_link_id", 423 | mcpgo.Description("ID of the payment link to update "+ 424 | "(ID should have a plink_ prefix)."), 425 | mcpgo.Required(), 426 | ), 427 | mcpgo.WithString( 428 | "reference_id", 429 | mcpgo.Description("Adds a unique reference number to the payment link."), 430 | ), 431 | mcpgo.WithNumber( 432 | "expire_by", 433 | mcpgo.Description("Timestamp, in Unix format, when the payment link "+ 434 | "should expire."), 435 | ), 436 | mcpgo.WithBoolean( 437 | "reminder_enable", 438 | mcpgo.Description("Enable or disable reminders for the payment link."), 439 | ), 440 | mcpgo.WithBoolean( 441 | "accept_partial", 442 | mcpgo.Description("Allow customers to make partial payments. "+ 443 | "Not allowed with UPI payment links."), 444 | ), 445 | mcpgo.WithObject( 446 | "notes", 447 | mcpgo.Description("Key-value pairs for additional information. "+ 448 | "Maximum 15 pairs, each value limited to 256 characters."), 449 | ), 450 | } 451 | 452 | handler := func( 453 | ctx context.Context, 454 | r mcpgo.CallToolRequest, 455 | ) (*mcpgo.ToolResult, error) { 456 | client, err := getClientFromContextOrDefault(ctx, client) 457 | if err != nil { 458 | return mcpgo.NewToolResultError(err.Error()), nil 459 | } 460 | 461 | plUpdateReq := make(map[string]interface{}) 462 | otherFields := make(map[string]interface{}) 463 | 464 | validator := NewValidator(&r). 465 | ValidateAndAddRequiredString(otherFields, "payment_link_id"). 466 | ValidateAndAddOptionalString(plUpdateReq, "reference_id"). 467 | ValidateAndAddOptionalInt(plUpdateReq, "expire_by"). 468 | ValidateAndAddOptionalBool(plUpdateReq, "reminder_enable"). 469 | ValidateAndAddOptionalBool(plUpdateReq, "accept_partial"). 470 | ValidateAndAddOptionalMap(plUpdateReq, "notes") 471 | 472 | if result, err := validator.HandleErrorsIfAny(); result != nil { 473 | return result, err 474 | } 475 | 476 | paymentLinkId := otherFields["payment_link_id"].(string) 477 | 478 | // Ensure we have at least one field to update 479 | if len(plUpdateReq) == 0 { 480 | return mcpgo.NewToolResultError( 481 | "at least one field to update must be provided"), nil 482 | } 483 | 484 | // Call the SDK function 485 | paymentLink, err := client.PaymentLink.Update(paymentLinkId, plUpdateReq, nil) 486 | if err != nil { 487 | return mcpgo.NewToolResultError( 488 | fmt.Sprintf("updating payment link failed: %s", err.Error())), nil 489 | } 490 | 491 | return mcpgo.NewToolResultJSON(paymentLink) 492 | } 493 | 494 | return mcpgo.NewTool( 495 | "update_payment_link", 496 | "Update any existing standard or UPI payment link with new details such as reference ID, "+ // nolint:lll 497 | "expiry date, or notes.", 498 | parameters, 499 | handler, 500 | ) 501 | } 502 | 503 | // FetchAllPaymentLinks returns a tool that fetches all payment links 504 | // with optional filtering 505 | func FetchAllPaymentLinks( 506 | obs *observability.Observability, 507 | client *rzpsdk.Client, 508 | ) mcpgo.Tool { 509 | parameters := []mcpgo.ToolParameter{ 510 | mcpgo.WithString( 511 | "payment_id", 512 | mcpgo.Description("Optional: Filter by payment ID associated with payment links"), // nolint:lll 513 | ), 514 | mcpgo.WithString( 515 | "reference_id", 516 | mcpgo.Description("Optional: Filter by reference ID used when creating payment links"), // nolint:lll 517 | ), 518 | mcpgo.WithNumber( 519 | "upi_link", 520 | mcpgo.Description("Optional: Filter only upi links. "+ 521 | "Value should be 1 if you want only upi links, 0 for only standard links"+ 522 | "If not provided, all types of links will be returned"), 523 | ), 524 | } 525 | 526 | handler := func( 527 | ctx context.Context, 528 | r mcpgo.CallToolRequest, 529 | ) (*mcpgo.ToolResult, error) { 530 | client, err := getClientFromContextOrDefault(ctx, client) 531 | if err != nil { 532 | return mcpgo.NewToolResultError(err.Error()), nil 533 | } 534 | 535 | plListReq := make(map[string]interface{}) 536 | 537 | validator := NewValidator(&r). 538 | ValidateAndAddOptionalString(plListReq, "payment_id"). 539 | ValidateAndAddOptionalString(plListReq, "reference_id"). 540 | ValidateAndAddOptionalInt(plListReq, "upi_link") 541 | 542 | if result, err := validator.HandleErrorsIfAny(); result != nil { 543 | return result, err 544 | } 545 | 546 | // Call the API directly using the Request object 547 | response, err := client.PaymentLink.All(plListReq, nil) 548 | if err != nil { 549 | return mcpgo.NewToolResultError( 550 | fmt.Sprintf("fetching payment links failed: %s", err.Error())), nil 551 | } 552 | 553 | return mcpgo.NewToolResultJSON(response) 554 | } 555 | 556 | return mcpgo.NewTool( 557 | "fetch_all_payment_links", 558 | "Fetch all payment links with optional filtering by payment ID or reference ID."+ // nolint:lll 559 | "You can specify the upi_link parameter to filter by link type.", 560 | parameters, 561 | handler, 562 | ) 563 | } 564 | ```