This is page 1 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 -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` 1 | .git/ 2 | .dockerignore 3 | .goreleaser.yaml 4 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | /dist 2 | /bin 3 | /.go 4 | /logs 5 | /vendor 6 | /razorpay-mcp-server 7 | /.idea ``` -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- ```yaml 1 | run: 2 | timeout: 5m 3 | tests: true 4 | concurrency: 4 5 | 6 | linters: 7 | disable-all: true 8 | enable: 9 | - errcheck 10 | - gosimple 11 | - govet 12 | - ineffassign 13 | - staticcheck 14 | - typecheck 15 | - unused 16 | - gocyclo 17 | - gosec 18 | - misspell 19 | - gofmt 20 | - goimports 21 | - revive 22 | - interfacebloat 23 | - iface 24 | - gocritic 25 | - bodyclose 26 | - makezero 27 | - lll 28 | 29 | linters-settings: 30 | gocyclo: 31 | min-complexity: 15 32 | dupl: 33 | threshold: 100 34 | goconst: 35 | min-len: 2 36 | min-occurrences: 2 37 | goimports: 38 | local-prefixes: github.com/razorpay/razorpay-mcp-server 39 | interfacebloat: 40 | max: 5 41 | iface: 42 | enable: 43 | - opaque 44 | - identical 45 | revive: 46 | rules: 47 | - name: blank-imports 48 | disabled: true 49 | lll: 50 | line-length: 80 51 | tab-width: 1 52 | 53 | output: 54 | formats: colored-line-number 55 | print-issued-lines: true 56 | print-linter-name: true 57 | ``` -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- ```yaml 1 | version: 2 2 | 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | 10 | builds: 11 | - env: 12 | - CGO_ENABLED=0 13 | ldflags: 14 | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} 15 | goos: 16 | - linux 17 | - windows 18 | - darwin 19 | main: ./cmd/razorpay-mcp-server 20 | 21 | archives: 22 | - formats: [tar.gz] 23 | # this name template makes the OS and Arch compatible with the results of `uname`. 24 | name_template: >- 25 | {{ .ProjectName }}_ 26 | {{- title .Os }}_ 27 | {{- if eq .Arch "amd64" }}x86_64 28 | {{- else if eq .Arch "386" }}i386 29 | {{- else }}{{ .Arch }}{{ end }} 30 | {{- if .Arm }}v{{ .Arm }}{{ end }} 31 | # use zip for windows archives 32 | format_overrides: 33 | - goos: windows 34 | formats: [zip] 35 | 36 | changelog: 37 | sort: asc 38 | filters: 39 | exclude: 40 | - "^docs:" 41 | - "^test:" 42 | 43 | release: 44 | draft: true 45 | prerelease: auto 46 | name_template: "Razorpay MCP Server {{.Version}}" 47 | ``` -------------------------------------------------------------------------------- /.cursorignore: -------------------------------------------------------------------------------- ``` 1 | # Distribution and Environment 2 | dist/* 3 | build/* 4 | venv/* 5 | env/* 6 | *.env 7 | .env.* 8 | virtualenv/* 9 | .python-version 10 | .ruby-version 11 | .node-version 12 | 13 | # Logs and Temporary Files 14 | *.log 15 | *.tsv 16 | *.csv 17 | *.txt 18 | tmp/* 19 | temp/* 20 | .tmp/* 21 | *.temp 22 | *.cache 23 | .cache/* 24 | logs/* 25 | 26 | # Sensitive Data 27 | *.sqlite 28 | *.sqlite3 29 | *.dbsql 30 | secrets.* 31 | .npmrc 32 | .yarnrc 33 | .aws/* 34 | .config/* 35 | 36 | # Credentials and Keys 37 | *.pem 38 | *.ppk 39 | *.key 40 | *.pub 41 | *.p12 42 | *.pfx 43 | *.htpasswd 44 | *.keystore 45 | *.jks 46 | *.truststore 47 | *.cer 48 | id_rsa* 49 | known_hosts 50 | authorized_keys 51 | .ssh/* 52 | .gnupg/* 53 | .pgpass 54 | 55 | # Config Files 56 | *.conf 57 | *.toml 58 | *.ini 59 | .env.local 60 | .env.development 61 | .env.test 62 | .env.production 63 | config/* 64 | 65 | # Database Files 66 | *.sql 67 | *.db 68 | *.dmp 69 | *.dump 70 | *.backup 71 | *.restore 72 | *.mdb 73 | *.accdb 74 | *.realm* 75 | 76 | # Backup and Archive Files 77 | *.bak 78 | *.backup 79 | *.swp 80 | *.swo 81 | *.swn 82 | *~ 83 | *.old 84 | *.orig 85 | *.archive 86 | *.gz 87 | *.zip 88 | *.tar 89 | *.rar 90 | *.7z 91 | 92 | # Compiled and Binary Files 93 | *.pyc 94 | *.pyo 95 | **/__pycache__/** 96 | *.class 97 | *.jar 98 | *.war 99 | *.ear 100 | *.dll 101 | *.exe 102 | *.so 103 | *.dylib 104 | *.bin 105 | *.obj 106 | 107 | # IDE and Editor Files 108 | .idea/* 109 | *.iml 110 | .vscode/* 111 | .project 112 | .classpath 113 | .settings/* 114 | *.sublime-* 115 | .atom/* 116 | .eclipse/* 117 | *.code-workspace 118 | .history/* 119 | 120 | # Build and Dependency Directories 121 | node_modules/* 122 | bower_components/* 123 | vendor/* 124 | packages/* 125 | jspm_packages/* 126 | .gradle/* 127 | target/* 128 | out/* 129 | 130 | # Testing and Coverage Files 131 | coverage/* 132 | .coverage 133 | htmlcov/* 134 | .pytest_cache/* 135 | .tox/* 136 | junit.xml 137 | test-results/* 138 | 139 | # Mobile Development 140 | *.apk 141 | *.aab 142 | *.ipa 143 | *.xcarchive 144 | *.provisionprofile 145 | google-services.json 146 | GoogleService-Info.plist 147 | 148 | # Certificate and Security Files 149 | *.crt 150 | *.csr 151 | *.ovpn 152 | *.p7b 153 | *.p7s 154 | *.pfx 155 | *.spc 156 | *.stl 157 | *.pem.crt 158 | ssl/* 159 | 160 | # Container and Infrastructure 161 | *.tfstate 162 | *.tfstate.backup 163 | .terraform/* 164 | .vagrant/* 165 | docker-compose.override.yml 166 | kubernetes/* 167 | 168 | # Design and Media Files (often large and binary) 169 | *.psd 170 | *.ai 171 | *.sketch 172 | *.fig 173 | *.xd 174 | assets/raw/* 175 | ``` -------------------------------------------------------------------------------- /pkg/mcpgo/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCPGO Package 2 | 3 | The `mcpgo` package provides an abstraction layer over the `github.com/mark3labs/mcp-go` library. Its purpose is to isolate this external dependency from the rest of the application by wrapping all necessary functionality within clean interfaces. 4 | 5 | ## Purpose 6 | 7 | This package was created to isolate the `mark3labs/mcp-go` dependency for several key reasons: 8 | 9 | 1. **Dependency Isolation**: Confine all `mark3labs/mcp-go` imports to this package, ensuring the rest of the application does not directly depend on this external library. 10 | 11 | 2. **Official MCP GO SDK and Future Compatibility**: Prepare for the eventual release of an official MCP SDK by creating a clean abstraction layer that can be updated to use the official SDK when it becomes available. The official SDK is currently under development (see [Official MCP Go SDK discussion](https://github.com/orgs/modelcontextprotocol/discussions/224#discussioncomment-12927030)). 12 | 13 | 3. **Simplified API**: Provide a more focused, application-specific API that only exposes the functionality needed by our application. 14 | 15 | 4. **Error Handling**: Implement proper error handling patterns rather than relying on panics, making the application more robust. 16 | 17 | ## Components 18 | 19 | The package contains several core components: 20 | 21 | - **Server**: An interface representing an MCP server, with the `mark3labsImpl` providing the current implementation. 22 | - **Tool**: Interface for defining MCP tools that can be registered with the server. 23 | - **TransportServer**: Interface for different transport mechanisms (stdio, TCP). 24 | - **ToolResult/ToolParameter**: Structures for handling tool calls and results. 25 | 26 | ## Parameter Helper Functions 27 | 28 | The package provides convenience functions for creating tool parameters: 29 | 30 | - `WithString(name, description string, required bool)`: Creates a string parameter 31 | - `WithNumber(name, description string, required bool)`: Creates a number parameter 32 | - `WithBoolean(name, description string, required bool)`: Creates a boolean parameter 33 | - `WithObject(name, description string, required bool)`: Creates an object parameter 34 | - `WithArray(name, description string, required bool)`: Creates an array parameter 35 | 36 | ## Tool Result Helper Functions 37 | 38 | The package also provides functions for creating tool results: 39 | 40 | - `NewToolResultText(text string)`: Creates a text result 41 | - `NewToolResultJSON(data interface{})`: Creates a JSON result 42 | - `NewToolResultError(text string)`: Creates an error result 43 | 44 | ## Usage Example 45 | 46 | ```go 47 | // Create a server 48 | server := mcpgo.NewServer( 49 | "my-server", 50 | "1.0.0", 51 | mcpgo.WithLogging(), 52 | mcpgo.WithToolCapabilities(true), 53 | ) 54 | 55 | // Create a tool 56 | tool := mcpgo.NewTool( 57 | "my_tool", 58 | "Description of my tool", 59 | []mcpgo.ToolParameter{ 60 | mcpgo.WithString( 61 | "param1", 62 | mcpgo.Description("Description of param1"), 63 | mcpgo.Required(), 64 | ), 65 | }, 66 | func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) { 67 | // Extract parameter value 68 | param1Value, ok := req.Arguments["param1"] 69 | if !ok { 70 | return mcpgo.NewToolResultError("Missing required parameter: param1"), nil 71 | } 72 | 73 | // Process and return result 74 | return mcpgo.NewToolResultText("Result: " + param1Value.(string)), nil 75 | }, 76 | ) 77 | 78 | // Add tool to server 79 | server.AddTools(tool) 80 | 81 | // Create and run a stdio server 82 | stdioServer, err := mcpgo.NewStdioServer(server) 83 | if err != nil { 84 | log.Fatalf("Failed to create stdio server: %v", err) 85 | } 86 | err = stdioServer.Listen(context.Background(), os.Stdin, os.Stdout) 87 | if err != nil { 88 | log.Fatalf("Server error: %v", err) 89 | } 90 | ``` 91 | 92 | ## Real-world Example 93 | 94 | Here's how we use this package in the Razorpay MCP server to create a payment fetching tool: 95 | 96 | ```go 97 | // FetchPayment returns a tool that fetches payment details using payment_id 98 | func FetchPayment( 99 | log *slog.Logger, 100 | client *rzpsdk.Client, 101 | ) mcpgo.Tool { 102 | parameters := []mcpgo.ToolParameter{ 103 | mcpgo.WithString( 104 | "payment_id", 105 | mcpgo.Description("payment_id is unique identifier of the payment to be retrieved."), 106 | mcpgo.Required(), 107 | ), 108 | } 109 | 110 | handler := func( 111 | ctx context.Context, 112 | r mcpgo.CallToolRequest, 113 | ) (*mcpgo.ToolResult, error) { 114 | arg, ok := r.Arguments["payment_id"] 115 | if !ok { 116 | return mcpgo.NewToolResultError( 117 | "payment id is a required field"), nil 118 | } 119 | id, ok := arg.(string) 120 | if !ok { 121 | return mcpgo.NewToolResultError( 122 | "payment id is expected to be a string"), nil 123 | } 124 | 125 | payment, err := client.Payment.Fetch(id, nil, nil) 126 | if err != nil { 127 | return mcpgo.NewToolResultError( 128 | fmt.Sprintf("fetching payment failed: %s", err.Error())), nil 129 | } 130 | 131 | return mcpgo.NewToolResultJSON(payment) 132 | } 133 | 134 | return mcpgo.NewTool( 135 | "fetch_payment", 136 | "fetch payment details using payment id.", 137 | parameters, 138 | handler, 139 | ) 140 | } 141 | ``` 142 | 143 | ## Design Principles 144 | 145 | 1. **Minimal Interface Exposure**: The interfaces defined in this package include only methods that are actually used by our application. 146 | 147 | 2. **Proper Error Handling**: Functions return errors instead of panicking, allowing for graceful error handling throughout the application. 148 | 149 | 3. **Implementation Hiding**: The implementation details using `mark3labs/mcp-go` are hidden behind clean interfaces, making future transitions easier. 150 | 151 | 4. **Naming Clarity**: All implementation types are prefixed with `mark3labs` to clearly indicate they are specifically tied to the current library being used. 152 | 153 | ## Directory Structure 154 | 155 | ``` 156 | pkg/mcpgo/ 157 | ├── server.go # Server interface and implementation 158 | ├── transport.go # TransportServer interface 159 | ├── stdio.go # StdioServer implementation 160 | ├── tool.go # Tool interfaces and implementation 161 | └── README.md # This file 162 | ``` ``` -------------------------------------------------------------------------------- /pkg/razorpay/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Razorpay MCP Server Tools 2 | 3 | This package contains tools for interacting with the Razorpay API via the Model Context Protocol (MCP). 4 | 5 | ## Creating New API Tools 6 | 7 | This guide explains how to add new Razorpay API tools to the MCP server. 8 | 9 | ### Quick Start 10 | 11 | 1. Locate the API documentation at https://razorpay.com/docs/api/ 12 | 2. Identify the equivalent function call for the API in the razorpay go sdk. 13 | 3. Create a new tool function in the appropriate file (or create a new file for a new resource type). Add validations for mandatory fields and call the sdk 14 | 5. Register the tool in `server.go` 15 | 6. Update "Available Tools" section in the main README.md 16 | 17 | ### Tool Structure 18 | 19 | Add the tool definition inside pkg/razorpay's resource file. You can define a new tool using this following template: 20 | 21 | ```go 22 | // ToolName returns a tool that [description of what it does] 23 | func ToolName( 24 | log *slog.Logger, 25 | client *rzpsdk.Client, 26 | ) mcpgo.Tool { 27 | parameters := []mcpgo.ToolParameter{ 28 | // Parameters defined here 29 | } 30 | 31 | handler := func( 32 | ctx context.Context, 33 | r mcpgo.CallToolRequest, 34 | ) (*mcpgo.ToolResult, error) { 35 | // Parameter validation 36 | // API call 37 | // Response handling 38 | return mcpgo.NewToolResultJSON(response) 39 | } 40 | 41 | return mcpgo.NewTool( 42 | "tool_name", 43 | "A description of the tool. NOTE: Add any exceptions/rules if relevant for the LLMs.", 44 | parameters, 45 | handler, 46 | ) 47 | } 48 | ``` 49 | 50 | Tool Naming Conventions: 51 | - Fetch methods: `fetch_resource` 52 | - Create methods: `create_resource` 53 | - FetchAll methods: `fetch_all_resources` 54 | 55 | ### Parameter Definition 56 | 57 | Define parameters using the mcpgo helpers. This would include the type, name, description of the parameter and also specifying if the parameter required or not. 58 | 59 | ```go 60 | // Required parameters 61 | mcpgo.WithString( 62 | "parameter_name", 63 | mcpgo.Description("Description of the parameter"), 64 | mcpgo.Required(), 65 | ) 66 | 67 | // Optional parameters 68 | mcpgo.WithNumber( 69 | "amount", 70 | mcpgo.Description("Amount in smallest currency unit"), 71 | ) 72 | ``` 73 | 74 | Available parameter types: 75 | - `WithString`: For string values 76 | - `WithNumber`: For numeric values 77 | - `WithBoolean`: For boolean values 78 | - `WithObject`: For nested objects 79 | 80 | ### Parameter Validation 81 | 82 | Inside the handler function, use the fluent validator pattern for parameter validation. This provides cleaner, more readable code through method chaining: 83 | 84 | ```go 85 | // Create a new validator 86 | v := NewValidator(&r) 87 | 88 | // Create a map for API request parameters 89 | payload := make(map[string]interface{}) 90 | 91 | // Validate and add parameters to the payload with method chaining 92 | v.ValidateAndAddRequiredString(payload, "id"). 93 | ValidateAndAddOptionalString(payload, "description"). 94 | ValidateAndAddRequiredInt(payload, "amount"). 95 | ValidateAndAddOptionalInt(payload, "limit") 96 | 97 | // Validate and add common parameters 98 | v.ValidateAndAddPagination(payload). 99 | ValidateAndAddExpand(payload) 100 | 101 | // Check for validation errors 102 | if result, err := validator.HandleErrorsIfAny(); result != nil { 103 | return result, err 104 | } 105 | 106 | // Proceed with API call using validated parameters in payload 107 | ``` 108 | 109 | ### Example: GET Endpoint 110 | 111 | ```go 112 | // FetchResource returns a tool that fetches a resource by ID 113 | func FetchResource( 114 | log *slog.Logger, 115 | client *rzpsdk.Client, 116 | ) mcpgo.Tool { 117 | parameters := []mcpgo.ToolParameter{ 118 | mcpgo.WithString( 119 | "id", 120 | mcpgo.Description("Unique identifier of the resource"), 121 | mcpgo.Required(), 122 | ), 123 | } 124 | 125 | handler := func( 126 | ctx context.Context, 127 | r mcpgo.CallToolRequest, 128 | ) (*mcpgo.ToolResult, error) { 129 | // Create validator and a payload map 130 | payload := make(map[string]interface{}) 131 | v := NewValidator(&r). 132 | ValidateAndAddRequiredString(payload, "id") 133 | 134 | // Check for validation errors 135 | if result, err := validator.HandleErrorsIfAny(); result != nil { 136 | return result, err 137 | } 138 | 139 | // Extract validated ID and make API call 140 | id := payload["id"].(string) 141 | resource, err := client.Resource.Fetch(id, nil, nil) 142 | if err != nil { 143 | return mcpgo.NewToolResultError( 144 | fmt.Sprintf("fetching resource failed: %s", err.Error())), nil 145 | } 146 | 147 | return mcpgo.NewToolResultJSON(resource) 148 | } 149 | 150 | return mcpgo.NewTool( 151 | "fetch_resource", 152 | "Fetch a resource from Razorpay by ID", 153 | parameters, 154 | handler, 155 | ) 156 | } 157 | ``` 158 | 159 | ### Example: POST Endpoint 160 | 161 | ```go 162 | // CreateResource returns a tool that creates a new resource 163 | func CreateResource( 164 | log *slog.Logger, 165 | client *rzpsdk.Client, 166 | ) mcpgo.Tool { 167 | parameters := []mcpgo.ToolParameter{ 168 | mcpgo.WithNumber( 169 | "amount", 170 | mcpgo.Description("Amount in smallest currency unit"), 171 | mcpgo.Required(), 172 | ), 173 | mcpgo.WithString( 174 | "currency", 175 | mcpgo.Description("Three-letter ISO code for the currency"), 176 | mcpgo.Required(), 177 | ), 178 | mcpgo.WithString( 179 | "description", 180 | mcpgo.Description("Brief description of the resource"), 181 | ), 182 | } 183 | 184 | handler := func( 185 | ctx context.Context, 186 | r mcpgo.CallToolRequest, 187 | ) (*mcpgo.ToolResult, error) { 188 | // Create payload map and validator 189 | data := make(map[string]interface{}) 190 | v := NewValidator(&r). 191 | ValidateAndAddRequiredInt(data, "amount"). 192 | ValidateAndAddRequiredString(data, "currency"). 193 | ValidateAndAddOptionalString(data, "description") 194 | 195 | // Check for validation errors 196 | if result, err := validator.HandleErrorsIfAny(); result != nil { 197 | return result, err 198 | } 199 | 200 | // Call the API with validated data 201 | resource, err := client.Resource.Create(data, nil) 202 | if err != nil { 203 | return mcpgo.NewToolResultError( 204 | fmt.Sprintf("creating resource failed: %s", err.Error())), nil 205 | } 206 | 207 | return mcpgo.NewToolResultJSON(resource) 208 | } 209 | 210 | return mcpgo.NewTool( 211 | "create_resource", 212 | "Create a new resource in Razorpay", 213 | parameters, 214 | handler, 215 | ) 216 | } 217 | ``` 218 | 219 | ### Registering Tools 220 | 221 | Add your tool to the appropriate toolset in the `NewToolSets` function in [`pkg/razorpay/tools.go`](tools.go): 222 | 223 | ```go 224 | // NewToolSets creates and configures all available toolsets 225 | func NewToolSets( 226 | log *slog.Logger, 227 | client *rzpsdk.Client, 228 | enabledToolsets []string, 229 | readOnly bool, 230 | ) (*toolsets.ToolsetGroup, error) { 231 | // Create a new toolset group 232 | toolsetGroup := toolsets.NewToolsetGroup(readOnly) 233 | 234 | // Create toolsets 235 | payments := toolsets.NewToolset("payments", "Razorpay Payments related tools"). 236 | AddReadTools( 237 | FetchPayment(log, client), 238 | // Add your read-only payment tool here 239 | ). 240 | AddWriteTools( 241 | // Add your write payment tool here 242 | ) 243 | 244 | paymentLinks := toolsets.NewToolset( 245 | "payment_links", 246 | "Razorpay Payment Links related tools"). 247 | AddReadTools( 248 | FetchPaymentLink(log, client), 249 | // Add your read-only payment link tool here 250 | ). 251 | AddWriteTools( 252 | CreatePaymentLink(log, client), 253 | // Add your write payment link tool here 254 | ) 255 | 256 | orders := toolsets.NewToolset("orders", "Razorpay Orders related tools"). 257 | AddReadTools( 258 | FetchOrder(log, client), 259 | // Add your read-only order tool here 260 | ). 261 | AddWriteTools( 262 | CreateOrder(log, client), 263 | // Add your write order tool here 264 | ) 265 | 266 | // If adding a new resource type, create a new toolset: 267 | /* 268 | newResource := toolsets.NewToolset("new_resource", "Razorpay New Resource related tools"). 269 | AddReadTools( 270 | FetchNewResource(log, client), 271 | ). 272 | AddWriteTools( 273 | CreateNewResource(log, client), 274 | ) 275 | toolsetGroup.AddToolset(newResource) 276 | */ 277 | 278 | // Add toolsets to the group 279 | toolsetGroup.AddToolset(payments) 280 | toolsetGroup.AddToolset(paymentLinks) 281 | toolsetGroup.AddToolset(orders) 282 | 283 | return toolsetGroup, nil 284 | } 285 | ``` 286 | 287 | Tools are organized into toolsets by resource type, and each toolset has separate collections for read-only tools (`AddReadTools`) and write tools (`AddWriteTools`). This allows the server to enable/disable write operations when in read-only mode. 288 | 289 | ### Writing Unit Tests 290 | 291 | All new tools should have unit tests to verify their behavior. We use a standard pattern for testing tools: 292 | 293 | ```go 294 | func Test_ToolName(t *testing.T) { 295 | // Define API path that needs to be mocked 296 | apiPathFmt := fmt.Sprintf( 297 | "/%s%s/%%s", 298 | constants.VERSION_V1, 299 | constants.PAYMENT_URL, 300 | ) 301 | 302 | // Define mock responses 303 | successResponse := map[string]interface{}{ 304 | "id": "resource_123", 305 | "amount": float64(1000), 306 | "currency": "INR", 307 | // Other expected fields 308 | } 309 | 310 | // Define test cases 311 | tests := []RazorpayToolTestCase{ 312 | { 313 | Name: "successful case with all parameters", 314 | Request: map[string]interface{}{ 315 | "key1": "value1", 316 | "key2": float64(1000), 317 | // All parameters for a complete request 318 | }, 319 | MockHttpClient: func() (*http.Client, *httptest.Server) { 320 | return mock.NewHTTPClient( 321 | mock.Endpoint{ 322 | Path: fmt.Sprintf(apiPathFmt, "path_params") // or just apiPath. DO NOT add query params here. 323 | Method: "POST", // or "GET" for fetch operations 324 | Response: successResponse, 325 | }, 326 | ) 327 | }, 328 | ExpectError: false, 329 | ExpectedResult: successResponse, 330 | }, 331 | { 332 | Name: "missing required parameter", 333 | Request: map[string]interface{}{ 334 | // Missing a required parameter 335 | }, 336 | MockHttpClient: nil, // No HTTP client needed for validation errors 337 | ExpectError: true, 338 | ExpectedErrMsg: "missing required parameter: param1", 339 | }, 340 | { 341 | Name: "multiple validation errors", 342 | Request: map[string]interface{}{ 343 | // Missing required parameters and/or including invalid types 344 | "optional_param": "invalid_type", // Wrong type for a parameter 345 | }, 346 | MockHttpClient: nil, // No HTTP client needed for validation errors 347 | ExpectError: true, 348 | ExpectedErrMsg: "Validation errors:\n- missing required parameter: param1\n- invalid parameter type: optional_param", 349 | }, 350 | // Additional test cases for other scenarios 351 | } 352 | 353 | // Run the tests 354 | for _, tc := range tests { 355 | t.Run(tc.Name, func(t *testing.T) { 356 | runToolTest(t, tc, ToolFunction, "Resource Name") 357 | }) 358 | } 359 | } 360 | ``` 361 | 362 | #### Best Practices while writing UTs for a new Tool 363 | 364 | 1. **Test Coverage**: At minimum, include: 365 | - One positive test case with all parameters (required and optional) 366 | - One negative test case for each required parameter 367 | - Any edge cases specific to your tool 368 | 369 | 2. **Mock HTTP Responses**: Use the `mock.NewHTTPClient` function to create mock HTTP responses for Razorpay API calls. 370 | 371 | 3. **Validation Errors**: For parameter validation errors, you don't need to mock HTTP responses as these errors are caught before the API call. 372 | 373 | 4. **Test API Errors**: Include at least one test for API-level errors (like invalid currency, not found, etc.). 374 | 375 | 5. **Naming Convention**: Use `Test_FunctionName` format for test functions. 376 | 377 | 6. Use the resource URLs from [Razorpay Go sdk constants](https://github.com/razorpay/razorpay-go/blob/master/constants/url.go) to specify the apiPath to be mocked. 378 | 379 | See [`payment_links_test.go`](payment_links_test.go) for a complete example of tool tests. 380 | 381 | ### Updating Documentation 382 | 383 | After adding a new tool, Update the "Available Tools" section in the README.md in the root of the repository 384 | 385 | ### Best Practices 386 | 387 | 1. **Consistent Naming**: Use consistent naming patterns: 388 | - Fetch methods: `fetch_resource` 389 | - Create methods: `create_resource` 390 | - FetchAll methods: `fetch_all_resources` 391 | 392 | 2. **Error Handling**: Always provide clear error messages 393 | 394 | 3. **Validation**: Always validate required parameters and collect all validation errors before returning using fluent validator pattern. 395 | - Use the `NewValidator` to create a validator 396 | - Chain validation methods (`ValidateAndAddRequiredString`, etc.) 397 | - Return formatted errors with `HandleErrorsIfAny()` 398 | 399 | 4. **Documentation**: Describe all the parameters clearly for the LLMs to understand. 400 | 401 | 5. **Organization**: Add tools to the appropriate file based on resource type 402 | 403 | 6. **Testing**: Test your tool with different parameter combinations ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Razorpay MCP Server (Official) 2 | 3 | The Razorpay MCP Server is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that provides seamless integration with Razorpay APIs, enabling advanced payment processing capabilities for developers and AI tools. 4 | 5 | ## Quick Start 6 | 7 | Choose your preferred setup method: 8 | - **[Remote MCP Server](#remote-mcp-server-recommended)** - Hosted by Razorpay, no setup required 9 | - **[Local MCP Server](#local-mcp-server)** - Run on your own infrastructure 10 | 11 | ## Available Tools 12 | 13 | Currently, the Razorpay MCP Server provides the following tools: 14 | 15 | | Tool | Description | API | Remote Server Support | 16 | |:-------------------------------------|:-------------------------------------------------------|:------------------------------------|:---------------------| 17 | | `capture_payment` | Change the payment status from authorized to captured. | [Payment](https://razorpay.com/docs/api/payments/capture) | ✅ | 18 | | `fetch_payment` | Fetch payment details with ID | [Payment](https://razorpay.com/docs/api/payments/fetch-with-id) | ✅ | 19 | | `fetch_payment_card_details` | Fetch card details used for a payment | [Payment](https://razorpay.com/docs/api/payments/fetch-payment-expanded-card) | ✅ | 20 | | `fetch_all_payments` | Fetch all payments with filtering and pagination | [Payment](https://razorpay.com/docs/api/payments/fetch-all-payments) | ✅ | 21 | | `update_payment` | Update the notes field of a payment | [Payment](https://razorpay.com/docs/api/payments/update) | ✅ | 22 | | `initiate_payment` | Initiate a payment using saved payment method with order and customer details | [Payment](https://github.com/razorpay/razorpay-go/blob/master/documents/payment.md#create-payment-json) | ✅ | 23 | | `resend_otp` | Resend OTP if the previous one was not received or expired | [Payment](https://github.com/razorpay/razorpay-go/blob/master/documents/payment.md#otp-resend) | ✅ | 24 | | `submit_otp` | Verify and submit OTP to complete payment authentication | [Payment](https://github.com/razorpay/razorpay-go/blob/master/documents/payment.md#otp-submit) | ✅ | 25 | | `create_payment_link` | Creates a new payment link (standard) | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/create-standard) | ✅ | 26 | | `create_payment_link_upi` | Creates a new UPI payment link | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/create-upi) | ✅ | 27 | | `fetch_all_payment_links` | Fetch all the payment links | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/fetch-all-standard) | ✅ | 28 | | `fetch_payment_link` | Fetch details of a payment link | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/fetch-id-standard/) | ✅ | 29 | | `send_payment_link` | Send a payment link via SMS or email. | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/resend) | ✅ | 30 | | `update_payment_link` | Updates a new standard payment link | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/update-standard) | ✅ | 31 | | `create_order` | Creates an order | [Order](https://razorpay.com/docs/api/orders/create/) | ✅ | 32 | | `fetch_order` | Fetch order with ID | [Order](https://razorpay.com/docs/api/orders/fetch-with-id) | ✅ | 33 | | `fetch_all_orders` | Fetch all orders | [Order](https://razorpay.com/docs/api/orders/fetch-all) | ✅ | 34 | | `update_order` | Update an order | [Order](https://razorpay.com/docs/api/orders/update) | ✅ | 35 | | `fetch_order_payments` | Fetch all payments for an order | [Order](https://razorpay.com/docs/api/orders/fetch-payments/) | ✅ | 36 | | `create_refund` | Creates a refund | [Refund](https://razorpay.com/docs/api/refunds/create-instant/) | ❌ | 37 | | `fetch_refund` | Fetch refund details with ID | [Refund](https://razorpay.com/docs/api/refunds/fetch-with-id/) | ✅ | 38 | | `fetch_all_refunds` | Fetch all refunds | [Refund](https://razorpay.com/docs/api/refunds/fetch-all) | ✅ | 39 | | `update_refund` | Update refund notes with ID | [Refund](https://razorpay.com/docs/api/refunds/update/) | ✅ | 40 | | `fetch_multiple_refunds_for_payment` | Fetch multiple refunds for a payment | [Refund](https://razorpay.com/docs/api/refunds/fetch-multiple-refund-payment/) | ✅ | 41 | | `fetch_specific_refund_for_payment` | Fetch a specific refund for a payment | [Refund](https://razorpay.com/docs/api/refunds/fetch-specific-refund-payment/) | ✅ | 42 | | `create_qr_code` | Creates a QR Code | [QR Code](https://razorpay.com/docs/api/qr-codes/create/) | ✅ | 43 | | `fetch_qr_code` | Fetch QR Code with ID | [QR Code](https://razorpay.com/docs/api/qr-codes/fetch-with-id/) | ✅ | 44 | | `fetch_all_qr_codes` | Fetch all QR Codes | [QR Code](https://razorpay.com/docs/api/qr-codes/fetch-all/) | ✅ | 45 | | `fetch_qr_codes_by_customer_id` | Fetch QR Codes with Customer ID | [QR Code](https://razorpay.com/docs/api/qr-codes/fetch-customer-id/) | ✅ | 46 | | `fetch_qr_codes_by_payment_id` | Fetch QR Codes with Payment ID | [QR Code](https://razorpay.com/docs/api/qr-codes/fetch-payment-id/) | ✅ | 47 | | `fetch_payments_for_qr_code` | Fetch Payments for a QR Code | [QR Code](https://razorpay.com/docs/api/qr-codes/fetch-payments/) | ✅ | 48 | | `close_qr_code` | Closes a QR Code | [QR Code](https://razorpay.com/docs/api/qr-codes/close/) | ❌ | 49 | | `fetch_all_settlements` | Fetch all settlements | [Settlement](https://razorpay.com/docs/api/settlements/fetch-all) | ✅ | 50 | | `fetch_settlement_with_id` | Fetch settlement details | [Settlement](https://razorpay.com/docs/api/settlements/fetch-with-id) | ✅ | 51 | | `fetch_settlement_recon_details` | Fetch settlement reconciliation report | [Settlement](https://razorpay.com/docs/api/settlements/fetch-recon) | ✅ | 52 | | `create_instant_settlement` | Create an instant settlement | [Settlement](https://razorpay.com/docs/api/settlements/instant/create) | ❌ | 53 | | `fetch_all_instant_settlements` | Fetch all instant settlements | [Settlement](https://razorpay.com/docs/api/settlements/instant/fetch-all) | ✅ | 54 | | `fetch_instant_settlement_with_id` | Fetch instant settlement with ID | [Settlement](https://razorpay.com/docs/api/settlements/instant/fetch-with-id) | ✅ | 55 | | `fetch_all_payouts` | Fetch all payout details with A/c number | [Payout](https://razorpay.com/docs/api/x/payouts/fetch-all/) | ✅ | 56 | | `fetch_payout_by_id` | Fetch the payout details with payout ID | [Payout](https://razorpay.com/docs/api/x/payouts/fetch-with-id) | ✅ | 57 | | `fetch_tokens` | Get all saved payment methods for a contact number | [Token](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/recurring-payments/cards/tokens/) | ✅ | 58 | | `revoke_token` | Revoke a saved payment method (token) for a customer | [Token](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/recurring-payments/upi-otm/collect/tokens/#24-cancel-token) | ✅ | 59 | 60 | 61 | ## Use Cases 62 | - Workflow Automation: Automate your day to day workflow using Razorpay MCP Server. 63 | - Agentic Applications: Building AI powered tools that interact with Razorpay's payment ecosystem using this Razorpay MCP server. 64 | 65 | ## Remote MCP Server (Recommended) 66 | 67 | The Remote MCP Server is hosted by Razorpay and provides instant access to Razorpay APIs without any local setup. This is the recommended approach for most users. 68 | 69 | ### Benefits of Remote MCP Server 70 | 71 | - **Zero Setup**: No need to install Docker, Go, or manage local infrastructure 72 | - **Always Updated**: Automatically stays updated with the latest features and security patches 73 | - **High Availability**: Backed by Razorpay's robust infrastructure with 99.9% uptime 74 | - **Reduced Latency**: Optimized routing and caching for faster API responses 75 | - **Enhanced Security**: Secure token-based authentication with automatic token rotation 76 | - **No Maintenance**: No need to worry about updates, patches, or server maintenance 77 | 78 | ### Prerequisites 79 | 80 | `npx` is needed to use mcp server. 81 | You need to have Node.js installed on your system, which includes both `npm` (Node Package Manager) and `npx` (Node Package Execute) by default: 82 | 83 | #### macOS 84 | ```bash 85 | # Install Node.js (which includes npm and npx) using Homebrew 86 | brew install node 87 | 88 | # Alternatively, download from https://nodejs.org/ 89 | ``` 90 | 91 | #### Windows 92 | ```bash 93 | # Install Node.js (which includes npm and npx) using Chocolatey 94 | choco install nodejs 95 | 96 | # Alternatively, download from https://nodejs.org/ 97 | ``` 98 | 99 | #### Verify Installation 100 | ```bash 101 | npx --version 102 | ``` 103 | 104 | ### Usage with Cursor 105 | 106 | Inside your cursor settings in MCP, add this config. 107 | 108 | ```json 109 | { 110 | "mcpServers": { 111 | "rzp-mcp-server": { 112 | "command": "npx", 113 | "args": [ 114 | "mcp-remote", 115 | "https://mcp.razorpay.com/mcp", 116 | "--header", 117 | "Authorization:${AUTH_HEADER}" 118 | ], 119 | "env": { 120 | "AUTH_HEADER": "Basic <Base64(key:secret)>" 121 | } 122 | } 123 | } 124 | } 125 | ``` 126 | 127 | Replace `key` & `secret` with your Razorpay API KEY & API SECRET 128 | 129 | ### Usage with Claude Desktop 130 | 131 | Add the following to your `claude_desktop_config.json`: 132 | 133 | ```json 134 | { 135 | "mcpServers": { 136 | "rzp-mcp-server": { 137 | "command": "npx", 138 | "args": [ 139 | "mcp-remote", 140 | "https://mcp.razorpay.com/mcp", 141 | "--header", 142 | "Authorization: Basic <Merchant Token>" 143 | ] 144 | } 145 | } 146 | } 147 | ``` 148 | 149 | Replace `<Merchant Token>` with your Razorpay merchant token. Check Authentication section for steps to generate token. 150 | 151 | - Learn about how to configure MCP servers in Claude desktop: [Link](https://modelcontextprotocol.io/quickstart/user) 152 | - How to install Claude Desktop: [Link](https://claude.ai/download) 153 | 154 | ### Usage with VS Code 155 | 156 | Add the following to your VS Code settings (JSON): 157 | 158 | ```json 159 | { 160 | "mcp": { 161 | "inputs": [ 162 | { 163 | "type": "promptString", 164 | "id": "merchant_token", 165 | "description": "Razorpay Merchant Token", 166 | "password": true 167 | } 168 | ], 169 | "servers": { 170 | "razorpay-remote": { 171 | "command": "npx", 172 | "args": [ 173 | "mcp-remote", 174 | "https://mcp.razorpay.com/mcp", 175 | "--header", 176 | "Authorization: Basic ${input:merchant_token}" 177 | ] 178 | } 179 | } 180 | } 181 | } 182 | ``` 183 | 184 | Learn more about MCP servers in VS Code's [agent mode documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers). 185 | 186 | ## Authentication 187 | 188 | The Remote MCP Server uses merchant token-based authentication. To generate your merchant token: 189 | 190 | 1. Go to the [Razorpay Dashboard](https://dashboard.razorpay.com/) and navigate to Settings > API Keys 191 | 2. Locate your API Key and API Secret: 192 | - API Key is visible on the dashboard 193 | - API Secret is generated only once when you first create it. **Important:** Do not generate a new secret if you already have one 194 | 195 | 3. Generate your merchant token by running this command in your terminal: 196 | ```bash 197 | echo <RAZORPAY_API_KEY>:<RAZORPAY_API_SECRET> | base64 198 | ``` 199 | Replace `<RAZORPAY_API_KEY>` and `<RAZORPAY_API_SECRET>` with your actual credentials 200 | 201 | 4. Copy the base64-encoded output - this is your merchant token for the Remote MCP Server 202 | 203 | > **Note:** For local MCP Server deployment, you can use the API Key and Secret directly without generating a merchant token. 204 | 205 | 206 | ## Local MCP Server 207 | 208 | For users who prefer to run the MCP server on their own infrastructure or need access to all tools (including those restricted in the remote server), you can deploy the server locally. 209 | 210 | ### Prerequisites 211 | 212 | - Docker 213 | - Golang (Go) 214 | - Git 215 | 216 | To run the Razorpay MCP server, use one of the following methods: 217 | 218 | ### Using Public Docker Image (Recommended) 219 | 220 | You can use the public Razorpay image directly. No need to build anything yourself - just copy-paste the configurations below and make sure Docker is already installed. 221 | 222 | > **Note:** To use a specific version instead of the latest, replace `razorpay/mcp` with `razorpay/mcp:v1.0.0` (or your desired version tag) in the configurations below. Available tags can be found on [Docker Hub](https://hub.docker.com/r/razorpay/mcp/tags). 223 | 224 | 225 | #### Usage with Claude Desktop 226 | 227 | This will use the public razorpay image 228 | 229 | Add the following to your `claude_desktop_config.json`: 230 | 231 | ```json 232 | { 233 | "mcpServers": { 234 | "razorpay-mcp-server": { 235 | "command": "docker", 236 | "args": [ 237 | "run", 238 | "--rm", 239 | "-i", 240 | "-e", 241 | "RAZORPAY_KEY_ID", 242 | "-e", 243 | "RAZORPAY_KEY_SECRET", 244 | "razorpay/mcp" 245 | ], 246 | "env": { 247 | "RAZORPAY_KEY_ID": "your_razorpay_key_id", 248 | "RAZORPAY_KEY_SECRET": "your_razorpay_key_secret" 249 | } 250 | } 251 | } 252 | } 253 | ``` 254 | Please replace the `your_razorpay_key_id` and `your_razorpay_key_secret` with your keys. 255 | 256 | - Learn about how to configure MCP servers in Claude desktop: [Link](https://modelcontextprotocol.io/quickstart/user) 257 | - How to install Claude Desktop: [Link](https://claude.ai/download) 258 | 259 | #### Usage with VS Code 260 | 261 | Add the following to your VS Code settings (JSON): 262 | 263 | ```json 264 | { 265 | "mcpServers": { 266 | "razorpay-mcp-server": { 267 | "command": "docker", 268 | "args": [ 269 | "run", 270 | "--rm", 271 | "-i", 272 | "-e", 273 | "RAZORPAY_KEY_ID", 274 | "-e", 275 | "RAZORPAY_KEY_SECRET", 276 | "razorpay/mcp" 277 | ], 278 | "env": { 279 | "RAZORPAY_KEY_ID": "your_razorpay_key_id", 280 | "RAZORPAY_KEY_SECRET": "your_razorpay_key_secret" 281 | } 282 | } 283 | } 284 | } 285 | ``` 286 | Please replace the `your_razorpay_key_id` and `your_razorpay_key_secret` with your keys. 287 | 288 | - Learn about how to configure MCP servers in Claude desktop: [Link](https://modelcontextprotocol.io/quickstart/user) 289 | - How to install Claude Desktop: [Link](https://claude.ai/download) 290 | 291 | #### Usage with VS Code 292 | 293 | Add the following to your VS Code settings (JSON): 294 | 295 | ```json 296 | { 297 | "mcp": { 298 | "inputs": [ 299 | { 300 | "type": "promptString", 301 | "id": "razorpay_key_id", 302 | "description": "Razorpay Key ID", 303 | "password": false 304 | }, 305 | { 306 | "type": "promptString", 307 | "id": "razorpay_key_secret", 308 | "description": "Razorpay Key Secret", 309 | "password": true 310 | } 311 | ], 312 | "servers": { 313 | "razorpay": { 314 | "command": "docker", 315 | "args": [ 316 | "run", 317 | "-i", 318 | "--rm", 319 | "-e", 320 | "RAZORPAY_KEY_ID", 321 | "-e", 322 | "RAZORPAY_KEY_SECRET", 323 | "razorpay/mcp" 324 | ], 325 | "env": { 326 | "RAZORPAY_KEY_ID": "${input:razorpay_key_id}", 327 | "RAZORPAY_KEY_SECRET": "${input:razorpay_key_secret}" 328 | } 329 | } 330 | } 331 | } 332 | } 333 | ``` 334 | 335 | Learn more about MCP servers in VS Code's [agent mode documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers). 336 | 337 | ### Build from Docker (Alternative) 338 | 339 | You need to clone the Github repo and build the image for Razorpay MCP Server using `docker`. Do make sure `docker` is installed and running in your system. 340 | 341 | ```bash 342 | # Run the server 343 | git clone https://github.com/razorpay/razorpay-mcp-server.git 344 | cd razorpay-mcp-server 345 | docker build -t razorpay-mcp-server:latest . 346 | ``` 347 | 348 | Once the razorpay-mcp-server:latest docker image is built, you can replace the public image(`razorpay/mcp`) with it in the above configurations. 349 | 350 | ### Build from source 351 | 352 | You can directly build from the source instead of using docker by following these steps: 353 | 354 | ```bash 355 | # Clone the repository 356 | git clone https://github.com/razorpay/razorpay-mcp-server.git 357 | cd razorpay-mcp-server 358 | 359 | # Build the binary 360 | go build -o razorpay-mcp-server ./cmd/razorpay-mcp-server 361 | ``` 362 | Once the build is ready, you need to specify the path to the binary executable in the `command` option. Here's an example for VS Code settings: 363 | 364 | ```json 365 | { 366 | "razorpay": { 367 | "command": "/path/to/razorpay-mcp-server", 368 | "args": ["stdio","--log-file=/path/to/rzp-mcp.log"], 369 | "env": { 370 | "RAZORPAY_KEY_ID": "<YOUR_ID>", 371 | "RAZORPAY_KEY_SECRET" : "<YOUR_SECRET>" 372 | } 373 | } 374 | } 375 | ``` 376 | 377 | ## Configuration 378 | 379 | The server requires the following configuration: 380 | 381 | - `RAZORPAY_KEY_ID`: Your Razorpay API key ID 382 | - `RAZORPAY_KEY_SECRET`: Your Razorpay API key secret 383 | - `LOG_FILE` (optional): Path to log file for server logs 384 | - `TOOLSETS` (optional): Comma-separated list of toolsets to enable (default: "all") 385 | - `READ_ONLY` (optional): Run server in read-only mode (default: false) 386 | 387 | ### Command Line Flags 388 | 389 | The server supports the following command line flags: 390 | 391 | - `--key` or `-k`: Your Razorpay API key ID 392 | - `--secret` or `-s`: Your Razorpay API key secret 393 | - `--log-file` or `-l`: Path to log file 394 | - `--toolsets` or `-t`: Comma-separated list of toolsets to enable 395 | - `--read-only`: Run server in read-only mode 396 | 397 | ## Debugging the Server 398 | 399 | You can use the standard Go debugging tools to troubleshoot issues with the server. Log files can be specified using the `--log-file` flag (defaults to ./logs) 400 | 401 | ## License 402 | 403 | This project is licensed under the terms of the MIT open source license. Please refer to [LICENSE](./LICENSE) for the full terms. 404 | ``` -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- ```markdown 1 | # Security 2 | 3 | Razorpay takes the security of our software products and services seriously, including all of the open source code repositories managed through our Razorpay organizations and looks forward to working with the security community to find vulnerabilities and protect our businesses and customers. We are dedicated to responsibly resolving any security concerns. 4 | 5 | Our [open source repositories are outside of the scope of our bug bounty program](https://hackerone.com/razorpay) and therefore not eligible for bounty rewards. However, we will ensure that your finding (if valid), is accepted and gets passed along to the appropriate maintainers for remediation. 6 | 7 | ## Reporting Security Issues 8 | 9 | If you believe you have found a security vulnerability in any Razorpay owned repository, please report it to us through our [Hackerone program](https://hackerone.com/razorpay). 10 | 11 | Please refrain from disclosing vulnerabilities via public channels such as issues, discussions, or pull requests. 12 | 13 | All vulnerability reports must be submitted via our [Hackerone program](https://hackerone.com/razorpay). 14 | 15 | Please include as much of the information listed below as you can to help us better understand and resolve the issue: 16 | 17 | - The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) 18 | - Full paths of source file(s) related to the manifestation of the issue 19 | - The location of the affected source code (tag/branch/commit or direct URL) 20 | - Any special configuration required to reproduce the issue 21 | - Step-by-step instructions to reproduce the issue 22 | - Proof-of-concept or exploit code (if possible) 23 | - Impact of the issue, including how an attacker might exploit the issue 24 | 25 | This information will help us triage your report more quickly. 26 | ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown 1 | # Contributing to Razorpay MCP Server 2 | 3 | Thank you for your interest in contributing to the Razorpay MCP Server! This document outlines the process for contributing to this project. 4 | 5 | ## Code of Conduct 6 | 7 | Please be respectful and considerate of others when contributing to this project. We strive to maintain a welcoming and inclusive environment for all contributors. 8 | 9 | ## TLDR; 10 | 11 | ``` 12 | make test 13 | make fmt 14 | make lint 15 | make build 16 | make run 17 | ``` 18 | 19 | We use Cursor to contribute - our AI developer. Look at `~/.cusor/rules`. It understands the standards we have defined and codes with that. 20 | 21 | ## Development Process 22 | 23 | We use a fork-based workflow for all contributions: 24 | 25 | 1. **Fork the repository**: Start by forking the [razorpay-mcp-server repository](https://github.com/razorpay/razorpay-mcp-server) to your GitHub account. 26 | 27 | 2. **Clone your fork**: Clone your fork to your local machine: 28 | ```bash 29 | git clone https://github.com/YOUR-USERNAME/razorpay-mcp-server.git 30 | cd razorpay-mcp-server 31 | ``` 32 | 33 | 3. **Add upstream remote**: Add the original repository as an "upstream" remote: 34 | ```bash 35 | git remote add upstream https://github.com/razorpay/razorpay-mcp-server.git 36 | ``` 37 | 38 | 4. **Create a branch**: Create a new branch for your changes: 39 | ```bash 40 | git checkout -b username/feature 41 | ``` 42 | Use a descriptive branch name that includes your username followed by a brief feature description. 43 | 44 | 5. **Make your changes**: Implement your changes, following the code style guidelines. 45 | 46 | 6. **Write tests**: Add tests for your changes when applicable. 47 | 48 | 7. **Run tests and linting**: Make sure all tests pass and the code meets our linting standards. 49 | 50 | 8. **Commit your changes**: Make commits with clear messages following this format: 51 | ```bash 52 | git commit -m "[type]: description of the change" 53 | ``` 54 | Where `type` is one of: 55 | - `chore`: for tasks like adding linter config, GitHub Actions, addressing PR review comments, etc. 56 | - `feat`: for adding new features like a new fetch_payment tool 57 | - `fix`: for bug fixes 58 | - `ref`: for code refactoring 59 | - `test`: for adding UTs or E2Es 60 | 61 | Example: `git commit -m "feat: add payment verification tool"` 62 | 63 | 9. **Keep your branch updated**: Regularly sync your branch with the upstream repository: 64 | ```bash 65 | git fetch upstream 66 | git rebase upstream/main 67 | ``` 68 | 69 | 10. **Push to your fork**: Push your changes to your fork: 70 | ```bash 71 | git push origin username/feature 72 | ``` 73 | 74 | 11. **Create a Pull Request**: Open a pull request from your fork to the main repository. 75 | 76 | ## Pull Request Process 77 | 78 | 1. Fill out the pull request template with all relevant information. 79 | 2. Link any related issues in the pull request description. 80 | 3. Ensure all status checks pass. 81 | 4. Wait for review from maintainers. 82 | 5. Address any feedback from the code review. 83 | 6. Once approved, a maintainer will merge your changes. 84 | 85 | ## Local Development Setup 86 | 87 | ### Prerequisites 88 | 89 | - Go 1.21 or later 90 | - Docker (for containerized development) 91 | - Git 92 | 93 | ### Setting up the Development Environment 94 | 95 | 1. Clone your fork of the repository (see above). 96 | 97 | 2. Install dependencies: 98 | ```bash 99 | go mod download 100 | ``` 101 | 102 | 3. Set up your environment variables: 103 | ```bash 104 | export RAZORPAY_KEY_ID=your_key_id 105 | export RAZORPAY_KEY_SECRET=your_key_secret 106 | ``` 107 | 108 | ### Running the Server Locally 109 | 110 | There are `make` commands also available now for the below, refer TLDR; above. 111 | 112 | To run the server in development mode: 113 | 114 | ```bash 115 | go run ./cmd/razorpay-mcp-server/main.go stdio 116 | ``` 117 | 118 | ### Running Tests 119 | 120 | To run all tests: 121 | 122 | ```bash 123 | go test ./... 124 | ``` 125 | 126 | To run tests with coverage: 127 | 128 | ```bash 129 | go test -coverprofile=coverage.out ./... 130 | go tool cover -html=coverage.out 131 | ``` 132 | 133 | ## Code Quality and Linting 134 | 135 | We use golangci-lint for code quality checks. To run the linter: 136 | 137 | ```bash 138 | # Install golangci-lint if you don't have it 139 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 140 | 141 | # Run the linter 142 | golangci-lint run 143 | ``` 144 | 145 | Our linting configuration is defined in `.golangci.yaml` and includes: 146 | - Code style checks (gofmt, goimports) 147 | - Static analysis (gosimple, govet, staticcheck) 148 | - Security checks (gosec) 149 | - Complexity checks (gocyclo) 150 | - And more 151 | 152 | Please ensure your code passes all linting checks before submitting a pull request. 153 | 154 | ## Documentation 155 | 156 | When adding new features or modifying existing ones, please update the documentation accordingly. This includes: 157 | 158 | - Code comments 159 | - README updates 160 | - Tool documentation 161 | 162 | ## Adding New Tools 163 | 164 | When adding a new tool to the Razorpay MCP Server: 165 | 166 | 1. Review the detailed developer guide at [pkg/razorpay/README.md](pkg/razorpay/README.md) for complete instructions and examples. 167 | 2. Create a new function in the appropriate resource file under `pkg/razorpay` (or create a new file if needed). 168 | 3. Implement the tool following the patterns in the developer guide. 169 | 4. Register the tool in `server.go`. 170 | 5. Add appropriate tests. 171 | 6. Update the main README.md to document the new tool. 172 | 173 | The developer guide for tools includes: 174 | - Tool structure and patterns 175 | - Parameter definition and validation 176 | - Examples for both GET and POST endpoints 177 | - Best practices for naming and organization 178 | 179 | ## Releasing 180 | 181 | Releases are managed by the maintainers. We use [GoReleaser](https://goreleaser.com/) for creating releases. 182 | 183 | ## Getting Help 184 | 185 | If you have questions or need help with the contribution process, please use [GitHub Discussions](https://github.com/razorpay/razorpay-mcp-server/discussions) to ask for assistance. 186 | 187 | Thank you for contributing to the Razorpay MCP Server! ``` -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- ```yaml 1 | coverage: 2 | status: 3 | patch: 4 | default: 5 | target: 90.0 6 | threshold: 0.0 7 | project: 8 | default: 9 | target: 70.0 10 | threshold: 0.0 11 | ``` -------------------------------------------------------------------------------- /pkg/mcpgo/transport.go: -------------------------------------------------------------------------------- ```go 1 | package mcpgo 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // TransportServer defines a server that can listen for MCP connections 9 | type TransportServer interface { 10 | // Listen listens for connections 11 | Listen(ctx context.Context, in io.Reader, out io.Writer) error 12 | } 13 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- ```yaml 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions, Help, or General Advice 4 | url: https://github.com/razorpay/razorpay-mcp-server/discussions 5 | about: For any questions, help, or general advice, please use GitHub Discussions instead of opening an issue 6 | - name: Documentation 7 | url: https://github.com/razorpay/razorpay-mcp-server#readme 8 | about: Check the documentation before reporting issues 9 | - name: Report Security Vulnerabilities 10 | url: https://razorpay.com/security/ 11 | about: Please report security vulnerabilities according to our security policy ``` -------------------------------------------------------------------------------- /.github/workflows/assign.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Assign 2 | 3 | on: 4 | issues: 5 | types: [opened, reopened] 6 | pull_request: 7 | types: [opened, reopened] 8 | 9 | jobs: 10 | assign: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea 14 | with: 15 | # Please add your name to assignees 16 | script: | 17 | github.rest.issues.addAssignees({ 18 | issue_number: context.issue.number, 19 | owner: context.repo.owner, 20 | repo: context.repo.repo, 21 | assignees: ['KarthikBoddeda', 'ChiragChiranjib', 'stuckinforloop', 'alok87'] 22 | }) ``` -------------------------------------------------------------------------------- /pkg/razorpay/tools_test.go: -------------------------------------------------------------------------------- ```go 1 | package razorpay 2 | 3 | import ( 4 | "testing" 5 | 6 | rzpsdk "github.com/razorpay/razorpay-go" 7 | ) 8 | 9 | func TestNewToolSets(t *testing.T) { 10 | // Create test observability 11 | obs := CreateTestObservability() 12 | 13 | // Create a test client 14 | client := &rzpsdk.Client{} 15 | 16 | // Test with empty enabled toolsets 17 | toolsetGroup, err := NewToolSets(obs, client, []string{}, false) 18 | if err != nil { 19 | t.Fatalf("NewToolSets failed: %v", err) 20 | } 21 | 22 | if toolsetGroup == nil { 23 | t.Fatal("NewToolSets returned nil toolset group") 24 | } 25 | 26 | // This test ensures that the FetchSavedPaymentMethods line is executed 27 | // providing the missing code coverage 28 | } 29 | ``` -------------------------------------------------------------------------------- /pkg/contextkey/context_key.go: -------------------------------------------------------------------------------- ```go 1 | package contextkey 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // contextKey is a type used for context value keys to avoid key collisions. 8 | type contextKey string 9 | 10 | // Context keys for storing various values. 11 | const ( 12 | clientKey contextKey = "client" 13 | ) 14 | 15 | // WithClient returns a new context with the client instance attached. 16 | func WithClient(ctx context.Context, client interface{}) context.Context { 17 | return context.WithValue(ctx, clientKey, client) 18 | } 19 | 20 | // ClientFromContext extracts the client instance from the context. 21 | // Returns nil if no client is found. 22 | func ClientFromContext(ctx context.Context) interface{} { 23 | return ctx.Value(clientKey) 24 | } 25 | ``` -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Build 2 | on: [push, pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: [ubuntu-latest, windows-latest, macos-latest] 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 22 | with: 23 | go-version-file: "go.mod" 24 | 25 | - name: Download dependencies 26 | run: go mod download 27 | 28 | - name: Run unit tests 29 | run: go test -race ./... 30 | 31 | - name: Build 32 | run: go build -v ./cmd/razorpay-mcp-server ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for the MCP server 4 | title: '[FEATURE] ' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ## 🚀 Feature Description 10 | <!-- A clear and concise description of the feature you're requesting --> 11 | 12 | ## 🤔 Problem Statement 13 | <!-- Describe the problem this feature would solve --> 14 | 15 | ## 💡 Proposed Solution 16 | <!-- Describe how you envision this feature working --> 17 | 18 | ## 🔄 Alternatives Considered 19 | <!-- Describe any alternative solutions or features you've considered --> 20 | 21 | ## 📝 Additional Context 22 | <!-- Add any other context, screenshots, or examples about the feature request here --> 23 | 24 | --- 25 | 26 | > **Note:** For general questions or discussions, please use [GitHub Discussions](https://github.com/razorpay/razorpay-mcp-server/discussions) instead of opening an issue. ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve the MCP server 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## 🐛 Bug Description 10 | <!-- A clear and concise description of what the bug is --> 11 | 12 | ## 🔍 Steps To Reproduce 13 | 1. 14 | 2. 15 | 3. 16 | 17 | ## 🤔 Expected Behavior 18 | <!-- A clear and concise description of what you expected to happen --> 19 | 20 | ## 📱 Environment 21 | - OS version: 22 | - Go version: 23 | - Any other relevant environment details: 24 | 25 | ## 📝 Additional Context 26 | <!-- Add any other context about the problem here --> 27 | 28 | ## 📊 Logs/Screenshots 29 | <!-- If applicable, add logs or screenshots to help explain your problem --> 30 | 31 | --- 32 | 33 | > **Note:** For general questions or discussions, please use [GitHub Discussions](https://github.com/razorpay/razorpay-mcp-server/discussions) instead of opening an issue. ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - v[0-9]+.[0-9]+.[0-9]+* 8 | pull_request: 9 | branches: 10 | - main 11 | jobs: 12 | test: 13 | name: Run tests and publish test coverage 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 2 19 | - uses: actions/setup-go@v4 20 | with: 21 | go-version: '1.23' 22 | 23 | - name: Run coverage 24 | run: | 25 | go test -race -covermode=atomic -coverprofile=coverage.out ./... 26 | - name: Upload coverage to Codecov 27 | uses: codecov/codecov-action@v4 28 | with: 29 | token: ${{ secrets.CODECOV_TOKEN }} 30 | files: ./coverage.out 31 | flags: unittests 32 | name: codecov-umbrella 33 | fail_ci_if_error: true 34 | verbose: true 35 | ``` -------------------------------------------------------------------------------- /pkg/observability/observability.go: -------------------------------------------------------------------------------- ```go 1 | package observability 2 | 3 | import ( 4 | "github.com/razorpay/razorpay-mcp-server/pkg/log" 5 | ) 6 | 7 | // Option is used make Observability 8 | type Option func(*Observability) 9 | 10 | // Observability holds all the observability related dependencies 11 | type Observability struct { 12 | // Logger will be passed as dependency to other services 13 | // which will help in pushing logs 14 | Logger log.Logger 15 | } 16 | 17 | // New will create a new Observability object and 18 | // apply all the options to that object and returns pointer to the object 19 | func New(opts ...Option) *Observability { 20 | observability := &Observability{} 21 | // Loop through each option 22 | for _, opt := range opts { 23 | opt(observability) 24 | } 25 | return observability 26 | } 27 | 28 | // WithLoggingService will set the logging dependency in Deps 29 | func WithLoggingService(s log.Logger) Option { 30 | return func(observe *Observability) { 31 | observe.Logger = s 32 | } 33 | } 34 | ``` -------------------------------------------------------------------------------- /pkg/mcpgo/stdio.go: -------------------------------------------------------------------------------- ```go 1 | package mcpgo 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/mark3labs/mcp-go/server" 10 | ) 11 | 12 | // ErrInvalidServerImplementation indicates that the server 13 | // implementation is not compatible 14 | var ErrInvalidServerImplementation = errors.New( 15 | "invalid server implementation", 16 | ) 17 | 18 | // NewStdioServer creates a new stdio transport server 19 | func NewStdioServer(mcpServer Server) (*mark3labsStdioImpl, error) { 20 | sImpl, ok := mcpServer.(*Mark3labsImpl) 21 | if !ok { 22 | return nil, fmt.Errorf("%w: expected *Mark3labsImpl, got %T", 23 | ErrInvalidServerImplementation, mcpServer) 24 | } 25 | 26 | return &mark3labsStdioImpl{ 27 | mcpStdioServer: server.NewStdioServer(sImpl.McpServer), 28 | }, nil 29 | } 30 | 31 | // mark3labsStdioImpl implements the TransportServer 32 | // interface for stdio transport 33 | type mark3labsStdioImpl struct { 34 | mcpStdioServer *server.StdioServer 35 | } 36 | 37 | // Listen implements the TransportServer interface 38 | func (s *mark3labsStdioImpl) Listen( 39 | ctx context.Context, in io.Reader, out io.Writer) error { 40 | return s.mcpStdioServer.Listen(ctx, in, out) 41 | } 42 | ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Release (GoReleaser) 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | permissions: 7 | contents: write 8 | id-token: write 9 | attestations: write 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 21 | with: 22 | go-version-file: "go.mod" 23 | 24 | - name: Download dependencies 25 | run: go mod download 26 | 27 | - name: Run GoReleaser 28 | uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 29 | with: 30 | distribution: goreleaser 31 | # GoReleaser version 32 | version: "~> v2" 33 | # Arguments to pass to GoReleaser 34 | args: release --clean 35 | workdir: . 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Generate signed build provenance attestations for workflow artifacts 40 | uses: actions/attest-build-provenance@v2 41 | with: 42 | subject-path: | 43 | dist/*.tar.gz 44 | dist/*.zip 45 | dist/*.txt ``` -------------------------------------------------------------------------------- /pkg/log/log.go: -------------------------------------------------------------------------------- ```go 1 | package log 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | // Logger is an interface for logging, it is used internally 10 | // at present but has scope for external implementations 11 | // 12 | //nolint:interfacebloat 13 | type Logger interface { 14 | Infof(ctx context.Context, format string, args ...interface{}) 15 | Errorf(ctx context.Context, format string, args ...interface{}) 16 | Fatalf(ctx context.Context, format string, args ...interface{}) 17 | Debugf(ctx context.Context, format string, args ...interface{}) 18 | Warningf(ctx context.Context, format string, args ...interface{}) 19 | Close() error 20 | } 21 | 22 | // New creates a new logger based on the provided configuration. 23 | // It returns an enhanced context and a logger implementation. 24 | // For stdio mode, it creates a file-based slog logger. 25 | // For sse mode, it creates a stdout-based slog logger. 26 | func New(ctx context.Context, config *Config) (context.Context, Logger) { 27 | var ( 28 | logger Logger 29 | err error 30 | ) 31 | 32 | switch config.GetMode() { 33 | case ModeStdio: 34 | // For stdio mode, use slog logger that writes to file 35 | logger, err = NewSloggerWithFile(config.GetSlogConfig().GetPath()) 36 | if err != nil { 37 | fmt.Printf("failed to initialize logger\n") 38 | os.Exit(1) 39 | } 40 | default: 41 | fmt.Printf("failed to initialize logger\n") 42 | os.Exit(1) 43 | } 44 | 45 | return ctx, logger 46 | } 47 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | FROM golang:1.24.2-alpine AS builder 2 | 3 | # Install git 4 | RUN apk add --no-cache git 5 | 6 | WORKDIR /app 7 | 8 | COPY go.mod go.sum ./ 9 | 10 | RUN go mod download 11 | 12 | COPY . . 13 | 14 | # Build arguments with defaults 15 | ARG VERSION="dev" 16 | ARG COMMIT 17 | ARG BUILD_DATE 18 | 19 | # Use build args if provided, otherwise use fallbacks 20 | RUN if [ -z "$COMMIT" ]; then \ 21 | COMMIT=$(git rev-parse HEAD 2>/dev/null || echo 'unknown'); \ 22 | fi && \ 23 | if [ -z "$BUILD_DATE" ]; then \ 24 | BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ); \ 25 | fi && \ 26 | CGO_ENABLED=0 GOOS=linux go build -ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${BUILD_DATE}" -o razorpay-mcp-server ./cmd/razorpay-mcp-server 27 | 28 | FROM alpine:latest 29 | 30 | RUN apk --no-cache add ca-certificates 31 | 32 | # Create a non-root user to run the application 33 | RUN addgroup -S rzpgroup && adduser -S rzp -G rzpgroup 34 | 35 | WORKDIR /app 36 | 37 | COPY --from=builder /app/razorpay-mcp-server . 38 | 39 | # Change ownership of the application to the non-root user 40 | RUN chown -R rzp:rzpgroup /app 41 | 42 | ENV CONFIG="" \ 43 | RAZORPAY_KEY_ID="" \ 44 | RAZORPAY_KEY_SECRET="" \ 45 | PORT="8090" \ 46 | MODE="stdio" \ 47 | LOG_FILE="" 48 | 49 | # Switch to the non-root user 50 | USER rzp 51 | 52 | # Use shell form to allow variable substitution and conditional execution 53 | ENTRYPOINT ["sh", "-c", "./razorpay-mcp-server stdio --key ${RAZORPAY_KEY_ID} --secret ${RAZORPAY_KEY_SECRET} ${CONFIG:+--config ${CONFIG}} ${LOG_FILE:+--log-file ${LOG_FILE}}"] ``` -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Description 2 | <!-- Provide a brief summary of the changes introduced by this PR --> 3 | 4 | ## Related Issues 5 | <!-- List any related issues that this PR addresses (e.g., "Fixes #123", "Resolves #456") --> 6 | 7 | ## Type of Change 8 | <!-- Please delete options that are not relevant --> 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - [ ] Refactoring (no functional changes, code improvements only) 13 | - [ ] Documentation update 14 | - [ ] Performance improvement 15 | 16 | ## Testing 17 | <!-- Describe the tests you've performed to verify your changes --> 18 | - [ ] Manual testing 19 | - [ ] Added unit tests 20 | - [ ] Added integration tests (if applicable) 21 | - [ ] All tests pass locally 22 | 23 | ## Checklist 24 | <!-- Please check all items that apply to this PR --> 25 | - [ ] I have followed the code style of this project 26 | - [ ] I have added comments to code where necessary, particularly in hard-to-understand areas 27 | - [ ] I have updated the documentation where necessary 28 | - [ ] I have verified that my changes do not introduce new warnings or errors 29 | - [ ] I have checked for and resolved any merge conflicts 30 | - [ ] I have considered the performance implications of my changes 31 | 32 | ## Additional Information 33 | <!-- Add any additional context, screenshots, or information that might be helpful for reviewers --> ``` -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Lint 2 | on: 3 | push: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 19 | with: 20 | go-version-file: 'go.mod' 21 | 22 | - name: Verify dependencies 23 | run: | 24 | go mod verify 25 | go mod download 26 | 27 | LINT_VERSION=1.64.8 28 | curl -fsSL https://github.com/golangci/golangci-lint/releases/download/v${LINT_VERSION}/golangci-lint-${LINT_VERSION}-linux-amd64.tar.gz | \ 29 | tar xz --strip-components 1 --wildcards \*/golangci-lint 30 | mkdir -p bin && mv golangci-lint bin/ 31 | 32 | - name: Run checks 33 | run: | 34 | STATUS=0 35 | assert-nothing-changed() { 36 | local diff 37 | "$@" >/dev/null || return 1 38 | if ! diff="$(git diff -U1 --color --exit-code)"; then 39 | printf '\e[31mError: running `\e[1m%s\e[22m` results in modifications that you must check into version control:\e[0m\n%s\n\n' "$*" "$diff" >&2 40 | git checkout -- . 41 | STATUS=1 42 | fi 43 | } 44 | 45 | assert-nothing-changed go fmt ./... 46 | assert-nothing-changed go mod tidy 47 | 48 | bin/golangci-lint run --out-format=colored-line-number --timeout=3m || STATUS=$? 49 | 50 | exit $STATUS 51 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/server.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/contextkey" 10 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" 11 | "github.com/razorpay/razorpay-mcp-server/pkg/observability" 12 | ) 13 | 14 | func NewRzpMcpServer( 15 | obs *observability.Observability, 16 | client *rzpsdk.Client, 17 | enabledToolsets []string, 18 | readOnly bool, 19 | mcpOpts ...mcpgo.ServerOption, 20 | ) (mcpgo.Server, error) { 21 | // Validate required parameters 22 | if obs == nil { 23 | return nil, fmt.Errorf("observability is required") 24 | } 25 | if client == nil { 26 | return nil, fmt.Errorf("razorpay client is required") 27 | } 28 | 29 | // Set up default MCP options with Razorpay-specific hooks 30 | defaultOpts := []mcpgo.ServerOption{ 31 | mcpgo.WithLogging(), 32 | mcpgo.WithResourceCapabilities(true, true), 33 | mcpgo.WithToolCapabilities(true), 34 | mcpgo.WithHooks(mcpgo.SetupHooks(obs)), 35 | } 36 | // Merge with user-provided options 37 | mcpOpts = append(defaultOpts, mcpOpts...) 38 | 39 | // Create server 40 | server := mcpgo.NewMcpServer("razorpay-mcp-server", "1.0.0", mcpOpts...) 41 | 42 | // Register Razorpay tools 43 | toolsets, err := NewToolSets(obs, client, enabledToolsets, readOnly) 44 | if err != nil { 45 | return nil, fmt.Errorf("failed to create toolsets: %w", err) 46 | } 47 | toolsets.RegisterTools(server) 48 | 49 | return server, nil 50 | } 51 | 52 | // getClientFromContextOrDefault returns either the provided default 53 | // client or gets one from context. 54 | func getClientFromContextOrDefault( 55 | ctx context.Context, 56 | defaultClient *rzpsdk.Client, 57 | ) (*rzpsdk.Client, error) { 58 | if defaultClient != nil { 59 | return defaultClient, nil 60 | } 61 | 62 | clientInterface := contextkey.ClientFromContext(ctx) 63 | if clientInterface == nil { 64 | return nil, fmt.Errorf("no client found in context") 65 | } 66 | 67 | client, ok := clientInterface.(*rzpsdk.Client) 68 | if !ok { 69 | return nil, fmt.Errorf("invalid client type in context") 70 | } 71 | 72 | return client, nil 73 | } 74 | ``` -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Docker Image Build & Push 2 | on: 3 | push: 4 | branches: ["main", "sojinss4u/dockerimagebuild"] 5 | tags: ['v*.*.*'] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up QEMU 17 | uses: docker/setup-qemu-action@v3 18 | 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v3 21 | 22 | - name: Login to Docker Hub 23 | uses: docker/login-action@v3 24 | with: 25 | username: ${{ secrets.PUBLIC_DOCKER_USERNAME }} 26 | password: ${{ secrets.PUBLIC_DOCKER_PASSWORD }} 27 | 28 | - name: Get Build Info 29 | id: build_info 30 | run: | 31 | TRIGGER_SHA=${{ github.event.pull_request.head.sha || github.sha }} 32 | echo "trigger_sha=${TRIGGER_SHA}" >> $GITHUB_OUTPUT 33 | 34 | # Generate build timestamp in UTC 35 | BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") 36 | echo "build_date=${BUILD_DATE}" >> $GITHUB_OUTPUT 37 | 38 | - name: Determine Docker Tag 39 | id: vars 40 | run: | 41 | if [[ "${GITHUB_REF}" == refs/tags/* ]]; then 42 | IMAGE_TAG="${GITHUB_REF#refs/tags/}" 43 | echo "tags=razorpay/mcp:${IMAGE_TAG},razorpay/mcp:latest" >> $GITHUB_OUTPUT 44 | else 45 | # Use the trigger SHA instead of the merge commit SHA 46 | IMAGE_TAG="${{ steps.build_info.outputs.trigger_sha }}" 47 | echo "tags=razorpay/mcp:${IMAGE_TAG}" >> $GITHUB_OUTPUT 48 | fi 49 | 50 | - name: Build & Push 51 | uses: docker/build-push-action@v6 52 | with: 53 | context: . 54 | platforms: linux/amd64,linux/arm64 55 | push: true 56 | tags: ${{ steps.vars.outputs.tags }} 57 | build-args: | 58 | VERSION=${{ github.ref_name }} 59 | COMMIT=${{ steps.build_info.outputs.trigger_sha }} 60 | BUILD_DATE=${{ steps.build_info.outputs.build_date }} 61 | ``` -------------------------------------------------------------------------------- /pkg/log/config.go: -------------------------------------------------------------------------------- ```go 1 | package log 2 | 3 | import ( 4 | "log/slog" 5 | ) 6 | 7 | // Logger modes 8 | const ( 9 | ModeStdio = "stdio" 10 | ) 11 | 12 | // Config holds logger configuration with options pattern. 13 | // Use NewConfig to create a new configuration with default values, 14 | // then customize it using the With* option functions. 15 | type Config struct { 16 | // mode determines the logger type (stdio or sse) 17 | mode string 18 | // Embedded configs for different logger types 19 | slog slogConfig 20 | } 21 | 22 | // slogConfig holds slog-specific configuration for stdio mode 23 | type slogConfig struct { 24 | // path is the file path where logs will be written 25 | path string 26 | // logLevel sets the minimum log level to output 27 | logLevel slog.Leveler 28 | } 29 | 30 | // GetMode returns the logger mode (stdio or sse) 31 | func (c Config) GetMode() string { 32 | return c.mode 33 | } 34 | 35 | // GetSlogConfig returns the slog logger configuration 36 | func (c Config) GetSlogConfig() slogConfig { 37 | return c.slog 38 | } 39 | 40 | // GetLogLevel returns the log level 41 | func (z Config) GetLogLevel() slog.Leveler { 42 | return z.slog.logLevel 43 | } 44 | 45 | // GetPath returns the log file path 46 | func (s slogConfig) GetPath() string { 47 | return s.path 48 | } 49 | 50 | // ConfigOption represents a configuration option function 51 | type ConfigOption func(*Config) 52 | 53 | // WithMode sets the logger mode (stdio or sse) 54 | func WithMode(mode string) ConfigOption { 55 | return func(c *Config) { 56 | c.mode = mode 57 | } 58 | } 59 | 60 | // WithLogPath sets the log file path 61 | func WithLogPath(path string) ConfigOption { 62 | return func(c *Config) { 63 | c.slog.path = path 64 | } 65 | } 66 | 67 | // WithLogLevel sets the log level for the mode 68 | func WithLogLevel(level slog.Level) ConfigOption { 69 | return func(c *Config) { 70 | c.slog.logLevel = level 71 | } 72 | } 73 | 74 | // NewConfig creates a new config with default values. 75 | // By default, it uses stdio mode with info log level. 76 | // Use With* options to customize the configuration. 77 | func NewConfig(opts ...ConfigOption) *Config { 78 | config := &Config{ 79 | mode: ModeStdio, 80 | slog: slogConfig{ 81 | logLevel: slog.LevelInfo, 82 | }, 83 | } 84 | 85 | for _, opt := range opts { 86 | opt(config) 87 | } 88 | 89 | return config 90 | } 91 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/mock/server.go: -------------------------------------------------------------------------------- ```go 1 | package mock 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | // Endpoint defines a route and its response 13 | type Endpoint struct { 14 | Path string 15 | Method string 16 | Response interface{} 17 | } 18 | 19 | // NewHTTPClient creates and returns a mock HTTP client with configured 20 | // endpoints 21 | func NewHTTPClient( 22 | endpoints ...Endpoint, 23 | ) (*http.Client, *httptest.Server) { 24 | mockServer := NewServer(endpoints...) 25 | client := mockServer.Client() 26 | return client, mockServer 27 | } 28 | 29 | // NewServer creates a mock HTTP server for testing 30 | func NewServer(endpoints ...Endpoint) *httptest.Server { 31 | router := mux.NewRouter() 32 | 33 | for _, endpoint := range endpoints { 34 | path := endpoint.Path 35 | method := endpoint.Method 36 | response := endpoint.Response 37 | 38 | router.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { 39 | w.Header().Set("Content-Type", "application/json") 40 | 41 | if respMap, ok := response.(map[string]interface{}); ok { 42 | if _, hasError := respMap["error"]; hasError { 43 | w.WriteHeader(http.StatusBadRequest) 44 | } else { 45 | w.WriteHeader(http.StatusOK) 46 | } 47 | } else { 48 | w.WriteHeader(http.StatusOK) 49 | } 50 | 51 | switch resp := response.(type) { 52 | case []byte: 53 | _, err := w.Write(resp) 54 | if err != nil { 55 | http.Error(w, err.Error(), http.StatusInternalServerError) 56 | } 57 | case string: 58 | _, err := w.Write([]byte(resp)) 59 | if err != nil { 60 | http.Error(w, err.Error(), http.StatusInternalServerError) 61 | } 62 | default: 63 | err := json.NewEncoder(w).Encode(resp) 64 | if err != nil { 65 | http.Error(w, err.Error(), http.StatusInternalServerError) 66 | } 67 | } 68 | }).Methods(method) 69 | } 70 | 71 | router.NotFoundHandler = http.HandlerFunc( 72 | func(w http.ResponseWriter, r *http.Request) { 73 | w.Header().Set("Content-Type", "application/json") 74 | w.WriteHeader(http.StatusNotFound) 75 | 76 | _ = json.NewEncoder(w).Encode(map[string]interface{}{ 77 | "error": map[string]interface{}{ 78 | "code": "NOT_FOUND", 79 | "description": fmt.Sprintf("No mock for %s %s", r.Method, r.URL.Path), 80 | }, 81 | }) 82 | }) 83 | 84 | return httptest.NewServer(router) 85 | } 86 | ``` -------------------------------------------------------------------------------- /cmd/razorpay-mcp-server/main.go: -------------------------------------------------------------------------------- ```go 1 | //nolint:lll 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | var ( 13 | version = "version" 14 | commit = "commit" 15 | date = "date" 16 | ) 17 | 18 | var cfgFile string 19 | 20 | var rootCmd = &cobra.Command{ 21 | Use: "server", 22 | Short: "Razorpay MCP Server", 23 | Version: fmt.Sprintf("%s\ncommit %s\ndate %s", version, commit, date), 24 | } 25 | 26 | // Execute runs the root command and handles any errors 27 | func Execute() { 28 | err := rootCmd.Execute() 29 | if err != nil { 30 | os.Exit(1) 31 | } 32 | } 33 | 34 | func init() { 35 | cobra.OnInitialize(initConfig) 36 | 37 | // flags will be available for all subcommands 38 | rootCmd.PersistentFlags().StringP("key", "k", "", "your razorpay api key") 39 | rootCmd.PersistentFlags().StringP("secret", "s", "", "your razorpay api secret") 40 | rootCmd.PersistentFlags().StringP("log-file", "l", "", "path to the log file") 41 | rootCmd.PersistentFlags().StringSliceP("toolsets", "t", []string{}, "comma-separated list of toolsets to enable") 42 | rootCmd.PersistentFlags().Bool("read-only", false, "run server in read-only mode") 43 | 44 | // bind flags to viper 45 | _ = viper.BindPFlag("key", rootCmd.PersistentFlags().Lookup("key")) 46 | _ = viper.BindPFlag("secret", rootCmd.PersistentFlags().Lookup("secret")) 47 | _ = viper.BindPFlag("log_file", rootCmd.PersistentFlags().Lookup("log-file")) 48 | _ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets")) 49 | _ = viper.BindPFlag("read_only", rootCmd.PersistentFlags().Lookup("read-only")) 50 | 51 | // Set environment variable mappings 52 | _ = viper.BindEnv("key", "RAZORPAY_KEY_ID") // Maps RAZORPAY_KEY_ID to key 53 | _ = viper.BindEnv("secret", "RAZORPAY_KEY_SECRET") // Maps RAZORPAY_KEY_SECRET to secret 54 | 55 | // Enable environment variable reading 56 | viper.AutomaticEnv() 57 | 58 | // subcommands 59 | rootCmd.AddCommand(stdioCmd) 60 | } 61 | 62 | // initConfig reads in config file and ENV variables if set. 63 | func initConfig() { 64 | if cfgFile != "" { 65 | viper.SetConfigFile(cfgFile) 66 | } else { 67 | home, err := os.UserHomeDir() 68 | cobra.CheckErr(err) 69 | 70 | viper.AddConfigPath(home) 71 | viper.SetConfigType("yaml") 72 | viper.SetConfigName(".razorpay-mcp-server") 73 | } 74 | 75 | viper.AutomaticEnv() 76 | 77 | if err := viper.ReadInConfig(); err == nil { 78 | fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) 79 | } 80 | } 81 | 82 | func main() { 83 | if err := rootCmd.Execute(); err != nil { 84 | os.Exit(1) 85 | } 86 | } 87 | ``` -------------------------------------------------------------------------------- /pkg/log/slog_test.go: -------------------------------------------------------------------------------- ```go 1 | package log 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestGetDefaultLogPath(t *testing.T) { 14 | path := getDefaultLogPath() 15 | 16 | assert.NotEmpty(t, path, "expected non-empty path") 17 | assert.True(t, filepath.IsAbs(path), 18 | "expected absolute path, got: %s", path) 19 | } 20 | 21 | func TestNewSlogger(t *testing.T) { 22 | logger, err := NewSlogger() 23 | require.NoError(t, err) 24 | require.NotNil(t, logger) 25 | 26 | // Test Close 27 | err = logger.Close() 28 | assert.NoError(t, err) 29 | } 30 | 31 | func TestNewSloggerWithFile(t *testing.T) { 32 | tests := []struct { 33 | name string 34 | path string 35 | wantErr bool 36 | }{ 37 | { 38 | name: "with empty path", 39 | path: "", 40 | wantErr: false, 41 | }, 42 | { 43 | name: "with valid path", 44 | path: filepath.Join(os.TempDir(), "test-log-file.log"), 45 | wantErr: false, 46 | }, 47 | { 48 | name: "with invalid path", 49 | path: "/this/path/should/not/exist/log.txt", 50 | wantErr: false, // Should fallback to stderr 51 | }, 52 | } 53 | 54 | for _, tt := range tests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | // Clean up test file after test 57 | if tt.path != "" { 58 | defer os.Remove(tt.path) 59 | } 60 | 61 | logger, err := NewSloggerWithFile(tt.path) 62 | if tt.wantErr { 63 | assert.Error(t, err) 64 | return 65 | } 66 | 67 | require.NoError(t, err) 68 | require.NotNil(t, logger) 69 | 70 | // Test logging 71 | ctx := context.Background() 72 | logger.Infof(ctx, "test message") 73 | logger.Debugf(ctx, "test debug") 74 | logger.Warningf(ctx, "test warning") 75 | logger.Errorf(ctx, "test error") 76 | 77 | // Test Close 78 | err = logger.Close() 79 | assert.NoError(t, err) 80 | 81 | // Verify file was created if path was specified 82 | if tt.path != "" && tt.path != "/this/path/should/not/exist/log.txt" { 83 | _, err := os.Stat(tt.path) 84 | assert.NoError(t, err, "log file should exist") 85 | } 86 | }) 87 | } 88 | } 89 | 90 | func TestNew(t *testing.T) { 91 | tests := []struct { 92 | name string 93 | config *Config 94 | }{ 95 | { 96 | name: "stdio mode", 97 | config: NewConfig( 98 | WithMode(ModeStdio), 99 | WithLogPath(""), 100 | ), 101 | }, 102 | { 103 | name: "default mode", 104 | config: NewConfig(), 105 | }, 106 | } 107 | 108 | for _, tt := range tests { 109 | t.Run(tt.name, func(t *testing.T) { 110 | ctx := context.Background() 111 | newCtx, logger := New(ctx, tt.config) 112 | 113 | require.NotNil(t, newCtx) 114 | require.NotNil(t, logger) 115 | 116 | // Test logging 117 | logger.Infof(ctx, "test message") 118 | logger.Debugf(ctx, "test debug") 119 | logger.Warningf(ctx, "test warning") 120 | logger.Errorf(ctx, "test error") 121 | 122 | // Test Close 123 | err := logger.Close() 124 | assert.NoError(t, err) 125 | }) 126 | } 127 | } 128 | ``` -------------------------------------------------------------------------------- /pkg/toolsets/toolsets.go: -------------------------------------------------------------------------------- ```go 1 | package toolsets 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" 7 | ) 8 | 9 | // Toolset represents a group of related tools 10 | type Toolset struct { 11 | Name string 12 | Description string 13 | Enabled bool 14 | readOnly bool 15 | writeTools []mcpgo.Tool 16 | readTools []mcpgo.Tool 17 | } 18 | 19 | // ToolsetGroup manages multiple toolsets 20 | type ToolsetGroup struct { 21 | Toolsets map[string]*Toolset 22 | everythingOn bool 23 | readOnly bool 24 | } 25 | 26 | // NewToolset creates a new toolset with the given name and description 27 | func NewToolset(name string, description string) *Toolset { 28 | return &Toolset{ 29 | Name: name, 30 | Description: description, 31 | Enabled: false, 32 | readOnly: false, 33 | } 34 | } 35 | 36 | // NewToolsetGroup creates a new toolset group 37 | func NewToolsetGroup(readOnly bool) *ToolsetGroup { 38 | return &ToolsetGroup{ 39 | Toolsets: make(map[string]*Toolset), 40 | everythingOn: false, 41 | readOnly: readOnly, 42 | } 43 | } 44 | 45 | // AddWriteTools adds write tools to the toolset 46 | func (t *Toolset) AddWriteTools(tools ...mcpgo.Tool) *Toolset { 47 | if !t.readOnly { 48 | t.writeTools = append(t.writeTools, tools...) 49 | } 50 | return t 51 | } 52 | 53 | // AddReadTools adds read tools to the toolset 54 | func (t *Toolset) AddReadTools(tools ...mcpgo.Tool) *Toolset { 55 | t.readTools = append(t.readTools, tools...) 56 | return t 57 | } 58 | 59 | // RegisterTools registers all active tools with the server 60 | func (t *Toolset) RegisterTools(s mcpgo.Server) { 61 | if !t.Enabled { 62 | return 63 | } 64 | for _, tool := range t.readTools { 65 | s.AddTools(tool) 66 | } 67 | if !t.readOnly { 68 | for _, tool := range t.writeTools { 69 | s.AddTools(tool) 70 | } 71 | } 72 | } 73 | 74 | // AddToolset adds a toolset to the group 75 | func (tg *ToolsetGroup) AddToolset(ts *Toolset) { 76 | if tg.readOnly { 77 | ts.readOnly = true 78 | } 79 | tg.Toolsets[ts.Name] = ts 80 | } 81 | 82 | // EnableToolset enables a specific toolset 83 | func (tg *ToolsetGroup) EnableToolset(name string) error { 84 | toolset, exists := tg.Toolsets[name] 85 | if !exists { 86 | return fmt.Errorf("toolset %s does not exist", name) 87 | } 88 | toolset.Enabled = true 89 | return nil 90 | } 91 | 92 | // EnableToolsets enables multiple toolsets 93 | func (tg *ToolsetGroup) EnableToolsets(names []string) error { 94 | if len(names) == 0 { 95 | tg.everythingOn = true 96 | } 97 | 98 | for _, name := range names { 99 | err := tg.EnableToolset(name) 100 | if err != nil { 101 | return err 102 | } 103 | } 104 | 105 | if tg.everythingOn { 106 | for name := range tg.Toolsets { 107 | err := tg.EnableToolset(name) 108 | if err != nil { 109 | return err 110 | } 111 | } 112 | return nil 113 | } 114 | 115 | return nil 116 | } 117 | 118 | // RegisterTools registers all active toolsets with the server 119 | func (tg *ToolsetGroup) RegisterTools(s mcpgo.Server) { 120 | for _, toolset := range tg.Toolsets { 121 | toolset.RegisterTools(s) 122 | } 123 | } 124 | ``` -------------------------------------------------------------------------------- /cmd/razorpay-mcp-server/stdio.go: -------------------------------------------------------------------------------- ```go 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | stdlog "log" 8 | "log/slog" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | 16 | rzpsdk "github.com/razorpay/razorpay-go" 17 | 18 | "github.com/razorpay/razorpay-mcp-server/pkg/log" 19 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" 20 | "github.com/razorpay/razorpay-mcp-server/pkg/observability" 21 | "github.com/razorpay/razorpay-mcp-server/pkg/razorpay" 22 | ) 23 | 24 | // stdioCmd starts the mcp server in stdio transport mode 25 | var stdioCmd = &cobra.Command{ 26 | Use: "stdio", 27 | Short: "start the stdio server", 28 | Run: func(cmd *cobra.Command, args []string) { 29 | logPath := viper.GetString("log_file") 30 | 31 | config := log.NewConfig( 32 | log.WithMode(log.ModeStdio), 33 | log.WithLogLevel(slog.LevelInfo), 34 | log.WithLogPath(logPath), 35 | ) 36 | 37 | ctx, logger := log.New(context.Background(), config) 38 | 39 | // Create observability with SSE mode 40 | obs := observability.New( 41 | observability.WithLoggingService(logger), 42 | ) 43 | 44 | key := viper.GetString("key") 45 | secret := viper.GetString("secret") 46 | client := rzpsdk.NewClient(key, secret) 47 | 48 | client.SetUserAgent("razorpay-mcp" + version + "/stdio") 49 | 50 | // Get toolsets to enable from config 51 | enabledToolsets := viper.GetStringSlice("toolsets") 52 | 53 | // Get read-only mode from config 54 | readOnly := viper.GetBool("read_only") 55 | 56 | err := runStdioServer(ctx, obs, client, enabledToolsets, readOnly) 57 | if err != nil { 58 | obs.Logger.Errorf(ctx, 59 | "error running stdio server", "error", err) 60 | stdlog.Fatalf("failed to run stdio server: %v", err) 61 | } 62 | }, 63 | } 64 | 65 | func runStdioServer( 66 | ctx context.Context, 67 | obs *observability.Observability, 68 | client *rzpsdk.Client, 69 | enabledToolsets []string, 70 | readOnly bool, 71 | ) error { 72 | ctx, stop := signal.NotifyContext( 73 | ctx, 74 | os.Interrupt, 75 | syscall.SIGTERM, 76 | ) 77 | defer stop() 78 | 79 | srv, err := razorpay.NewRzpMcpServer(obs, client, enabledToolsets, readOnly) 80 | if err != nil { 81 | return fmt.Errorf("failed to create server: %w", err) 82 | } 83 | 84 | stdioSrv, err := mcpgo.NewStdioServer(srv) 85 | if err != nil { 86 | return fmt.Errorf("failed to create stdio server: %w", err) 87 | } 88 | 89 | in, out := io.Reader(os.Stdin), io.Writer(os.Stdout) 90 | errC := make(chan error, 1) 91 | go func() { 92 | obs.Logger.Infof(ctx, "starting server") 93 | errC <- stdioSrv.Listen(ctx, in, out) 94 | }() 95 | 96 | _, _ = fmt.Fprintf( 97 | os.Stderr, 98 | "Razorpay MCP Server running on stdio\n", 99 | ) 100 | 101 | // Wait for shutdown signal 102 | select { 103 | case <-ctx.Done(): 104 | obs.Logger.Infof(ctx, "shutting down server...") 105 | return nil 106 | case err := <-errC: 107 | if err != nil { 108 | obs.Logger.Errorf(ctx, "server error", "error", err) 109 | return err 110 | } 111 | return nil 112 | } 113 | } 114 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/test_helpers.go: -------------------------------------------------------------------------------- ```go 1 | package razorpay 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/go-test/deep" 11 | "github.com/stretchr/testify/assert" 12 | 13 | rzpsdk "github.com/razorpay/razorpay-go" 14 | 15 | "github.com/razorpay/razorpay-mcp-server/pkg/log" 16 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" 17 | "github.com/razorpay/razorpay-mcp-server/pkg/observability" 18 | ) 19 | 20 | // RazorpayToolTestCase defines a common structure for Razorpay tool tests 21 | type RazorpayToolTestCase struct { 22 | Name string 23 | Request map[string]interface{} 24 | MockHttpClient func() (*http.Client, *httptest.Server) 25 | ExpectError bool 26 | ExpectedResult map[string]interface{} 27 | ExpectedErrMsg string 28 | } 29 | 30 | // CreateTestObservability creates an observability stack suitable for testing 31 | func CreateTestObservability() *observability.Observability { 32 | // Create a logger that discards output 33 | _, logger := log.New(context.Background(), log.NewConfig( 34 | log.WithMode(log.ModeStdio)), 35 | ) 36 | return &observability.Observability{ 37 | Logger: logger, 38 | } 39 | } 40 | 41 | // createMCPRequest creates a CallToolRequest with the given arguments 42 | func createMCPRequest(args any) mcpgo.CallToolRequest { 43 | return mcpgo.CallToolRequest{ 44 | Arguments: args, 45 | } 46 | } 47 | 48 | // newMockRzpClient configures a Razorpay client with a mock 49 | // HTTP client for testing. It returns the configured client 50 | // and the mock server (which should be closed by the caller) 51 | func newMockRzpClient( 52 | mockHttpClient func() (*http.Client, *httptest.Server), 53 | ) (*rzpsdk.Client, *httptest.Server) { 54 | rzpMockClient := rzpsdk.NewClient("sample_key", "sample_secret") 55 | 56 | var mockServer *httptest.Server 57 | if mockHttpClient != nil { 58 | var client *http.Client 59 | client, mockServer = mockHttpClient() 60 | 61 | // This Request object is shared by reference across all 62 | // API resources in the client 63 | req := rzpMockClient.Order.Request 64 | req.BaseURL = mockServer.URL 65 | req.HTTPClient = client 66 | } 67 | 68 | return rzpMockClient, mockServer 69 | } 70 | 71 | // runToolTest executes a common test pattern for Razorpay tools 72 | func runToolTest( 73 | t *testing.T, 74 | tc RazorpayToolTestCase, 75 | toolCreator func(*observability.Observability, *rzpsdk.Client) mcpgo.Tool, 76 | objectType string, 77 | ) { 78 | mockRzpClient, mockServer := newMockRzpClient(tc.MockHttpClient) 79 | if mockServer != nil { 80 | defer mockServer.Close() 81 | } 82 | 83 | obs := CreateTestObservability() 84 | tool := toolCreator(obs, mockRzpClient) 85 | 86 | request := createMCPRequest(tc.Request) 87 | result, err := tool.GetHandler()(context.Background(), request) 88 | 89 | assert.NoError(t, err) 90 | 91 | if tc.ExpectError { 92 | assert.NotNil(t, result) 93 | assert.Contains(t, result.Text, tc.ExpectedErrMsg) 94 | return 95 | } 96 | 97 | assert.NotNil(t, result) 98 | 99 | var returnedObj map[string]interface{} 100 | err = json.Unmarshal([]byte(result.Text), &returnedObj) 101 | assert.NoError(t, err) 102 | 103 | if diff := deep.Equal(tc.ExpectedResult, returnedObj); diff != nil { 104 | t.Errorf("%s mismatch: %s", objectType, diff) 105 | } 106 | } 107 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/payouts.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 | // FetchPayoutByID returns a tool that fetches a payout by its ID 14 | func FetchPayout( 15 | obs *observability.Observability, 16 | client *rzpsdk.Client, 17 | ) mcpgo.Tool { 18 | parameters := []mcpgo.ToolParameter{ 19 | mcpgo.WithString( 20 | "payout_id", 21 | mcpgo.Description( 22 | "The unique identifier of the payout. For example, 'pout_00000000000001'", 23 | ), 24 | mcpgo.Required(), 25 | ), 26 | } 27 | 28 | handler := func( 29 | ctx context.Context, 30 | r mcpgo.CallToolRequest, 31 | ) (*mcpgo.ToolResult, error) { 32 | client, err := getClientFromContextOrDefault(ctx, client) 33 | if err != nil { 34 | return mcpgo.NewToolResultError(err.Error()), nil 35 | } 36 | 37 | FetchPayoutOptions := make(map[string]interface{}) 38 | 39 | validator := NewValidator(&r). 40 | ValidateAndAddRequiredString(FetchPayoutOptions, "payout_id") 41 | 42 | if result, err := validator.HandleErrorsIfAny(); result != nil { 43 | return result, err 44 | } 45 | 46 | payout, err := client.Payout.Fetch( 47 | FetchPayoutOptions["payout_id"].(string), 48 | nil, 49 | nil, 50 | ) 51 | if err != nil { 52 | return mcpgo.NewToolResultError( 53 | fmt.Sprintf("fetching payout failed: %s", err.Error())), nil 54 | } 55 | 56 | return mcpgo.NewToolResultJSON(payout) 57 | } 58 | 59 | return mcpgo.NewTool( 60 | "fetch_payout_with_id", 61 | "Fetch a payout's details using its ID", 62 | parameters, 63 | handler, 64 | ) 65 | } 66 | 67 | // FetchAllPayouts returns a tool that fetches all payouts 68 | func FetchAllPayouts( 69 | obs *observability.Observability, 70 | client *rzpsdk.Client, 71 | ) mcpgo.Tool { 72 | parameters := []mcpgo.ToolParameter{ 73 | mcpgo.WithString( 74 | "account_number", 75 | mcpgo.Description("The account from which the payouts were done."+ 76 | "For example, 7878780080316316"), 77 | mcpgo.Required(), 78 | ), 79 | mcpgo.WithNumber( 80 | "count", 81 | mcpgo.Description("Number of payouts to be fetched. Default value is 10."+ 82 | "Maximum value is 100. This can be used for pagination,"+ 83 | "in combination with the skip parameter"), 84 | mcpgo.Min(1), 85 | ), 86 | mcpgo.WithNumber( 87 | "skip", 88 | mcpgo.Description("Numbers of payouts to be skipped. Default value is 0."+ 89 | "This can be used for pagination, in combination with count"), 90 | mcpgo.Min(0), 91 | ), 92 | } 93 | 94 | handler := func( 95 | ctx context.Context, 96 | r mcpgo.CallToolRequest, 97 | ) (*mcpgo.ToolResult, error) { 98 | client, err := getClientFromContextOrDefault(ctx, client) 99 | if err != nil { 100 | return mcpgo.NewToolResultError(err.Error()), nil 101 | } 102 | 103 | FetchAllPayoutsOptions := make(map[string]interface{}) 104 | 105 | validator := NewValidator(&r). 106 | ValidateAndAddRequiredString(FetchAllPayoutsOptions, "account_number"). 107 | ValidateAndAddPagination(FetchAllPayoutsOptions) 108 | 109 | if result, err := validator.HandleErrorsIfAny(); result != nil { 110 | return result, err 111 | } 112 | 113 | payout, err := client.Payout.All(FetchAllPayoutsOptions, nil) 114 | if err != nil { 115 | return mcpgo.NewToolResultError( 116 | fmt.Sprintf("fetching payouts failed: %s", err.Error())), nil 117 | } 118 | 119 | return mcpgo.NewToolResultJSON(payout) 120 | } 121 | 122 | return mcpgo.NewTool( 123 | "fetch_all_payouts", 124 | "Fetch all payouts for a bank account number", 125 | parameters, 126 | handler, 127 | ) 128 | } 129 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/tools.go: -------------------------------------------------------------------------------- ```go 1 | package razorpay 2 | 3 | import ( 4 | rzpsdk "github.com/razorpay/razorpay-go" 5 | 6 | "github.com/razorpay/razorpay-mcp-server/pkg/observability" 7 | "github.com/razorpay/razorpay-mcp-server/pkg/toolsets" 8 | ) 9 | 10 | func NewToolSets( 11 | obs *observability.Observability, 12 | client *rzpsdk.Client, 13 | enabledToolsets []string, 14 | readOnly bool, 15 | ) (*toolsets.ToolsetGroup, error) { 16 | // Create a new toolset group 17 | toolsetGroup := toolsets.NewToolsetGroup(readOnly) 18 | 19 | // Create toolsets 20 | payments := toolsets.NewToolset("payments", "Razorpay Payments related tools"). 21 | AddReadTools( 22 | FetchPayment(obs, client), 23 | FetchPaymentCardDetails(obs, client), 24 | FetchAllPayments(obs, client), 25 | ). 26 | AddWriteTools( 27 | CapturePayment(obs, client), 28 | UpdatePayment(obs, client), 29 | InitiatePayment(obs, client), 30 | ResendOtp(obs, client), 31 | SubmitOtp(obs, client), 32 | ) 33 | 34 | paymentLinks := toolsets.NewToolset( 35 | "payment_links", 36 | "Razorpay Payment Links related tools"). 37 | AddReadTools( 38 | FetchPaymentLink(obs, client), 39 | FetchAllPaymentLinks(obs, client), 40 | ). 41 | AddWriteTools( 42 | CreatePaymentLink(obs, client), 43 | CreateUpiPaymentLink(obs, client), 44 | ResendPaymentLinkNotification(obs, client), 45 | UpdatePaymentLink(obs, client), 46 | ) 47 | 48 | orders := toolsets.NewToolset("orders", "Razorpay Orders related tools"). 49 | AddReadTools( 50 | FetchOrder(obs, client), 51 | FetchAllOrders(obs, client), 52 | FetchOrderPayments(obs, client), 53 | ). 54 | AddWriteTools( 55 | CreateOrder(obs, client), 56 | UpdateOrder(obs, client), 57 | ) 58 | 59 | refunds := toolsets.NewToolset("refunds", "Razorpay Refunds related tools"). 60 | AddReadTools( 61 | FetchRefund(obs, client), 62 | FetchMultipleRefundsForPayment(obs, client), 63 | FetchSpecificRefundForPayment(obs, client), 64 | FetchAllRefunds(obs, client), 65 | ). 66 | AddWriteTools( 67 | CreateRefund(obs, client), 68 | UpdateRefund(obs, client), 69 | ) 70 | 71 | payouts := toolsets.NewToolset("payouts", "Razorpay Payouts related tools"). 72 | AddReadTools( 73 | FetchPayout(obs, client), 74 | FetchAllPayouts(obs, client), 75 | ) 76 | 77 | qrCodes := toolsets.NewToolset("qr_codes", "Razorpay QR Codes related tools"). 78 | AddReadTools( 79 | FetchQRCode(obs, client), 80 | FetchAllQRCodes(obs, client), 81 | FetchQRCodesByCustomerID(obs, client), 82 | FetchQRCodesByPaymentID(obs, client), 83 | FetchPaymentsForQRCode(obs, client), 84 | ). 85 | AddWriteTools( 86 | CreateQRCode(obs, client), 87 | CloseQRCode(obs, client), 88 | ) 89 | 90 | settlements := toolsets.NewToolset("settlements", 91 | "Razorpay Settlements related tools"). 92 | AddReadTools( 93 | FetchSettlement(obs, client), 94 | FetchSettlementRecon(obs, client), 95 | FetchAllSettlements(obs, client), 96 | FetchAllInstantSettlements(obs, client), 97 | FetchInstantSettlement(obs, client), 98 | ). 99 | AddWriteTools( 100 | CreateInstantSettlement(obs, client), 101 | ) 102 | 103 | // Add the single custom tool to an existing toolset 104 | payments.AddReadTools(FetchSavedPaymentMethods(obs, client)). 105 | AddWriteTools(RevokeToken(obs, client)) 106 | 107 | // Add toolsets to the group 108 | toolsetGroup.AddToolset(payments) 109 | toolsetGroup.AddToolset(paymentLinks) 110 | toolsetGroup.AddToolset(orders) 111 | toolsetGroup.AddToolset(refunds) 112 | toolsetGroup.AddToolset(payouts) 113 | toolsetGroup.AddToolset(qrCodes) 114 | toolsetGroup.AddToolset(settlements) 115 | 116 | // Enable the requested features 117 | if err := toolsetGroup.EnableToolsets(enabledToolsets); err != nil { 118 | return nil, err 119 | } 120 | 121 | return toolsetGroup, nil 122 | } 123 | ``` -------------------------------------------------------------------------------- /pkg/mcpgo/server.go: -------------------------------------------------------------------------------- ```go 1 | package mcpgo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/mark3labs/mcp-go/mcp" 7 | "github.com/mark3labs/mcp-go/server" 8 | 9 | "github.com/razorpay/razorpay-mcp-server/pkg/observability" 10 | ) 11 | 12 | // Server defines the minimal MCP server interface needed by the application 13 | type Server interface { 14 | // AddTools adds tools to the server 15 | AddTools(tools ...Tool) 16 | } 17 | 18 | // NewMcpServer creates a new MCP server 19 | func NewMcpServer(name, version string, opts ...ServerOption) *Mark3labsImpl { 20 | // Create option setter to collect mcp options 21 | optSetter := &mark3labsOptionSetter{ 22 | mcpOptions: []server.ServerOption{}, 23 | } 24 | 25 | // Apply our options, which will populate the mcp options 26 | for _, opt := range opts { 27 | _ = opt(optSetter) 28 | } 29 | 30 | // Create the underlying mcp server 31 | mcpServer := server.NewMCPServer( 32 | name, 33 | version, 34 | optSetter.mcpOptions..., 35 | ) 36 | 37 | return &Mark3labsImpl{ 38 | McpServer: mcpServer, 39 | Name: name, 40 | Version: version, 41 | } 42 | } 43 | 44 | // Mark3labsImpl implements the Server interface using mark3labs/mcp-go 45 | type Mark3labsImpl struct { 46 | McpServer *server.MCPServer 47 | Name string 48 | Version string 49 | } 50 | 51 | // mark3labsOptionSetter is used to apply options to the server 52 | type mark3labsOptionSetter struct { 53 | mcpOptions []server.ServerOption 54 | } 55 | 56 | func (s *mark3labsOptionSetter) SetOption(option interface{}) error { 57 | if opt, ok := option.(server.ServerOption); ok { 58 | s.mcpOptions = append(s.mcpOptions, opt) 59 | } 60 | return nil 61 | } 62 | 63 | // AddTools adds tools to the server 64 | func (s *Mark3labsImpl) AddTools(tools ...Tool) { 65 | // Convert our Tool to mcp's ServerTool 66 | var mcpTools []server.ServerTool 67 | for _, tool := range tools { 68 | mcpTools = append(mcpTools, tool.toMCPServerTool()) 69 | } 70 | s.McpServer.AddTools(mcpTools...) 71 | } 72 | 73 | // OptionSetter is an interface for setting options on a configurable object 74 | type OptionSetter interface { 75 | SetOption(option interface{}) error 76 | } 77 | 78 | // ServerOption is a function that configures a Server 79 | type ServerOption func(OptionSetter) error 80 | 81 | // WithLogging returns a server option that enables logging 82 | func WithLogging() ServerOption { 83 | return func(s OptionSetter) error { 84 | return s.SetOption(server.WithLogging()) 85 | } 86 | } 87 | 88 | func WithHooks(hooks *server.Hooks) ServerOption { 89 | return func(s OptionSetter) error { 90 | return s.SetOption(server.WithHooks(hooks)) 91 | } 92 | } 93 | 94 | // WithResourceCapabilities returns a server option 95 | // that enables resource capabilities 96 | func WithResourceCapabilities(read, list bool) ServerOption { 97 | return func(s OptionSetter) error { 98 | return s.SetOption(server.WithResourceCapabilities(read, list)) 99 | } 100 | } 101 | 102 | // WithToolCapabilities returns a server option that enables tool capabilities 103 | func WithToolCapabilities(enabled bool) ServerOption { 104 | return func(s OptionSetter) error { 105 | return s.SetOption(server.WithToolCapabilities(enabled)) 106 | } 107 | } 108 | 109 | // SetupHooks creates and configures the server hooks with logging 110 | func SetupHooks(obs *observability.Observability) *server.Hooks { 111 | hooks := &server.Hooks{} 112 | hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod, 113 | message any) { 114 | obs.Logger.Infof(ctx, "MCP_METHOD_CALLED", 115 | "method", method, 116 | "id", id, 117 | "message", message) 118 | }) 119 | 120 | hooks.AddOnSuccess(func(ctx context.Context, id any, method mcp.MCPMethod, 121 | message any, result any) { 122 | logResult := result 123 | if method == mcp.MethodToolsList { 124 | if r, ok := result.(*mcp.ListToolsResult); ok { 125 | simplifiedTools := make([]string, 0, len(r.Tools)) 126 | for _, tool := range r.Tools { 127 | simplifiedTools = append(simplifiedTools, tool.Name) 128 | } 129 | // Create new map for logging with just the tool names 130 | logResult = map[string]interface{}{ 131 | "tools": simplifiedTools, 132 | } 133 | } 134 | } 135 | 136 | obs.Logger.Infof(ctx, "MCP_METHOD_SUCCEEDED", 137 | "method", method, 138 | "id", id, 139 | "result", logResult) 140 | }) 141 | 142 | hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, 143 | message any, err error) { 144 | obs.Logger.Infof(ctx, "MCP_METHOD_FAILED", 145 | "method", method, 146 | "id", id, 147 | "message", message, 148 | "error", err) 149 | }) 150 | 151 | hooks.AddBeforeCallTool(func(ctx context.Context, id any, 152 | message *mcp.CallToolRequest) { 153 | obs.Logger.Infof(ctx, "TOOL_CALL_STARTED", 154 | "id", id, 155 | "request", message) 156 | }) 157 | 158 | hooks.AddAfterCallTool(func(ctx context.Context, id any, 159 | message *mcp.CallToolRequest, result *mcp.CallToolResult) { 160 | obs.Logger.Infof(ctx, "TOOL_CALL_COMPLETED", 161 | "id", id, 162 | "request", message, 163 | "result", result) 164 | }) 165 | 166 | return hooks 167 | } 168 | ``` -------------------------------------------------------------------------------- /pkg/log/slog.go: -------------------------------------------------------------------------------- ```go 1 | package log 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "log/slog" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | // slogLogger implements Logger interface using slog 13 | type slogLogger struct { 14 | logger *slog.Logger 15 | closer func() error 16 | } 17 | 18 | // logWithLevel is a helper function that handles common logging functionality 19 | func (s *slogLogger) logWithLevel( 20 | ctx context.Context, 21 | level slog.Level, 22 | format string, 23 | args ...interface{}, 24 | ) { 25 | // Extract context fields and add them as slog attributes 26 | attrs := s.extractContextAttrs(ctx) 27 | 28 | // Convert args to slog attributes 29 | attrs = append(attrs, s.convertArgsToAttrs(args...)...) 30 | 31 | s.logger.LogAttrs(ctx, level, format, attrs...) 32 | } 33 | 34 | // Infof logs an info message with context fields 35 | func (s *slogLogger) Infof( 36 | ctx context.Context, format string, args ...interface{}) { 37 | s.logWithLevel(ctx, slog.LevelInfo, format, args...) 38 | } 39 | 40 | // Errorf logs an error message with context fields 41 | func (s *slogLogger) Errorf( 42 | ctx context.Context, format string, args ...interface{}) { 43 | s.logWithLevel(ctx, slog.LevelError, format, args...) 44 | } 45 | 46 | // Fatalf logs a fatal message with context fields and exits 47 | func (s *slogLogger) Fatalf( 48 | ctx context.Context, format string, args ...interface{}) { 49 | s.logWithLevel(ctx, slog.LevelError, format, args...) 50 | os.Exit(1) 51 | } 52 | 53 | // Debugf logs a debug message with context fields 54 | func (s *slogLogger) Debugf( 55 | ctx context.Context, format string, args ...interface{}) { 56 | s.logWithLevel(ctx, slog.LevelDebug, format, args...) 57 | } 58 | 59 | // Warningf logs a warning message with context fields 60 | func (s *slogLogger) Warningf( 61 | ctx context.Context, format string, args ...interface{}) { 62 | s.logWithLevel(ctx, slog.LevelWarn, format, args...) 63 | } 64 | 65 | // extractContextAttrs extracts fields from context and converts to slog.Attr 66 | func (s *slogLogger) extractContextAttrs(_ context.Context) []slog.Attr { 67 | // Always include all fields as attributes 68 | return []slog.Attr{} 69 | } 70 | 71 | // convertArgsToAttrs converts key-value pairs to slog.Attr 72 | func (s *slogLogger) convertArgsToAttrs(args ...interface{}) []slog.Attr { 73 | if len(args) == 0 { 74 | return nil 75 | } 76 | 77 | var attrs []slog.Attr 78 | for i := 0; i < len(args)-1; i += 2 { 79 | if i+1 < len(args) { 80 | key, ok := args[i].(string) 81 | if !ok { 82 | continue 83 | } 84 | value := args[i+1] 85 | attrs = append(attrs, slog.Any(key, value)) 86 | } 87 | } 88 | return attrs 89 | } 90 | 91 | // Close implements the Logger interface Close method 92 | func (s *slogLogger) Close() error { 93 | if s.closer != nil { 94 | return s.closer() 95 | } 96 | return nil 97 | } 98 | 99 | // NewSlogger returns a new slog.Logger implementation of Logger interface. 100 | // If path to log file is not provided then logger uses stderr for stdio mode 101 | // If the log file cannot be opened, falls back to stderr 102 | func NewSlogger() (*slogLogger, error) { 103 | // For stdio mode, always use stderr regardless of path 104 | // This ensures logs don't interfere with MCP protocol on stdout 105 | return &slogLogger{ 106 | logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ 107 | Level: slog.LevelInfo, 108 | })), 109 | }, nil 110 | } 111 | 112 | func NewSloggerWithStdout(config *Config) (*slogLogger, error) { 113 | // For stdio mode, always use Stdout regardless of path 114 | // This ensures logs don't interfere with MCP protocol on stdout 115 | return &slogLogger{ 116 | logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 117 | Level: config.slog.logLevel, 118 | })), 119 | }, nil 120 | } 121 | 122 | // getDefaultLogPath returns an absolute path for the logs directory 123 | func getDefaultLogPath() string { 124 | execPath, err := os.Executable() 125 | if err != nil { 126 | // Fallback to temp directory if we can't determine executable path 127 | return filepath.Join(os.TempDir(), "razorpay-mcp-server-logs") 128 | } 129 | 130 | execDir := filepath.Dir(execPath) 131 | 132 | return filepath.Join(execDir, "logs") 133 | } 134 | 135 | // NewSloggerWithFile returns a new slog.Logger. 136 | // If path to log file is not provided then 137 | // logger uses a default path next to the executable 138 | // If the log file cannot be opened, falls back to stderr 139 | // 140 | // TODO: add redaction of sensitive data 141 | func NewSloggerWithFile(path string) (*slogLogger, error) { 142 | if path == "" { 143 | path = getDefaultLogPath() 144 | } 145 | 146 | file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 147 | if err != nil { 148 | // Fall back to stderr if we can't open the log file 149 | fmt.Fprintf( 150 | os.Stderr, 151 | "Warning: Failed to open log file: %v\nFalling back to stderr\n", 152 | err, 153 | ) 154 | logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) 155 | noop := func() error { return nil } 156 | return &slogLogger{ 157 | logger: logger, 158 | closer: noop, 159 | }, nil 160 | } 161 | 162 | fmt.Fprintf(os.Stderr, "logs are stored in: %v\n", path) 163 | return &slogLogger{ 164 | logger: slog.New(slog.NewTextHandler(file, nil)), 165 | closer: func() error { 166 | if err := file.Close(); err != nil { 167 | log.Printf("close log file: %v", err) 168 | } 169 | 170 | return nil 171 | }, 172 | }, nil 173 | } 174 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/mock/server_test.go: -------------------------------------------------------------------------------- ```go 1 | package mock 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestNewHTTPClient(t *testing.T) { 14 | client, server := NewHTTPClient( 15 | Endpoint{ 16 | Path: "/test", 17 | Method: "GET", 18 | Response: map[string]interface{}{"status": "ok"}, 19 | }, 20 | ) 21 | defer server.Close() 22 | 23 | assert.NotNil(t, client) 24 | assert.NotNil(t, server) 25 | 26 | resp, err := client.Get(server.URL + "/test") 27 | assert.NoError(t, err) 28 | defer resp.Body.Close() 29 | 30 | assert.Equal(t, http.StatusOK, resp.StatusCode) 31 | assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) 32 | 33 | var result map[string]interface{} 34 | err = json.NewDecoder(resp.Body).Decode(&result) 35 | assert.NoError(t, err) 36 | assert.Equal(t, "ok", result["status"]) 37 | } 38 | 39 | func TestNewServer(t *testing.T) { 40 | testCases := []struct { 41 | name string 42 | endpoints []Endpoint 43 | requestPath string 44 | requestMethod string 45 | expectedStatus int 46 | expectedBody string 47 | }{ 48 | { 49 | name: "successful GET with JSON response", 50 | endpoints: []Endpoint{ 51 | { 52 | Path: "/test", 53 | Method: "GET", 54 | Response: map[string]interface{}{"result": "success"}, 55 | }, 56 | }, 57 | requestPath: "/test", 58 | requestMethod: "GET", 59 | expectedStatus: http.StatusOK, 60 | expectedBody: `{"result":"success"}`, 61 | }, 62 | { 63 | name: "error response", 64 | endpoints: []Endpoint{ 65 | { 66 | Path: "/error", 67 | Method: "GET", 68 | Response: map[string]interface{}{ 69 | "error": map[string]interface{}{ 70 | "code": "BAD_REQUEST", 71 | "description": "Test error", 72 | }, 73 | }, 74 | }, 75 | }, 76 | requestPath: "/error", 77 | requestMethod: "GET", 78 | expectedStatus: http.StatusBadRequest, 79 | expectedBody: `{"error":{"code":"BAD_REQUEST",` + 80 | `"description":"Test error"}}`, 81 | }, 82 | { 83 | name: "string response", 84 | endpoints: []Endpoint{ 85 | { 86 | Path: "/string", 87 | Method: "GET", 88 | Response: "plain text response", 89 | }, 90 | }, 91 | requestPath: "/string", 92 | requestMethod: "GET", 93 | expectedStatus: http.StatusOK, 94 | expectedBody: "plain text response", 95 | }, 96 | { 97 | name: "byte array response", 98 | endpoints: []Endpoint{ 99 | { 100 | Path: "/bytes", 101 | Method: "POST", 102 | Response: []byte(`{"raw":"data"}`), 103 | }, 104 | }, 105 | requestPath: "/bytes", 106 | requestMethod: "POST", 107 | expectedStatus: http.StatusOK, 108 | expectedBody: `{"raw":"data"}`, 109 | }, 110 | { 111 | name: "not found", 112 | endpoints: []Endpoint{}, 113 | requestPath: "/nonexistent", 114 | requestMethod: "GET", 115 | expectedStatus: http.StatusNotFound, 116 | expectedBody: `{"error":{"code":"NOT_FOUND",` + 117 | `"description":"No mock for GET /nonexistent"}}`, 118 | }, 119 | } 120 | 121 | for _, tc := range testCases { 122 | t.Run(tc.name, func(t *testing.T) { 123 | server := NewServer(tc.endpoints...) 124 | defer server.Close() 125 | 126 | var req *http.Request 127 | var err error 128 | if tc.requestMethod == "GET" { 129 | req, err = http.NewRequest( 130 | tc.requestMethod, 131 | server.URL+tc.requestPath, 132 | nil, 133 | ) 134 | } else { 135 | req, err = http.NewRequest( 136 | tc.requestMethod, 137 | server.URL+tc.requestPath, 138 | strings.NewReader("test body"), 139 | ) 140 | } 141 | assert.NoError(t, err) 142 | 143 | client := server.Client() 144 | resp, err := client.Do(req) 145 | assert.NoError(t, err) 146 | defer resp.Body.Close() 147 | 148 | assert.Equal(t, tc.expectedStatus, resp.StatusCode) 149 | 150 | body, err := io.ReadAll(resp.Body) 151 | assert.NoError(t, err) 152 | 153 | actualBody := strings.TrimSpace(string(body)) 154 | if strings.HasPrefix(actualBody, "{") { 155 | var expected, actual interface{} 156 | err = json.Unmarshal([]byte(tc.expectedBody), &expected) 157 | assert.NoError(t, err) 158 | 159 | err = json.Unmarshal(body, &actual) 160 | assert.NoError(t, err) 161 | assert.Equal(t, expected, actual) 162 | } else { 163 | assert.Equal(t, tc.expectedBody, actualBody) 164 | } 165 | }) 166 | } 167 | } 168 | 169 | func TestMultipleEndpoints(t *testing.T) { 170 | endpoints := []Endpoint{ 171 | { 172 | Path: "/path1", 173 | Method: "GET", 174 | Response: map[string]interface{}{ 175 | "endpoint": "path1", 176 | }, 177 | }, 178 | { 179 | Path: "/path2", 180 | Method: "POST", 181 | Response: map[string]interface{}{ 182 | "endpoint": "path2", 183 | }, 184 | }, 185 | } 186 | 187 | server := NewServer(endpoints...) 188 | defer server.Close() 189 | 190 | client := server.Client() 191 | 192 | testCases := []struct { 193 | path string 194 | method string 195 | expectedValue string 196 | }{ 197 | {"/path1", "GET", "path1"}, 198 | {"/path2", "POST", "path2"}, 199 | } 200 | for _, tc := range testCases { 201 | t.Run(tc.method+" "+tc.path, func(t *testing.T) { 202 | var ( 203 | resp *http.Response 204 | err error 205 | ) 206 | 207 | if tc.method == "GET" { 208 | resp, err = client.Get(server.URL + tc.path) 209 | } else if tc.method == "POST" { 210 | resp, err = client.Post(server.URL+tc.path, 211 | "application/json", nil) 212 | } 213 | assert.NoError(t, err) 214 | defer resp.Body.Close() 215 | 216 | assert.Equal(t, http.StatusOK, resp.StatusCode) 217 | 218 | var result map[string]interface{} 219 | err = json.NewDecoder(resp.Body).Decode(&result) 220 | assert.NoError(t, err) 221 | assert.Equal(t, tc.expectedValue, result["endpoint"]) 222 | }) 223 | } 224 | } 225 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/tokens.go: -------------------------------------------------------------------------------- ```go 1 | package razorpay 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | rzpsdk "github.com/razorpay/razorpay-go" 8 | "github.com/razorpay/razorpay-go/constants" 9 | 10 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" 11 | "github.com/razorpay/razorpay-mcp-server/pkg/observability" 12 | ) 13 | 14 | // FetchSavedPaymentMethods returns a tool that fetches saved cards 15 | // using contact number 16 | func FetchSavedPaymentMethods( 17 | obs *observability.Observability, 18 | client *rzpsdk.Client, 19 | ) mcpgo.Tool { 20 | parameters := []mcpgo.ToolParameter{ 21 | mcpgo.WithString( 22 | "contact", 23 | mcpgo.Description( 24 | "Contact number of the customer to fetch all saved payment methods for. "+ 25 | "For example, 9876543210 or +919876543210"), 26 | mcpgo.Required(), 27 | ), 28 | } 29 | 30 | handler := func( 31 | ctx context.Context, 32 | r mcpgo.CallToolRequest, 33 | ) (*mcpgo.ToolResult, error) { 34 | // Get client from context or use default 35 | client, err := getClientFromContextOrDefault(ctx, client) 36 | if err != nil { 37 | return mcpgo.NewToolResultError(err.Error()), nil 38 | } 39 | 40 | validator := NewValidator(&r) 41 | 42 | // Validate required contact parameter 43 | contactValue, err := extractValueGeneric[string](&r, "contact", true) 44 | if err != nil { 45 | validator = validator.addError(err) 46 | } else if contactValue == nil || *contactValue == "" { 47 | validator = validator.addError( 48 | fmt.Errorf("missing required parameter: contact")) 49 | } 50 | if result, err := validator.HandleErrorsIfAny(); result != nil { 51 | return result, err 52 | } 53 | contact := *contactValue 54 | customerData := map[string]interface{}{ 55 | "contact": contact, 56 | "fail_existing": "0", // Get existing customer if exists 57 | } 58 | 59 | // Create/get customer using Razorpay SDK 60 | customer, err := client.Customer.Create(customerData, nil) 61 | if err != nil { 62 | return mcpgo.NewToolResultError( 63 | fmt.Sprintf( 64 | "Failed to create/fetch customer with contact %s: %v", contact, err, 65 | )), nil 66 | } 67 | 68 | customerID, ok := customer["id"].(string) 69 | if !ok { 70 | return mcpgo.NewToolResultError("Customer ID not found in response"), nil 71 | } 72 | 73 | url := fmt.Sprintf("/%s/customers/%s/tokens", 74 | constants.VERSION_V1, customerID) 75 | 76 | // Make the API request to get tokens 77 | tokensResponse, err := client.Request.Get(url, nil, nil) 78 | if err != nil { 79 | return mcpgo.NewToolResultError( 80 | fmt.Sprintf( 81 | "Failed to fetch saved payment methods for customer %s: %v", 82 | customerID, 83 | err, 84 | )), nil 85 | } 86 | 87 | result := map[string]interface{}{ 88 | "customer": customer, 89 | "saved_payment_methods": tokensResponse, 90 | } 91 | return mcpgo.NewToolResultJSON(result) 92 | } 93 | 94 | return mcpgo.NewTool( 95 | "fetch_tokens", 96 | "Get all saved payment methods (cards, UPI)"+ 97 | " for a contact number. "+ 98 | "This tool first finds or creates a"+ 99 | " customer with the given contact number, "+ 100 | "then fetches all saved payment tokens "+ 101 | "associated with that customer including "+ 102 | "credit/debit cards, UPI IDs, digital wallets,"+ 103 | " and other tokenized payment instruments.", 104 | parameters, 105 | handler, 106 | ) 107 | } 108 | 109 | // RevokeToken returns a tool that revokes a saved payment token 110 | func RevokeToken( 111 | obs *observability.Observability, 112 | client *rzpsdk.Client, 113 | ) mcpgo.Tool { 114 | parameters := []mcpgo.ToolParameter{ 115 | mcpgo.WithString( 116 | "customer_id", 117 | mcpgo.Description( 118 | "Customer ID for which the token should be revoked. "+ 119 | "Must start with 'cust_' followed by alphanumeric characters. "+ 120 | "Example: 'cust_xxx'"), 121 | mcpgo.Required(), 122 | ), 123 | mcpgo.WithString( 124 | "token_id", 125 | mcpgo.Description( 126 | "Token ID of the saved payment method to be revoked. "+ 127 | "Must start with 'token_' followed by alphanumeric characters. "+ 128 | "Example: 'token_xxx'"), 129 | mcpgo.Required(), 130 | ), 131 | } 132 | 133 | handler := func( 134 | ctx context.Context, 135 | r mcpgo.CallToolRequest, 136 | ) (*mcpgo.ToolResult, error) { 137 | // Get client from context or use default 138 | client, err := getClientFromContextOrDefault(ctx, client) 139 | if err != nil { 140 | return mcpgo.NewToolResultError(err.Error()), nil 141 | } 142 | 143 | validator := NewValidator(&r) 144 | 145 | // Validate required customer_id parameter 146 | customerIDValue, err := extractValueGeneric[string](&r, "customer_id", true) 147 | if err != nil { 148 | validator = validator.addError(err) 149 | } else if customerIDValue == nil || *customerIDValue == "" { 150 | validator = validator.addError( 151 | fmt.Errorf("missing required parameter: customer_id")) 152 | } 153 | if result, err := validator.HandleErrorsIfAny(); result != nil { 154 | return result, err 155 | } 156 | customerID := *customerIDValue 157 | 158 | // Validate required token_id parameter 159 | tokenIDValue, err := extractValueGeneric[string](&r, "token_id", true) 160 | if err != nil { 161 | validator = validator.addError(err) 162 | } else if tokenIDValue == nil || *tokenIDValue == "" { 163 | validator = validator.addError( 164 | fmt.Errorf("missing required parameter: token_id")) 165 | } 166 | if result, err := validator.HandleErrorsIfAny(); result != nil { 167 | return result, err 168 | } 169 | tokenID := *tokenIDValue 170 | 171 | url := fmt.Sprintf( 172 | "/%s%s/%s/tokens/%s/cancel", 173 | constants.VERSION_V1, 174 | constants.CUSTOMER_URL, 175 | customerID, 176 | tokenID, 177 | ) 178 | response, err := client.Token.Request.Put(url, nil, nil) 179 | 180 | if err != nil { 181 | return mcpgo.NewToolResultError( 182 | fmt.Sprintf( 183 | "Failed to revoke token %s for customer %s: %v", 184 | tokenID, 185 | customerID, 186 | err, 187 | )), nil 188 | } 189 | 190 | return mcpgo.NewToolResultJSON(response) 191 | } 192 | 193 | return mcpgo.NewTool( 194 | "revoke_token", 195 | "Revoke a saved payment method (token) for a customer. "+ 196 | "This tool revokes the specified token "+ 197 | "associated with the given customer ID. "+ 198 | "Once revoked, the token cannot be used for future payments.", 199 | parameters, 200 | handler, 201 | ) 202 | } 203 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/payouts_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_FetchPayout(t *testing.T) { 15 | fetchPayoutPathFmt := fmt.Sprintf( 16 | "/%s%s/%%s", 17 | constants.VERSION_V1, 18 | constants.PAYOUT_URL, 19 | ) 20 | 21 | successfulPayoutResp := map[string]interface{}{ 22 | "id": "pout_123", 23 | "entity": "payout", 24 | "fund_account": map[string]interface{}{ 25 | "id": "fa_123", 26 | "entity": "fund_account", 27 | }, 28 | "amount": float64(100000), 29 | "currency": "INR", 30 | "notes": map[string]interface{}{}, 31 | "fees": float64(0), 32 | "tax": float64(0), 33 | "utr": "123456789012345", 34 | "mode": "IMPS", 35 | "purpose": "payout", 36 | "processed_at": float64(1704067200), 37 | "created_at": float64(1704067200), 38 | "updated_at": float64(1704067200), 39 | "status": "processed", 40 | } 41 | 42 | payoutNotFoundResp := map[string]interface{}{ 43 | "error": map[string]interface{}{ 44 | "code": "BAD_REQUEST_ERROR", 45 | "description": "payout not found", 46 | }, 47 | } 48 | 49 | tests := []RazorpayToolTestCase{ 50 | { 51 | Name: "successful fetch", 52 | Request: map[string]interface{}{ 53 | "payout_id": "pout_123", 54 | }, 55 | MockHttpClient: func() (*http.Client, *httptest.Server) { 56 | return mock.NewHTTPClient( 57 | mock.Endpoint{ 58 | Path: fmt.Sprintf(fetchPayoutPathFmt, "pout_123"), 59 | Method: "GET", 60 | Response: successfulPayoutResp, 61 | }, 62 | ) 63 | }, 64 | ExpectError: false, 65 | ExpectedResult: successfulPayoutResp, 66 | }, 67 | { 68 | Name: "payout not found", 69 | Request: map[string]interface{}{ 70 | "payout_id": "pout_invalid", 71 | }, 72 | MockHttpClient: func() (*http.Client, *httptest.Server) { 73 | return mock.NewHTTPClient( 74 | mock.Endpoint{ 75 | Path: fmt.Sprintf( 76 | fetchPayoutPathFmt, 77 | "pout_invalid", 78 | ), 79 | Method: "GET", 80 | Response: payoutNotFoundResp, 81 | }, 82 | ) 83 | }, 84 | ExpectError: true, 85 | ExpectedErrMsg: "fetching payout failed: payout not found", 86 | }, 87 | { 88 | Name: "missing payout_id parameter", 89 | Request: map[string]interface{}{}, 90 | MockHttpClient: nil, // No HTTP client needed for validation error 91 | ExpectError: true, 92 | ExpectedErrMsg: "missing required parameter: payout_id", 93 | }, 94 | { 95 | Name: "multiple validation errors", 96 | Request: map[string]interface{}{ 97 | // Missing payout_id parameter 98 | "non_existent_param": 12345, // Additional parameter 99 | }, 100 | MockHttpClient: nil, // No HTTP client needed for validation error 101 | ExpectError: true, 102 | ExpectedErrMsg: "missing required parameter: payout_id", 103 | }, 104 | } 105 | 106 | for _, tc := range tests { 107 | t.Run(tc.Name, func(t *testing.T) { 108 | runToolTest(t, tc, FetchPayout, "Payout") 109 | }) 110 | } 111 | } 112 | 113 | func Test_FetchAllPayouts(t *testing.T) { 114 | fetchAllPayoutsPath := fmt.Sprintf( 115 | "/%s%s", 116 | constants.VERSION_V1, 117 | constants.PAYOUT_URL, 118 | ) 119 | 120 | successfulPayoutsResp := map[string]interface{}{ 121 | "entity": "collection", 122 | "count": float64(2), 123 | "items": []interface{}{ 124 | map[string]interface{}{ 125 | "id": "pout_1", 126 | "entity": "payout", 127 | "fund_account": map[string]interface{}{ 128 | "id": "fa_1", 129 | "entity": "fund_account", 130 | }, 131 | "amount": float64(100000), 132 | "currency": "INR", 133 | "notes": map[string]interface{}{}, 134 | "fees": float64(0), 135 | "tax": float64(0), 136 | "utr": "123456789012345", 137 | "mode": "IMPS", 138 | "purpose": "payout", 139 | "processed_at": float64(1704067200), 140 | "created_at": float64(1704067200), 141 | "updated_at": float64(1704067200), 142 | "status": "processed", 143 | }, 144 | map[string]interface{}{ 145 | "id": "pout_2", 146 | "entity": "payout", 147 | "fund_account": map[string]interface{}{ 148 | "id": "fa_2", 149 | "entity": "fund_account", 150 | }, 151 | "amount": float64(200000), 152 | "currency": "INR", 153 | "notes": map[string]interface{}{}, 154 | "fees": float64(0), 155 | "tax": float64(0), 156 | "utr": "123456789012346", 157 | "mode": "IMPS", 158 | "purpose": "payout", 159 | "processed_at": float64(1704067200), 160 | "created_at": float64(1704067200), 161 | "updated_at": float64(1704067200), 162 | "status": "pending", 163 | }, 164 | }, 165 | } 166 | 167 | invalidAccountErrorResp := map[string]interface{}{ 168 | "error": map[string]interface{}{ 169 | "code": "BAD_REQUEST_ERROR", 170 | "description": "Invalid account number", 171 | }, 172 | } 173 | 174 | tests := []RazorpayToolTestCase{ 175 | { 176 | Name: "successful fetch with pagination", 177 | Request: map[string]interface{}{ 178 | "account_number": "409002173420", 179 | "count": float64(10), 180 | "skip": float64(0), 181 | }, 182 | MockHttpClient: func() (*http.Client, *httptest.Server) { 183 | return mock.NewHTTPClient( 184 | mock.Endpoint{ 185 | Path: fetchAllPayoutsPath, 186 | Method: "GET", 187 | Response: successfulPayoutsResp, 188 | }, 189 | ) 190 | }, 191 | ExpectError: false, 192 | ExpectedResult: successfulPayoutsResp, 193 | }, 194 | { 195 | Name: "successful fetch without pagination", 196 | Request: map[string]interface{}{ 197 | "account_number": "409002173420", 198 | }, 199 | MockHttpClient: func() (*http.Client, *httptest.Server) { 200 | return mock.NewHTTPClient( 201 | mock.Endpoint{ 202 | Path: fetchAllPayoutsPath, 203 | Method: "GET", 204 | Response: successfulPayoutsResp, 205 | }, 206 | ) 207 | }, 208 | ExpectError: false, 209 | ExpectedResult: successfulPayoutsResp, 210 | }, 211 | { 212 | Name: "invalid account number", 213 | Request: map[string]interface{}{ 214 | "account_number": "invalid_account", 215 | }, 216 | MockHttpClient: func() (*http.Client, *httptest.Server) { 217 | return mock.NewHTTPClient( 218 | mock.Endpoint{ 219 | Path: fetchAllPayoutsPath, 220 | Method: "GET", 221 | Response: invalidAccountErrorResp, 222 | }, 223 | ) 224 | }, 225 | ExpectError: true, 226 | ExpectedErrMsg: "fetching payouts failed: Invalid account number", 227 | }, 228 | { 229 | Name: "missing account_number parameter", 230 | Request: map[string]interface{}{ 231 | "count": float64(10), 232 | "skip": float64(0), 233 | }, 234 | MockHttpClient: nil, // No HTTP client needed for validation error 235 | ExpectError: true, 236 | ExpectedErrMsg: "missing required parameter: account_number", 237 | }, 238 | { 239 | Name: "multiple validation errors", 240 | Request: map[string]interface{}{ 241 | // Missing account_number parameter 242 | "count": "10", // Wrong type for count 243 | "skip": "0", // Wrong type for skip 244 | }, 245 | MockHttpClient: nil, // No HTTP client needed for validation error 246 | ExpectError: true, 247 | ExpectedErrMsg: "Validation errors:\n- " + 248 | "missing required parameter: account_number\n- " + 249 | "invalid parameter type: count\n- " + 250 | "invalid parameter type: skip", 251 | }, 252 | } 253 | 254 | for _, tc := range tests { 255 | t.Run(tc.Name, func(t *testing.T) { 256 | runToolTest(t, tc, FetchAllPayouts, "Payouts") 257 | }) 258 | } 259 | } 260 | ``` -------------------------------------------------------------------------------- /pkg/razorpay/refunds.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 | // CreateRefund returns a tool that creates a normal refund for a payment 14 | func CreateRefund( 15 | obs *observability.Observability, 16 | client *rzpsdk.Client, 17 | ) mcpgo.Tool { 18 | parameters := []mcpgo.ToolParameter{ 19 | mcpgo.WithString( 20 | "payment_id", 21 | mcpgo.Description("Unique identifier of the payment which "+ 22 | "needs to be refunded. ID should have a pay_ prefix."), 23 | mcpgo.Required(), 24 | ), 25 | mcpgo.WithNumber( 26 | "amount", 27 | mcpgo.Description("Payment amount in the smallest currency unit "+ 28 | "(e.g., for ₹295, use 29500)"), 29 | mcpgo.Required(), 30 | mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency) 31 | ), 32 | mcpgo.WithString( 33 | "speed", 34 | mcpgo.Description("The speed at which the refund is to be "+ 35 | "processed. Default is 'normal'. For instant refunds, speed "+ 36 | "is set as 'optimum'."), 37 | ), 38 | mcpgo.WithObject( 39 | "notes", 40 | mcpgo.Description("Key-value pairs used to store additional "+ 41 | "information. A maximum of 15 key-value pairs can be included."), 42 | ), 43 | mcpgo.WithString( 44 | "receipt", 45 | mcpgo.Description("A unique identifier provided by you for "+ 46 | "your internal reference."), 47 | ), 48 | } 49 | 50 | handler := func( 51 | ctx context.Context, 52 | r mcpgo.CallToolRequest, 53 | ) (*mcpgo.ToolResult, error) { 54 | // Get client from context or use default 55 | client, err := getClientFromContextOrDefault(ctx, client) 56 | if err != nil { 57 | return mcpgo.NewToolResultError(err.Error()), nil 58 | } 59 | 60 | payload := make(map[string]interface{}) 61 | data := make(map[string]interface{}) 62 | 63 | validator := NewValidator(&r). 64 | ValidateAndAddRequiredString(payload, "payment_id"). 65 | ValidateAndAddRequiredFloat(payload, "amount"). 66 | ValidateAndAddOptionalString(data, "speed"). 67 | ValidateAndAddOptionalString(data, "receipt"). 68 | ValidateAndAddOptionalMap(data, "notes") 69 | 70 | if result, err := validator.HandleErrorsIfAny(); result != nil { 71 | return result, err 72 | } 73 | 74 | refund, err := client.Payment.Refund( 75 | payload["payment_id"].(string), 76 | int(payload["amount"].(float64)), data, nil) 77 | if err != nil { 78 | return mcpgo.NewToolResultError( 79 | fmt.Sprintf("creating refund failed: %s", err.Error())), nil 80 | } 81 | 82 | return mcpgo.NewToolResultJSON(refund) 83 | } 84 | 85 | return mcpgo.NewTool( 86 | "create_refund", 87 | "Use this tool to create a normal refund for a payment. "+ 88 | "Amount should be in the smallest currency unit "+ 89 | "(e.g., for ₹295, use 29500)", 90 | parameters, 91 | handler, 92 | ) 93 | } 94 | 95 | // FetchRefund returns a tool that fetches a refund by ID 96 | func FetchRefund( 97 | obs *observability.Observability, 98 | client *rzpsdk.Client, 99 | ) mcpgo.Tool { 100 | parameters := []mcpgo.ToolParameter{ 101 | mcpgo.WithString( 102 | "refund_id", 103 | mcpgo.Description( 104 | "Unique identifier of the refund which is to be retrieved. "+ 105 | "ID should have a rfnd_ prefix."), 106 | mcpgo.Required(), 107 | ), 108 | } 109 | 110 | handler := func( 111 | ctx context.Context, 112 | r mcpgo.CallToolRequest, 113 | ) (*mcpgo.ToolResult, error) { 114 | // Get client from context or use default 115 | client, err := getClientFromContextOrDefault(ctx, client) 116 | if err != nil { 117 | return mcpgo.NewToolResultError(err.Error()), nil 118 | } 119 | 120 | payload := make(map[string]interface{}) 121 | 122 | validator := NewValidator(&r). 123 | ValidateAndAddRequiredString(payload, "refund_id") 124 | 125 | if result, err := validator.HandleErrorsIfAny(); result != nil { 126 | return result, err 127 | } 128 | 129 | refund, err := client.Refund.Fetch(payload["refund_id"].(string), nil, nil) 130 | if err != nil { 131 | return mcpgo.NewToolResultError( 132 | fmt.Sprintf("fetching refund failed: %s", err.Error())), nil 133 | } 134 | 135 | return mcpgo.NewToolResultJSON(refund) 136 | } 137 | 138 | return mcpgo.NewTool( 139 | "fetch_refund", 140 | "Use this tool to retrieve the details of a specific refund using its id.", 141 | parameters, 142 | handler, 143 | ) 144 | } 145 | 146 | // UpdateRefund returns a tool that updates a refund's notes 147 | func UpdateRefund( 148 | obs *observability.Observability, 149 | client *rzpsdk.Client, 150 | ) mcpgo.Tool { 151 | parameters := []mcpgo.ToolParameter{ 152 | mcpgo.WithString( 153 | "refund_id", 154 | mcpgo.Description("Unique identifier of the refund which "+ 155 | "needs to be updated. ID should have a rfnd_ prefix."), 156 | mcpgo.Required(), 157 | ), 158 | mcpgo.WithObject( 159 | "notes", 160 | mcpgo.Description("Key-value pairs used to store additional "+ 161 | "information. A maximum of 15 key-value pairs can be included, "+ 162 | "with each value not exceeding 256 characters."), 163 | mcpgo.Required(), 164 | ), 165 | } 166 | 167 | handler := func( 168 | ctx context.Context, 169 | r mcpgo.CallToolRequest, 170 | ) (*mcpgo.ToolResult, error) { 171 | // Get client from context or use default 172 | client, err := getClientFromContextOrDefault(ctx, client) 173 | if err != nil { 174 | return mcpgo.NewToolResultError(err.Error()), nil 175 | } 176 | 177 | payload := make(map[string]interface{}) 178 | data := make(map[string]interface{}) 179 | 180 | validator := NewValidator(&r). 181 | ValidateAndAddRequiredString(payload, "refund_id"). 182 | ValidateAndAddRequiredMap(data, "notes") 183 | 184 | if result, err := validator.HandleErrorsIfAny(); result != nil { 185 | return result, err 186 | } 187 | 188 | refund, err := client.Refund.Update(payload["refund_id"].(string), data, nil) 189 | if err != nil { 190 | return mcpgo.NewToolResultError( 191 | fmt.Sprintf("updating refund failed: %s", err.Error())), nil 192 | } 193 | 194 | return mcpgo.NewToolResultJSON(refund) 195 | } 196 | 197 | return mcpgo.NewTool( 198 | "update_refund", 199 | "Use this tool to update the notes for a specific refund. "+ 200 | "Only the notes field can be modified.", 201 | parameters, 202 | handler, 203 | ) 204 | } 205 | 206 | // FetchMultipleRefundsForPayment returns a tool that fetches multiple refunds 207 | // for a payment 208 | func FetchMultipleRefundsForPayment( 209 | obs *observability.Observability, 210 | client *rzpsdk.Client, 211 | ) mcpgo.Tool { 212 | parameters := []mcpgo.ToolParameter{ 213 | mcpgo.WithString( 214 | "payment_id", 215 | mcpgo.Description("Unique identifier of the payment for which "+ 216 | "refunds are to be retrieved. ID should have a pay_ prefix."), 217 | mcpgo.Required(), 218 | ), 219 | mcpgo.WithNumber( 220 | "from", 221 | mcpgo.Description("Unix timestamp at which the refunds were created."), 222 | ), 223 | mcpgo.WithNumber( 224 | "to", 225 | mcpgo.Description("Unix timestamp till which the refunds were created."), 226 | ), 227 | mcpgo.WithNumber( 228 | "count", 229 | mcpgo.Description("The number of refunds to fetch for the payment."), 230 | ), 231 | mcpgo.WithNumber( 232 | "skip", 233 | mcpgo.Description("The number of refunds to be skipped for the payment."), 234 | ), 235 | } 236 | 237 | handler := func( 238 | ctx context.Context, 239 | r mcpgo.CallToolRequest, 240 | ) (*mcpgo.ToolResult, error) { 241 | client, err := getClientFromContextOrDefault(ctx, client) 242 | if err != nil { 243 | return mcpgo.NewToolResultError(err.Error()), nil 244 | } 245 | 246 | fetchReq := make(map[string]interface{}) 247 | fetchOptions := make(map[string]interface{}) 248 | 249 | validator := NewValidator(&r). 250 | ValidateAndAddRequiredString(fetchReq, "payment_id"). 251 | ValidateAndAddOptionalInt(fetchOptions, "from"). 252 | ValidateAndAddOptionalInt(fetchOptions, "to"). 253 | ValidateAndAddPagination(fetchOptions) 254 | 255 | if result, err := validator.HandleErrorsIfAny(); result != nil { 256 | return result, err 257 | } 258 | 259 | refunds, err := client.Payment.FetchMultipleRefund( 260 | fetchReq["payment_id"].(string), fetchOptions, nil) 261 | if err != nil { 262 | return mcpgo.NewToolResultError( 263 | fmt.Sprintf("fetching multiple refunds failed: %s", 264 | err.Error())), nil 265 | } 266 | 267 | return mcpgo.NewToolResultJSON(refunds) 268 | } 269 | 270 | return mcpgo.NewTool( 271 | "fetch_multiple_refunds_for_payment", 272 | "Use this tool to retrieve multiple refunds for a payment. "+ 273 | "By default, only the last 10 refunds are returned.", 274 | parameters, 275 | handler, 276 | ) 277 | } 278 | 279 | // FetchSpecificRefundForPayment returns a tool that fetches a specific refund 280 | // for a payment 281 | func FetchSpecificRefundForPayment( 282 | obs *observability.Observability, 283 | client *rzpsdk.Client, 284 | ) mcpgo.Tool { 285 | parameters := []mcpgo.ToolParameter{ 286 | mcpgo.WithString( 287 | "payment_id", 288 | mcpgo.Description("Unique identifier of the payment for which "+ 289 | "the refund has been made. ID should have a pay_ prefix."), 290 | mcpgo.Required(), 291 | ), 292 | mcpgo.WithString( 293 | "refund_id", 294 | mcpgo.Description("Unique identifier of the refund to be retrieved. "+ 295 | "ID should have a rfnd_ prefix."), 296 | mcpgo.Required(), 297 | ), 298 | } 299 | 300 | handler := func( 301 | ctx context.Context, 302 | r mcpgo.CallToolRequest, 303 | ) (*mcpgo.ToolResult, error) { 304 | client, err := getClientFromContextOrDefault(ctx, client) 305 | if err != nil { 306 | return mcpgo.NewToolResultError(err.Error()), nil 307 | } 308 | 309 | params := make(map[string]interface{}) 310 | 311 | validator := NewValidator(&r). 312 | ValidateAndAddRequiredString(params, "payment_id"). 313 | ValidateAndAddRequiredString(params, "refund_id") 314 | 315 | if result, err := validator.HandleErrorsIfAny(); result != nil { 316 | return result, err 317 | } 318 | 319 | refund, err := client.Payment.FetchRefund( 320 | params["payment_id"].(string), 321 | params["refund_id"].(string), 322 | nil, nil) 323 | if err != nil { 324 | return mcpgo.NewToolResultError( 325 | fmt.Sprintf("fetching specific refund for payment failed: %s", 326 | err.Error())), nil 327 | } 328 | 329 | return mcpgo.NewToolResultJSON(refund) 330 | } 331 | 332 | return mcpgo.NewTool( 333 | "fetch_specific_refund_for_payment", 334 | "Use this tool to retrieve details of a specific refund made for a payment.", 335 | parameters, 336 | handler, 337 | ) 338 | } 339 | 340 | // FetchAllRefunds returns a tool that fetches all refunds with pagination 341 | // support 342 | func FetchAllRefunds( 343 | obs *observability.Observability, 344 | client *rzpsdk.Client, 345 | ) mcpgo.Tool { 346 | parameters := []mcpgo.ToolParameter{ 347 | mcpgo.WithNumber( 348 | "from", 349 | mcpgo.Description("Unix timestamp at which the refunds were created"), 350 | ), 351 | mcpgo.WithNumber( 352 | "to", 353 | mcpgo.Description("Unix timestamp till which the refunds were created"), 354 | ), 355 | mcpgo.WithNumber( 356 | "count", 357 | mcpgo.Description("The number of refunds to fetch. "+ 358 | "You can fetch a maximum of 100 refunds"), 359 | ), 360 | mcpgo.WithNumber( 361 | "skip", 362 | mcpgo.Description("The number of refunds to be skipped"), 363 | ), 364 | } 365 | 366 | handler := func( 367 | ctx context.Context, 368 | r mcpgo.CallToolRequest, 369 | ) (*mcpgo.ToolResult, error) { 370 | client, err := getClientFromContextOrDefault(ctx, client) 371 | if err != nil { 372 | return mcpgo.NewToolResultError(err.Error()), nil 373 | } 374 | 375 | queryParams := make(map[string]interface{}) 376 | 377 | validator := NewValidator(&r). 378 | ValidateAndAddOptionalInt(queryParams, "from"). 379 | ValidateAndAddOptionalInt(queryParams, "to"). 380 | ValidateAndAddPagination(queryParams) 381 | 382 | if result, err := validator.HandleErrorsIfAny(); result != nil { 383 | return result, err 384 | } 385 | 386 | refunds, err := client.Refund.All(queryParams, nil) 387 | if err != nil { 388 | return mcpgo.NewToolResultError( 389 | fmt.Sprintf("fetching refunds failed: %s", err.Error())), nil 390 | } 391 | 392 | return mcpgo.NewToolResultJSON(refunds) 393 | } 394 | 395 | return mcpgo.NewTool( 396 | "fetch_all_refunds", 397 | "Use this tool to retrieve details of all refunds. "+ 398 | "By default, only the last 10 refunds are returned.", 399 | parameters, 400 | handler, 401 | ) 402 | } 403 | ```