#
tokens: 46627/50000 44/59 files (page 1/5)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 1/5FirstPrevNextLast