#
tokens: 47561/50000 48/59 files (page 1/4)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 4. Use http://codebase.md/razorpay/razorpay-mcp-server?lines=false&page={x} to view the full context.

# Directory Structure

```
├── .cursor
│   └── rules
│       └── new-tool-from-docs.mdc
├── .cursorignore
├── .dockerignore
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.md
│   │   ├── config.yml
│   │   └── feature_request.md
│   ├── pull_request_template.md
│   └── workflows
│       ├── assign.yml
│       ├── build.yml
│       ├── ci.yml
│       ├── docker-publish.yml
│       ├── lint.yml
│       └── release.yml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yaml
├── cmd
│   └── razorpay-mcp-server
│       ├── main.go
│       └── stdio.go
├── codecov.yml
├── CONTRIBUTING.md
├── Dockerfile
├── go.mod
├── go.sum
├── LICENSE
├── Makefile
├── pkg
│   ├── contextkey
│   │   └── context_key.go
│   ├── log
│   │   ├── config.go
│   │   ├── log.go
│   │   ├── slog_test.go
│   │   └── slog.go
│   ├── mcpgo
│   │   ├── README.md
│   │   ├── server.go
│   │   ├── stdio.go
│   │   ├── tool.go
│   │   └── transport.go
│   ├── observability
│   │   └── observability.go
│   ├── razorpay
│   │   ├── mock
│   │   │   ├── server_test.go
│   │   │   └── server.go
│   │   ├── orders_test.go
│   │   ├── orders.go
│   │   ├── payment_links_test.go
│   │   ├── payment_links.go
│   │   ├── payments_test.go
│   │   ├── payments.go
│   │   ├── payouts_test.go
│   │   ├── payouts.go
│   │   ├── qr_codes_test.go
│   │   ├── qr_codes.go
│   │   ├── README.md
│   │   ├── refunds_test.go
│   │   ├── refunds.go
│   │   ├── server.go
│   │   ├── settlements_test.go
│   │   ├── settlements.go
│   │   ├── test_helpers.go
│   │   ├── tokens_test.go
│   │   ├── tokens.go
│   │   ├── tools_params_test.go
│   │   ├── tools_params.go
│   │   ├── tools_test.go
│   │   └── tools.go
│   └── toolsets
│       └── toolsets.go
├── README.md
└── SECURITY.md
```

# Files

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
.git/
.dockerignore
.goreleaser.yaml

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
/dist
/bin
/.go
/logs
/vendor
/razorpay-mcp-server
/.idea
```

--------------------------------------------------------------------------------
/.golangci.yaml:
--------------------------------------------------------------------------------

```yaml
run:
  timeout: 5m
  tests: true
  concurrency: 4

linters:
  disable-all: true
  enable:
    - errcheck
    - gosimple
    - govet
    - ineffassign
    - staticcheck
    - typecheck
    - unused
    - gocyclo
    - gosec
    - misspell
    - gofmt
    - goimports
    - revive
    - interfacebloat
    - iface
    - gocritic
    - bodyclose
    - makezero
    - lll

linters-settings:
  gocyclo:
    min-complexity: 15
  dupl:
    threshold: 100
  goconst:
    min-len: 2
    min-occurrences: 2
  goimports:
    local-prefixes: github.com/razorpay/razorpay-mcp-server
  interfacebloat:
    max: 5
  iface:
    enable:
      - opaque
      - identical
  revive:
    rules:
      - name: blank-imports
        disabled: true
  lll:
    line-length: 80
    tab-width: 1

output:
  formats: colored-line-number
  print-issued-lines: true
  print-linter-name: true

```

--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------

```yaml
version: 2

before:
  hooks:
    # You may remove this if you don't use go modules.
    - go mod tidy
    # you may remove this if you don't need go generate
    - go generate ./...

builds:
  - env:
      - CGO_ENABLED=0
    ldflags:
      - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
    goos:
      - linux
      - windows
      - darwin
    main: ./cmd/razorpay-mcp-server

archives:
  - formats: [tar.gz]
    # this name template makes the OS and Arch compatible with the results of `uname`.
    name_template: >-
      {{ .ProjectName }}_
      {{- title .Os }}_
      {{- if eq .Arch "amd64" }}x86_64
      {{- else if eq .Arch "386" }}i386
      {{- else }}{{ .Arch }}{{ end }}
      {{- if .Arm }}v{{ .Arm }}{{ end }}
    # use zip for windows archives
    format_overrides:
      - goos: windows
        formats: [zip]

changelog:
  sort: asc
  filters:
    exclude:
      - "^docs:"
      - "^test:"

release:
  draft: true
  prerelease: auto
  name_template: "Razorpay MCP Server {{.Version}}"

```

--------------------------------------------------------------------------------
/.cursorignore:
--------------------------------------------------------------------------------

```
# Distribution and Environment
dist/*
build/*
venv/*
env/*
*.env
.env.*
virtualenv/*
.python-version
.ruby-version
.node-version

# Logs and Temporary Files
*.log
*.tsv
*.csv
*.txt
tmp/*
temp/*
.tmp/*
*.temp
*.cache
.cache/*
logs/*

# Sensitive Data
*.sqlite
*.sqlite3
*.dbsql
secrets.*
.npmrc
.yarnrc
.aws/*
.config/*

# Credentials and Keys
*.pem
*.ppk
*.key
*.pub
*.p12
*.pfx
*.htpasswd
*.keystore
*.jks
*.truststore
*.cer
id_rsa*
known_hosts
authorized_keys
.ssh/*
.gnupg/*
.pgpass

# Config Files
*.conf
*.toml
*.ini
.env.local
.env.development
.env.test
.env.production
config/*

# Database Files
*.sql
*.db
*.dmp
*.dump
*.backup
*.restore
*.mdb
*.accdb
*.realm*

# Backup and Archive Files
*.bak
*.backup
*.swp
*.swo
*.swn
*~
*.old
*.orig
*.archive
*.gz
*.zip
*.tar
*.rar
*.7z

# Compiled and Binary Files
*.pyc
*.pyo
**/__pycache__/**
*.class
*.jar
*.war
*.ear
*.dll
*.exe
*.so
*.dylib
*.bin
*.obj

# IDE and Editor Files
.idea/*
*.iml
.vscode/*
.project
.classpath
.settings/*
*.sublime-*
.atom/*
.eclipse/*
*.code-workspace
.history/*

# Build and Dependency Directories
node_modules/*
bower_components/*
vendor/*
packages/*
jspm_packages/*
.gradle/*
target/*
out/*

# Testing and Coverage Files
coverage/*
.coverage
htmlcov/*
.pytest_cache/*
.tox/*
junit.xml
test-results/*

# Mobile Development
*.apk
*.aab
*.ipa
*.xcarchive
*.provisionprofile
google-services.json
GoogleService-Info.plist

# Certificate and Security Files
*.crt
*.csr
*.ovpn
*.p7b
*.p7s
*.pfx
*.spc
*.stl
*.pem.crt
ssl/*

# Container and Infrastructure
*.tfstate
*.tfstate.backup
.terraform/*
.vagrant/*
docker-compose.override.yml
kubernetes/*

# Design and Media Files (often large and binary)
*.psd
*.ai
*.sketch
*.fig
*.xd
assets/raw/*

```

--------------------------------------------------------------------------------
/pkg/mcpgo/README.md:
--------------------------------------------------------------------------------

```markdown
# MCPGO Package

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.

## Purpose

This package was created to isolate the `mark3labs/mcp-go` dependency for several key reasons:

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.

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)).

3. **Simplified API**: Provide a more focused, application-specific API that only exposes the functionality needed by our application.

4. **Error Handling**: Implement proper error handling patterns rather than relying on panics, making the application more robust.

## Components

The package contains several core components:

- **Server**: An interface representing an MCP server, with the `mark3labsImpl` providing the current implementation.
- **Tool**: Interface for defining MCP tools that can be registered with the server.
- **TransportServer**: Interface for different transport mechanisms (stdio, TCP).
- **ToolResult/ToolParameter**: Structures for handling tool calls and results.

## Parameter Helper Functions

The package provides convenience functions for creating tool parameters:

- `WithString(name, description string, required bool)`: Creates a string parameter
- `WithNumber(name, description string, required bool)`: Creates a number parameter
- `WithBoolean(name, description string, required bool)`: Creates a boolean parameter
- `WithObject(name, description string, required bool)`: Creates an object parameter
- `WithArray(name, description string, required bool)`: Creates an array parameter

## Tool Result Helper Functions

The package also provides functions for creating tool results:

- `NewToolResultText(text string)`: Creates a text result
- `NewToolResultJSON(data interface{})`: Creates a JSON result
- `NewToolResultError(text string)`: Creates an error result

## Usage Example

```go
// Create a server
server := mcpgo.NewServer(
    "my-server",
    "1.0.0",
    mcpgo.WithLogging(),
    mcpgo.WithToolCapabilities(true),
)

// Create a tool
tool := mcpgo.NewTool(
    "my_tool",
    "Description of my tool",
    []mcpgo.ToolParameter{
        mcpgo.WithString(
            "param1",
            mcpgo.Description("Description of param1"),
            mcpgo.Required(),
        ),
    },
    func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
        // Extract parameter value
        param1Value, ok := req.Arguments["param1"]
        if !ok {
            return mcpgo.NewToolResultError("Missing required parameter: param1"), nil
        }
        
        // Process and return result
        return mcpgo.NewToolResultText("Result: " + param1Value.(string)), nil
    },
)

// Add tool to server
server.AddTools(tool)

// Create and run a stdio server
stdioServer, err := mcpgo.NewStdioServer(server)
if err != nil {
    log.Fatalf("Failed to create stdio server: %v", err)
}
err = stdioServer.Listen(context.Background(), os.Stdin, os.Stdout)
if err != nil {
    log.Fatalf("Server error: %v", err)
}
```

## Real-world Example

Here's how we use this package in the Razorpay MCP server to create a payment fetching tool:

```go
// FetchPayment returns a tool that fetches payment details using payment_id
func FetchPayment(
    log *slog.Logger,
    client *rzpsdk.Client,
) mcpgo.Tool {
    parameters := []mcpgo.ToolParameter{
        mcpgo.WithString(
            "payment_id",
            mcpgo.Description("payment_id is unique identifier of the payment to be retrieved."),
            mcpgo.Required(),
        ),
    }

    handler := func(
        ctx context.Context,
        r mcpgo.CallToolRequest,
    ) (*mcpgo.ToolResult, error) {
        arg, ok := r.Arguments["payment_id"]
        if !ok {
            return mcpgo.NewToolResultError(
                "payment id is a required field"), nil
        }
        id, ok := arg.(string)
        if !ok {
            return mcpgo.NewToolResultError(
                "payment id is expected to be a string"), nil
        }

        payment, err := client.Payment.Fetch(id, nil, nil)
        if err != nil {
            return mcpgo.NewToolResultError(
                fmt.Sprintf("fetching payment failed: %s", err.Error())), nil
        }

        return mcpgo.NewToolResultJSON(payment)
    }

    return mcpgo.NewTool(
        "fetch_payment",
        "fetch payment details using payment id.",
        parameters,
        handler,
    )
}
```

## Design Principles

1. **Minimal Interface Exposure**: The interfaces defined in this package include only methods that are actually used by our application.

2. **Proper Error Handling**: Functions return errors instead of panicking, allowing for graceful error handling throughout the application.

3. **Implementation Hiding**: The implementation details using `mark3labs/mcp-go` are hidden behind clean interfaces, making future transitions easier.

4. **Naming Clarity**: All implementation types are prefixed with `mark3labs` to clearly indicate they are specifically tied to the current library being used.

## Directory Structure

```
pkg/mcpgo/
├── server.go       # Server interface and implementation
├── transport.go    # TransportServer interface
├── stdio.go        # StdioServer implementation 
├── tool.go         # Tool interfaces and implementation
└── README.md       # This file
``` 
```

--------------------------------------------------------------------------------
/pkg/razorpay/README.md:
--------------------------------------------------------------------------------

```markdown
# Razorpay MCP Server Tools

This package contains tools for interacting with the Razorpay API via the Model Context Protocol (MCP).

## Creating New API Tools

This guide explains how to add new Razorpay API tools to the MCP server.

### Quick Start

1. Locate the API documentation at https://razorpay.com/docs/api/
2. Identify the equivalent function call for the API in the razorpay go sdk.
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
5. Register the tool in `server.go`
6. Update "Available Tools" section in the main README.md

### Tool Structure

Add the tool definition inside pkg/razorpay's resource file. You can define a new tool using this following template:

```go
// ToolName returns a tool that [description of what it does]
func ToolName(
    log *slog.Logger,
    client *rzpsdk.Client,
) mcpgo.Tool {
    parameters := []mcpgo.ToolParameter{
        // Parameters defined here
    }

    handler := func(
        ctx context.Context,
        r mcpgo.CallToolRequest,
    ) (*mcpgo.ToolResult, error) {
        // Parameter validation
        // API call
        // Response handling
        return mcpgo.NewToolResultJSON(response)
    }

    return mcpgo.NewTool(
        "tool_name",
        "A description of the tool. NOTE: Add any exceptions/rules if relevant for the LLMs.",
        parameters,
        handler,
    )
}
```

Tool Naming Conventions:
   - Fetch methods: `fetch_resource`
   - Create methods: `create_resource`
   - FetchAll methods: `fetch_all_resources`

### Parameter Definition

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.

```go
// Required parameters
mcpgo.WithString(
    "parameter_name",
    mcpgo.Description("Description of the parameter"),
    mcpgo.Required(),
)

// Optional parameters
mcpgo.WithNumber(
    "amount",
    mcpgo.Description("Amount in smallest currency unit"),
)
```

Available parameter types:
- `WithString`: For string values
- `WithNumber`: For numeric values
- `WithBoolean`: For boolean values
- `WithObject`: For nested objects

### Parameter Validation

Inside the handler function, use the fluent validator pattern for parameter validation. This provides cleaner, more readable code through method chaining:

```go
// Create a new validator
v := NewValidator(&r)

// Create a map for API request parameters
payload := make(map[string]interface{})

// Validate and add parameters to the payload with method chaining
v.ValidateAndAddRequiredString(payload, "id").
  ValidateAndAddOptionalString(payload, "description").
  ValidateAndAddRequiredInt(payload, "amount").
  ValidateAndAddOptionalInt(payload, "limit")

// Validate and add common parameters
v.ValidateAndAddPagination(payload).
  ValidateAndAddExpand(payload)

// Check for validation errors
if result, err := validator.HandleErrorsIfAny(); result != nil {
	return result, err
}

// Proceed with API call using validated parameters in payload
```

### Example: GET Endpoint

```go
// FetchResource returns a tool that fetches a resource by ID
func FetchResource(
    log *slog.Logger,
    client *rzpsdk.Client,
) mcpgo.Tool {
    parameters := []mcpgo.ToolParameter{
        mcpgo.WithString(
            "id",
            mcpgo.Description("Unique identifier of the resource"),
            mcpgo.Required(),
        ),
    }

    handler := func(
        ctx context.Context,
        r mcpgo.CallToolRequest,
    ) (*mcpgo.ToolResult, error) {
        // Create validator and a payload map
        payload := make(map[string]interface{})
        v := NewValidator(&r).
            ValidateAndAddRequiredString(payload, "id")
        
        // Check for validation errors
        if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

        // Extract validated ID and make API call
        id := payload["id"].(string)
        resource, err := client.Resource.Fetch(id, nil, nil)
        if err != nil {
            return mcpgo.NewToolResultError(
                fmt.Sprintf("fetching resource failed: %s", err.Error())), nil
        }

        return mcpgo.NewToolResultJSON(resource)
    }

    return mcpgo.NewTool(
        "fetch_resource",
        "Fetch a resource from Razorpay by ID",
        parameters,
        handler,
    )
}
```

### Example: POST Endpoint

```go
// CreateResource returns a tool that creates a new resource
func CreateResource(
    log *slog.Logger,
    client *rzpsdk.Client,
) mcpgo.Tool {
    parameters := []mcpgo.ToolParameter{
        mcpgo.WithNumber(
            "amount",
            mcpgo.Description("Amount in smallest currency unit"),
            mcpgo.Required(),
        ),
        mcpgo.WithString(
            "currency",
            mcpgo.Description("Three-letter ISO code for the currency"),
            mcpgo.Required(),
        ),
        mcpgo.WithString(
            "description",
            mcpgo.Description("Brief description of the resource"),
        ),
    }

    handler := func(
        ctx context.Context,
        r mcpgo.CallToolRequest,
    ) (*mcpgo.ToolResult, error) {
        // Create payload map and validator
        data := make(map[string]interface{})
        v := NewValidator(&r).
            ValidateAndAddRequiredInt(data, "amount").
            ValidateAndAddRequiredString(data, "currency").
            ValidateAndAddOptionalString(data, "description")
        
        // Check for validation errors
        if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

        // Call the API with validated data
        resource, err := client.Resource.Create(data, nil)
        if err != nil {
            return mcpgo.NewToolResultError(
                fmt.Sprintf("creating resource failed: %s", err.Error())), nil
        }

        return mcpgo.NewToolResultJSON(resource)
    }

    return mcpgo.NewTool(
        "create_resource",
        "Create a new resource in Razorpay",
        parameters,
        handler,
    )
}
```

### Registering Tools

Add your tool to the appropriate toolset in the `NewToolSets` function in [`pkg/razorpay/tools.go`](tools.go):

```go
// NewToolSets creates and configures all available toolsets
func NewToolSets(
    log *slog.Logger,
    client *rzpsdk.Client,
    enabledToolsets []string,
    readOnly bool,
) (*toolsets.ToolsetGroup, error) {
    // Create a new toolset group
    toolsetGroup := toolsets.NewToolsetGroup(readOnly)

    // Create toolsets
    payments := toolsets.NewToolset("payments", "Razorpay Payments related tools").
        AddReadTools(
            FetchPayment(log, client),
            // Add your read-only payment tool here
        ).
        AddWriteTools(
            // Add your write payment tool here
        )

    paymentLinks := toolsets.NewToolset(
        "payment_links",
        "Razorpay Payment Links related tools").
        AddReadTools(
            FetchPaymentLink(log, client),
            // Add your read-only payment link tool here
        ).
        AddWriteTools(
            CreatePaymentLink(log, client),
            // Add your write payment link tool here
        )

    orders := toolsets.NewToolset("orders", "Razorpay Orders related tools").
        AddReadTools(
            FetchOrder(log, client),
            // Add your read-only order tool here
        ).
        AddWriteTools(
            CreateOrder(log, client),
            // Add your write order tool here
        )

    // If adding a new resource type, create a new toolset:
    /*
    newResource := toolsets.NewToolset("new_resource", "Razorpay New Resource related tools").
        AddReadTools(
            FetchNewResource(log, client),
        ).
        AddWriteTools(
            CreateNewResource(log, client),
        )
    toolsetGroup.AddToolset(newResource)
    */

    // Add toolsets to the group
    toolsetGroup.AddToolset(payments)
    toolsetGroup.AddToolset(paymentLinks)
    toolsetGroup.AddToolset(orders)

    return toolsetGroup, nil
}
```

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.

### Writing Unit Tests

All new tools should have unit tests to verify their behavior. We use a standard pattern for testing tools:

```go
func Test_ToolName(t *testing.T) {
    // Define API path that needs to be mocked
    apiPathFmt := fmt.Sprintf(
        "/%s%s/%%s",
		constants.VERSION_V1,
        constants.PAYMENT_URL,
    )
    
    // Define mock responses
    successResponse := map[string]interface{}{
        "id": "resource_123",
        "amount": float64(1000),
        "currency": "INR",
        // Other expected fields
    }
    
    // Define test cases
    tests := []RazorpayToolTestCase{
        {
            Name: "successful case with all parameters",
            Request: map[string]interface{}{
                "key1": "value1",
                "key2": float64(1000),
                // All parameters for a complete request
            },
            MockHttpClient: func() (*http.Client, *httptest.Server) {
                return mock.NewHTTPClient(
                    mock.Endpoint{
                        Path:     fmt.Sprintf(apiPathFmt, "path_params") // or just apiPath. DO NOT add query params here.
                        Method:   "POST", // or "GET" for fetch operations
                        Response: successResponse,
                    },
                )
            },
            ExpectError:    false,
            ExpectedResult: successResponse,
        },
        {
            Name: "missing required parameter",
            Request: map[string]interface{}{
                // Missing a required parameter
            },
            MockHttpClient: nil, // No HTTP client needed for validation errors
            ExpectError:    true,
            ExpectedErrMsg: "missing required parameter: param1",
        },
        {
            Name: "multiple validation errors",
            Request: map[string]interface{}{
                // Missing required parameters and/or including invalid types
                "optional_param": "invalid_type", // Wrong type for a parameter
            },
            MockHttpClient: nil, // No HTTP client needed for validation errors
            ExpectError:    true,
            ExpectedErrMsg: "Validation errors:\n- missing required parameter: param1\n- invalid parameter type: optional_param",
        },
        // Additional test cases for other scenarios
    }
    
    // Run the tests
    for _, tc := range tests {
        t.Run(tc.Name, func(t *testing.T) {
            runToolTest(t, tc, ToolFunction, "Resource Name")
        })
    }
}
```

#### Best Practices while writing UTs for a new Tool

1. **Test Coverage**: At minimum, include:
   - One positive test case with all parameters (required and optional)
   - One negative test case for each required parameter
   - Any edge cases specific to your tool

2. **Mock HTTP Responses**: Use the `mock.NewHTTPClient` function to create mock HTTP responses for Razorpay API calls.

3. **Validation Errors**: For parameter validation errors, you don't need to mock HTTP responses as these errors are caught before the API call.

4. **Test API Errors**: Include at least one test for API-level errors (like invalid currency, not found, etc.).

5. **Naming Convention**: Use `Test_FunctionName` format for test functions.

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.

See [`payment_links_test.go`](payment_links_test.go) for a complete example of tool tests.

### Updating Documentation

After adding a new tool, Update the "Available Tools" section in the README.md in the root of the repository

### Best Practices

1. **Consistent Naming**: Use consistent naming patterns:
   - Fetch methods: `fetch_resource`
   - Create methods: `create_resource`
   - FetchAll methods: `fetch_all_resources`

2. **Error Handling**: Always provide clear error messages

3. **Validation**: Always validate required parameters and collect all validation errors before returning using fluent validator pattern.
   - Use the `NewValidator` to create a validator
   - Chain validation methods (`ValidateAndAddRequiredString`, etc.)
   - Return formatted errors with `HandleErrorsIfAny()`

4. **Documentation**: Describe all the parameters clearly for the LLMs to understand.

5. **Organization**: Add tools to the appropriate file based on resource type

6. **Testing**: Test your tool with different parameter combinations 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Razorpay MCP Server (Official)

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.

## Quick Start

Choose your preferred setup method:
- **[Remote MCP Server](#remote-mcp-server-recommended)** - Hosted by Razorpay, no setup required
- **[Local MCP Server](#local-mcp-server)** - Run on your own infrastructure

## Available Tools

Currently, the Razorpay MCP Server provides the following tools:

| Tool                                 | Description                                            | API | Remote Server Support |
|:-------------------------------------|:-------------------------------------------------------|:------------------------------------|:---------------------|
| `capture_payment`                    | Change the payment status from authorized to captured. | [Payment](https://razorpay.com/docs/api/payments/capture) | ✅ |
| `fetch_payment`                      | Fetch payment details with ID                          | [Payment](https://razorpay.com/docs/api/payments/fetch-with-id) | ✅ |
| `fetch_payment_card_details`         | Fetch card details used for a payment                  | [Payment](https://razorpay.com/docs/api/payments/fetch-payment-expanded-card) | ✅ |
| `fetch_all_payments`                 | Fetch all payments with filtering and pagination       | [Payment](https://razorpay.com/docs/api/payments/fetch-all-payments) | ✅ |
| `update_payment`                     | Update the notes field of a payment                    | [Payment](https://razorpay.com/docs/api/payments/update) | ✅ |
| `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) | ✅ |
| `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) | ✅ |
| `submit_otp`                        | Verify and submit OTP to complete payment authentication | [Payment](https://github.com/razorpay/razorpay-go/blob/master/documents/payment.md#otp-submit) | ✅ |
| `create_payment_link`                | Creates a new payment link (standard)                  | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/create-standard) | ✅ |
| `create_payment_link_upi`            | Creates a new UPI payment link                         | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/create-upi) | ✅ |
| `fetch_all_payment_links`            | Fetch all the payment links                            | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/fetch-all-standard) | ✅ |
| `fetch_payment_link`                 | Fetch details of a payment link                        | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/fetch-id-standard/) | ✅ |
| `send_payment_link`                  | Send a payment link via SMS or email.                  | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/resend) | ✅ |
| `update_payment_link`                | Updates a new standard payment link                    | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/update-standard) | ✅ |
| `create_order`                       | Creates an order                                       | [Order](https://razorpay.com/docs/api/orders/create/) | ✅ |
| `fetch_order`                        | Fetch order with ID                                    | [Order](https://razorpay.com/docs/api/orders/fetch-with-id) | ✅ |
| `fetch_all_orders`                   | Fetch all orders                                       | [Order](https://razorpay.com/docs/api/orders/fetch-all) | ✅ |
| `update_order`                       | Update an order                                        | [Order](https://razorpay.com/docs/api/orders/update) | ✅ |
| `fetch_order_payments`               | Fetch all payments for an order                        | [Order](https://razorpay.com/docs/api/orders/fetch-payments/) | ✅ |
| `create_refund`                      | Creates a refund                                       | [Refund](https://razorpay.com/docs/api/refunds/create-instant/) | ❌ |
| `fetch_refund`                       | Fetch refund details with ID                           | [Refund](https://razorpay.com/docs/api/refunds/fetch-with-id/) | ✅ |
| `fetch_all_refunds`                  | Fetch all refunds                                      | [Refund](https://razorpay.com/docs/api/refunds/fetch-all) | ✅ |
| `update_refund`                      | Update refund notes with ID                            | [Refund](https://razorpay.com/docs/api/refunds/update/) | ✅ |
| `fetch_multiple_refunds_for_payment` | Fetch multiple refunds for a payment                   | [Refund](https://razorpay.com/docs/api/refunds/fetch-multiple-refund-payment/) | ✅ |
| `fetch_specific_refund_for_payment`  | Fetch a specific refund for a payment                  | [Refund](https://razorpay.com/docs/api/refunds/fetch-specific-refund-payment/) | ✅ |
| `create_qr_code`                     | Creates a QR Code                                      | [QR Code](https://razorpay.com/docs/api/qr-codes/create/) | ✅ |
| `fetch_qr_code`                      | Fetch QR Code with ID                                  | [QR Code](https://razorpay.com/docs/api/qr-codes/fetch-with-id/) | ✅ |
| `fetch_all_qr_codes`                 | Fetch all QR Codes                                     | [QR Code](https://razorpay.com/docs/api/qr-codes/fetch-all/) | ✅ |
| `fetch_qr_codes_by_customer_id`      | Fetch QR Codes with Customer ID                        | [QR Code](https://razorpay.com/docs/api/qr-codes/fetch-customer-id/) | ✅ |
| `fetch_qr_codes_by_payment_id`       | Fetch QR Codes with Payment ID                         | [QR Code](https://razorpay.com/docs/api/qr-codes/fetch-payment-id/) | ✅ |
| `fetch_payments_for_qr_code`         | Fetch Payments for a QR Code                           | [QR Code](https://razorpay.com/docs/api/qr-codes/fetch-payments/) | ✅ |
| `close_qr_code`                      | Closes a QR Code                                       | [QR Code](https://razorpay.com/docs/api/qr-codes/close/) | ❌ |
| `fetch_all_settlements`              | Fetch all settlements                                  | [Settlement](https://razorpay.com/docs/api/settlements/fetch-all) | ✅ |
| `fetch_settlement_with_id`           | Fetch settlement details                               | [Settlement](https://razorpay.com/docs/api/settlements/fetch-with-id) | ✅ |
| `fetch_settlement_recon_details`     | Fetch settlement reconciliation report                 | [Settlement](https://razorpay.com/docs/api/settlements/fetch-recon) | ✅ |
| `create_instant_settlement`          | Create an instant settlement                           | [Settlement](https://razorpay.com/docs/api/settlements/instant/create) | ❌ |
| `fetch_all_instant_settlements`      | Fetch all instant settlements                          | [Settlement](https://razorpay.com/docs/api/settlements/instant/fetch-all) | ✅ |
| `fetch_instant_settlement_with_id`   | Fetch instant settlement with ID                       | [Settlement](https://razorpay.com/docs/api/settlements/instant/fetch-with-id) | ✅ |
| `fetch_all_payouts`                  | Fetch all payout details with A/c number               | [Payout](https://razorpay.com/docs/api/x/payouts/fetch-all/) | ✅ |
| `fetch_payout_by_id`                 | Fetch the payout details with payout ID                | [Payout](https://razorpay.com/docs/api/x/payouts/fetch-with-id) | ✅ |
| `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/) | ✅ |
| `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) | ✅ |


## Use Cases
- Workflow Automation: Automate your day to day workflow using Razorpay MCP Server.
- Agentic Applications: Building AI powered tools that interact with Razorpay's payment ecosystem using this Razorpay MCP server.

## Remote MCP Server (Recommended)

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.

### Benefits of Remote MCP Server

- **Zero Setup**: No need to install Docker, Go, or manage local infrastructure
- **Always Updated**: Automatically stays updated with the latest features and security patches
- **High Availability**: Backed by Razorpay's robust infrastructure with 99.9% uptime
- **Reduced Latency**: Optimized routing and caching for faster API responses
- **Enhanced Security**: Secure token-based authentication with automatic token rotation
- **No Maintenance**: No need to worry about updates, patches, or server maintenance

### Prerequisites

`npx` is needed to use mcp server.
You need to have Node.js installed on your system, which includes both `npm` (Node Package Manager) and `npx` (Node Package Execute) by default:

#### macOS
```bash
# Install Node.js (which includes npm and npx) using Homebrew
brew install node

# Alternatively, download from https://nodejs.org/
```

#### Windows
```bash
# Install Node.js (which includes npm and npx) using Chocolatey
choco install nodejs

# Alternatively, download from https://nodejs.org/
```

#### Verify Installation
```bash
npx --version
```

### Usage with Cursor

Inside your cursor settings in MCP, add this config.

```json
{
  "mcpServers": {
    "rzp-mcp-server": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://mcp.razorpay.com/mcp",
        "--header",
        "Authorization:${AUTH_HEADER}"
      ],
      "env": {
        "AUTH_HEADER": "Basic <Base64(key:secret)>"
      }
    }
  }
}
```

Replace `key` & `secret` with your Razorpay API KEY & API SECRET

### Usage with Claude Desktop

Add the following to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "rzp-mcp-server": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://mcp.razorpay.com/mcp",
        "--header",
        "Authorization: Basic <Merchant Token>"
      ]
    }
  }
}
```

Replace `<Merchant Token>` with your Razorpay merchant token. Check Authentication section for steps to generate token.

- Learn about how to configure MCP servers in Claude desktop: [Link](https://modelcontextprotocol.io/quickstart/user)
- How to install Claude Desktop: [Link](https://claude.ai/download)

### Usage with VS Code

Add the following to your VS Code settings (JSON):

```json
{
  "mcp": {
    "inputs": [
      {
        "type": "promptString",
        "id": "merchant_token",
        "description": "Razorpay Merchant Token",
        "password": true
      }
    ],
    "servers": {
      "razorpay-remote": {
        "command": "npx",
        "args": [
          "mcp-remote",
          "https://mcp.razorpay.com/mcp",
          "--header",
          "Authorization: Basic ${input:merchant_token}"
        ]
      }
    }
  }
}
```

Learn more about MCP servers in VS Code's [agent mode documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers).

## Authentication

The Remote MCP Server uses merchant token-based authentication. To generate your merchant token:

1. Go to the [Razorpay Dashboard](https://dashboard.razorpay.com/) and navigate to Settings > API Keys
2. Locate your API Key and API Secret:
   - API Key is visible on the dashboard
   - API Secret is generated only once when you first create it. **Important:** Do not generate a new secret if you already have one

3. Generate your merchant token by running this command in your terminal:
   ```bash
   echo <RAZORPAY_API_KEY>:<RAZORPAY_API_SECRET> | base64
   ```
   Replace `<RAZORPAY_API_KEY>` and `<RAZORPAY_API_SECRET>` with your actual credentials

4. Copy the base64-encoded output - this is your merchant token for the Remote MCP Server

> **Note:** For local MCP Server deployment, you can use the API Key and Secret directly without generating a merchant token.
     

## Local MCP Server

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.

### Prerequisites

- Docker
- Golang (Go)
- Git

To run the Razorpay MCP server, use one of the following methods:

### Using Public Docker Image (Recommended)

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.

> **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).


#### Usage with Claude Desktop

This will use the public razorpay image

Add the following to your `claude_desktop_config.json`:

```json
{
    "mcpServers": {
        "razorpay-mcp-server": {
            "command": "docker",
            "args": [
                "run",
                "--rm",
                "-i",
                "-e",
                "RAZORPAY_KEY_ID",
                "-e",
                "RAZORPAY_KEY_SECRET",
                "razorpay/mcp"
            ],
            "env": {
                "RAZORPAY_KEY_ID": "your_razorpay_key_id",
                "RAZORPAY_KEY_SECRET": "your_razorpay_key_secret"
            }
        }
    }
}
```
Please replace the `your_razorpay_key_id` and `your_razorpay_key_secret` with your keys.

- Learn about how to configure MCP servers in Claude desktop: [Link](https://modelcontextprotocol.io/quickstart/user)
- How to install Claude Desktop: [Link](https://claude.ai/download)

#### Usage with VS Code

Add the following to your VS Code settings (JSON):

```json
{
    "mcpServers": {
        "razorpay-mcp-server": {
            "command": "docker",
            "args": [
                "run",
                "--rm",
                "-i",
                "-e",
                "RAZORPAY_KEY_ID",
                "-e",
                "RAZORPAY_KEY_SECRET",
                "razorpay/mcp"
            ],
            "env": {
                "RAZORPAY_KEY_ID": "your_razorpay_key_id",
                "RAZORPAY_KEY_SECRET": "your_razorpay_key_secret"
            }
        }
    }
}
```
Please replace the `your_razorpay_key_id` and `your_razorpay_key_secret` with your keys.

- Learn about how to configure MCP servers in Claude desktop: [Link](https://modelcontextprotocol.io/quickstart/user)
- How to install Claude Desktop: [Link](https://claude.ai/download)

#### Usage with VS Code

Add the following to your VS Code settings (JSON):

```json
{
  "mcp": {
    "inputs": [
      {
        "type": "promptString",
        "id": "razorpay_key_id",
        "description": "Razorpay Key ID",
        "password": false
      },
      {
        "type": "promptString",
        "id": "razorpay_key_secret",
        "description": "Razorpay Key Secret",
        "password": true
      }
    ],
    "servers": {
      "razorpay": {
        "command": "docker",
        "args": [
          "run",
          "-i",
          "--rm",
          "-e",
          "RAZORPAY_KEY_ID",
          "-e",
          "RAZORPAY_KEY_SECRET",
          "razorpay/mcp"
        ],
        "env": {
          "RAZORPAY_KEY_ID": "${input:razorpay_key_id}",
          "RAZORPAY_KEY_SECRET": "${input:razorpay_key_secret}"
        }
      }
    }
  }
}
```

Learn more about MCP servers in VS Code's [agent mode documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers).

### Build from Docker (Alternative)

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.

```bash
# Run the server
git clone https://github.com/razorpay/razorpay-mcp-server.git
cd razorpay-mcp-server
docker build -t razorpay-mcp-server:latest .
```

Once the razorpay-mcp-server:latest docker image is built, you can replace the public image(`razorpay/mcp`) with it in the above configurations.

### Build from source

You can directly build from the source instead of using docker by following these steps:

```bash
# Clone the repository
git clone https://github.com/razorpay/razorpay-mcp-server.git
cd razorpay-mcp-server

# Build the binary
go build -o razorpay-mcp-server ./cmd/razorpay-mcp-server
```
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:

```json
{
  "razorpay": {
    "command": "/path/to/razorpay-mcp-server",
    "args": ["stdio","--log-file=/path/to/rzp-mcp.log"],
    "env": {
      "RAZORPAY_KEY_ID": "<YOUR_ID>",
      "RAZORPAY_KEY_SECRET" : "<YOUR_SECRET>"
    }
  }
}
```

## Configuration

The server requires the following configuration:

- `RAZORPAY_KEY_ID`: Your Razorpay API key ID
- `RAZORPAY_KEY_SECRET`: Your Razorpay API key secret
- `LOG_FILE` (optional): Path to log file for server logs
- `TOOLSETS` (optional): Comma-separated list of toolsets to enable (default: "all")
- `READ_ONLY` (optional): Run server in read-only mode (default: false)

### Command Line Flags

The server supports the following command line flags:

- `--key` or `-k`: Your Razorpay API key ID
- `--secret` or `-s`: Your Razorpay API key secret
- `--log-file` or `-l`: Path to log file
- `--toolsets` or `-t`: Comma-separated list of toolsets to enable
- `--read-only`: Run server in read-only mode

## Debugging the Server

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)

## License

This project is licensed under the terms of the MIT open source license. Please refer to [LICENSE](./LICENSE) for the full terms.

```

--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------

```markdown
# Security

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.

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.

## Reporting Security Issues

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).

Please refrain from disclosing vulnerabilities via public channels such as issues, discussions, or pull requests.

All vulnerability reports must be submitted via our [Hackerone program](https://hackerone.com/razorpay).

Please include as much of the information listed below as you can to help us better understand and resolve the issue:

-   The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting)
-   Full paths of source file(s) related to the manifestation of the issue
-   The location of the affected source code (tag/branch/commit or direct URL)
-   Any special configuration required to reproduce the issue
-   Step-by-step instructions to reproduce the issue
-   Proof-of-concept or exploit code (if possible)
-   Impact of the issue, including how an attacker might exploit the issue

This information will help us triage your report more quickly.

```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
# Contributing to Razorpay MCP Server

Thank you for your interest in contributing to the Razorpay MCP Server! This document outlines the process for contributing to this project.

## Code of Conduct

Please be respectful and considerate of others when contributing to this project. We strive to maintain a welcoming and inclusive environment for all contributors.

## TLDR;

```
make test
make fmt
make lint
make build
make run
```

We use Cursor to contribute - our AI developer. Look at `~/.cusor/rules`. It understands the standards we have defined and codes with that.

## Development Process

We use a fork-based workflow for all contributions:

1. **Fork the repository**: Start by forking the [razorpay-mcp-server repository](https://github.com/razorpay/razorpay-mcp-server) to your GitHub account.

2. **Clone your fork**: Clone your fork to your local machine:
   ```bash
   git clone https://github.com/YOUR-USERNAME/razorpay-mcp-server.git
   cd razorpay-mcp-server
   ```

3. **Add upstream remote**: Add the original repository as an "upstream" remote:
   ```bash
   git remote add upstream https://github.com/razorpay/razorpay-mcp-server.git
   ```

4. **Create a branch**: Create a new branch for your changes:
   ```bash
   git checkout -b username/feature
   ```
   Use a descriptive branch name that includes your username followed by a brief feature description.

5. **Make your changes**: Implement your changes, following the code style guidelines.

6. **Write tests**: Add tests for your changes when applicable.

7. **Run tests and linting**: Make sure all tests pass and the code meets our linting standards.

8. **Commit your changes**: Make commits with clear messages following this format:
   ```bash
   git commit -m "[type]: description of the change"
   ```
   Where `type` is one of:
   - `chore`: for tasks like adding linter config, GitHub Actions, addressing PR review comments, etc.
   - `feat`: for adding new features like a new fetch_payment tool
   - `fix`: for bug fixes
   - `ref`: for code refactoring
   - `test`: for adding UTs or E2Es
   
   Example: `git commit -m "feat: add payment verification tool"`

9. **Keep your branch updated**: Regularly sync your branch with the upstream repository:
   ```bash
   git fetch upstream
   git rebase upstream/main
   ```

10. **Push to your fork**: Push your changes to your fork:
    ```bash
    git push origin username/feature
    ```

11. **Create a Pull Request**: Open a pull request from your fork to the main repository.

## Pull Request Process

1. Fill out the pull request template with all relevant information.
2. Link any related issues in the pull request description.
3. Ensure all status checks pass.
4. Wait for review from maintainers.
5. Address any feedback from the code review.
6. Once approved, a maintainer will merge your changes.

## Local Development Setup

### Prerequisites

- Go 1.21 or later
- Docker (for containerized development)
- Git

### Setting up the Development Environment

1. Clone your fork of the repository (see above).

2. Install dependencies:
   ```bash
   go mod download
   ```

3. Set up your environment variables:
   ```bash
   export RAZORPAY_KEY_ID=your_key_id
   export RAZORPAY_KEY_SECRET=your_key_secret
   ```

### Running the Server Locally

There are `make` commands also available now for the below, refer TLDR; above.

To run the server in development mode:

```bash
go run ./cmd/razorpay-mcp-server/main.go stdio
```

### Running Tests

To run all tests:

```bash
go test ./...
```

To run tests with coverage:

```bash
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
```

## Code Quality and Linting

We use golangci-lint for code quality checks. To run the linter:

```bash
# Install golangci-lint if you don't have it
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

# Run the linter
golangci-lint run
```

Our linting configuration is defined in `.golangci.yaml` and includes:
- Code style checks (gofmt, goimports)
- Static analysis (gosimple, govet, staticcheck)
- Security checks (gosec)
- Complexity checks (gocyclo)
- And more

Please ensure your code passes all linting checks before submitting a pull request.

## Documentation

When adding new features or modifying existing ones, please update the documentation accordingly. This includes:

- Code comments
- README updates
- Tool documentation

## Adding New Tools

When adding a new tool to the Razorpay MCP Server:

1. Review the detailed developer guide at [pkg/razorpay/README.md](pkg/razorpay/README.md) for complete instructions and examples.
2. Create a new function in the appropriate resource file under `pkg/razorpay` (or create a new file if needed).
3. Implement the tool following the patterns in the developer guide.
4. Register the tool in `server.go`.
5. Add appropriate tests.
6. Update the main README.md to document the new tool.

The developer guide for tools includes:
- Tool structure and patterns
- Parameter definition and validation
- Examples for both GET and POST endpoints
- Best practices for naming and organization

## Releasing

Releases are managed by the maintainers. We use [GoReleaser](https://goreleaser.com/) for creating releases.

## Getting Help

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.

Thank you for contributing to the Razorpay MCP Server! 
```

--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------

```yaml
coverage:
  status:
    patch:
      default:
        target: 90.0
        threshold: 0.0
    project:
      default:
        target: 70.0
        threshold: 0.0

```

--------------------------------------------------------------------------------
/pkg/mcpgo/transport.go:
--------------------------------------------------------------------------------

```go
package mcpgo

import (
	"context"
	"io"
)

// TransportServer defines a server that can listen for MCP connections
type TransportServer interface {
	// Listen listens for connections
	Listen(ctx context.Context, in io.Reader, out io.Writer) error
}

```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------

```yaml
blank_issues_enabled: false
contact_links:
  - name: Questions, Help, or General Advice
    url: https://github.com/razorpay/razorpay-mcp-server/discussions
    about: For any questions, help, or general advice, please use GitHub Discussions instead of opening an issue
  - name: Documentation
    url: https://github.com/razorpay/razorpay-mcp-server#readme
    about: Check the documentation before reporting issues
  - name: Report Security Vulnerabilities
    url: https://razorpay.com/security/
    about: Please report security vulnerabilities according to our security policy 
```

--------------------------------------------------------------------------------
/.github/workflows/assign.yml:
--------------------------------------------------------------------------------

```yaml
name: Assign

on:
  issues:
    types: [opened, reopened]
  pull_request:
    types: [opened, reopened]

jobs:
  assign:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
        with:
          # Please add your name to assignees
          script: |
            github.rest.issues.addAssignees({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              assignees: ['KarthikBoddeda', 'ChiragChiranjib', 'stuckinforloop', 'alok87']
            })
```

--------------------------------------------------------------------------------
/pkg/razorpay/tools_test.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	"testing"

	rzpsdk "github.com/razorpay/razorpay-go"
)

func TestNewToolSets(t *testing.T) {
	// Create test observability
	obs := CreateTestObservability()

	// Create a test client
	client := &rzpsdk.Client{}

	// Test with empty enabled toolsets
	toolsetGroup, err := NewToolSets(obs, client, []string{}, false)
	if err != nil {
		t.Fatalf("NewToolSets failed: %v", err)
	}

	if toolsetGroup == nil {
		t.Fatal("NewToolSets returned nil toolset group")
	}

	// This test ensures that the FetchSavedPaymentMethods line is executed
	// providing the missing code coverage
}

```

--------------------------------------------------------------------------------
/pkg/contextkey/context_key.go:
--------------------------------------------------------------------------------

```go
package contextkey

import (
	"context"
)

// contextKey is a type used for context value keys to avoid key collisions.
type contextKey string

// Context keys for storing various values.
const (
	clientKey contextKey = "client"
)

// WithClient returns a new context with the client instance attached.
func WithClient(ctx context.Context, client interface{}) context.Context {
	return context.WithValue(ctx, clientKey, client)
}

// ClientFromContext extracts the client instance from the context.
// Returns nil if no client is found.
func ClientFromContext(ctx context.Context) interface{} {
	return ctx.Value(clientKey)
}

```

--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------

```yaml
name: Build
on: [push, pull_request]

permissions:
  contents: read

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]

    runs-on: ${{ matrix.os }}

    steps:
      - name: Check out code
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

      - name: Set up Go
        uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b
        with:
          go-version-file: "go.mod"

      - name: Download dependencies
        run: go mod download

      - name: Run unit tests
        run: go test -race ./...

      - name: Build
        run: go build -v ./cmd/razorpay-mcp-server
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------

```markdown
---
name: Feature Request
about: Suggest an idea for the MCP server
title: '[FEATURE] '
labels: enhancement
assignees: ''
---

## 🚀 Feature Description
<!-- A clear and concise description of the feature you're requesting -->

## 🤔 Problem Statement
<!-- Describe the problem this feature would solve -->

## 💡 Proposed Solution
<!-- Describe how you envision this feature working -->

## 🔄 Alternatives Considered
<!-- Describe any alternative solutions or features you've considered -->

## 📝 Additional Context
<!-- Add any other context, screenshots, or examples about the feature request here -->

---

> **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
---
name: Bug Report
about: Create a report to help us improve the MCP server
title: '[BUG] '
labels: bug
assignees: ''
---

## 🐛 Bug Description
<!-- A clear and concise description of what the bug is -->

## 🔍 Steps To Reproduce
1. 
2. 
3. 

## 🤔 Expected Behavior
<!-- A clear and concise description of what you expected to happen -->

## 📱 Environment
- OS version:
- Go version:
- Any other relevant environment details:

## 📝 Additional Context
<!-- Add any other context about the problem here -->

## 📊 Logs/Screenshots
<!-- If applicable, add logs or screenshots to help explain your problem -->

---

> **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
name: CI
on:
  push:
    branches:
      - main
    tags:
      - v[0-9]+.[0-9]+.[0-9]+*
  pull_request:
    branches:
      - main
jobs:
  test:
    name: Run tests and publish test coverage
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 2
      - uses: actions/setup-go@v4
        with:
          go-version: '1.23'

      - name: Run coverage
        run: |
         go test -race -covermode=atomic -coverprofile=coverage.out ./...    
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
           token: ${{ secrets.CODECOV_TOKEN }}
           files: ./coverage.out
           flags: unittests
           name: codecov-umbrella
           fail_ci_if_error: true
           verbose: true

```

--------------------------------------------------------------------------------
/pkg/observability/observability.go:
--------------------------------------------------------------------------------

```go
package observability

import (
	"github.com/razorpay/razorpay-mcp-server/pkg/log"
)

// Option is used make Observability
type Option func(*Observability)

// Observability holds all the observability related dependencies
type Observability struct {
	// Logger will be passed as dependency to other services
	// which will help in pushing logs
	Logger log.Logger
}

// New will create a new Observability object and
// apply all the options to that object and returns pointer to the object
func New(opts ...Option) *Observability {
	observability := &Observability{}
	// Loop through each option
	for _, opt := range opts {
		opt(observability)
	}
	return observability
}

// WithLoggingService will set the logging dependency in Deps
func WithLoggingService(s log.Logger) Option {
	return func(observe *Observability) {
		observe.Logger = s
	}
}

```

--------------------------------------------------------------------------------
/pkg/mcpgo/stdio.go:
--------------------------------------------------------------------------------

```go
package mcpgo

import (
	"context"
	"errors"
	"fmt"
	"io"

	"github.com/mark3labs/mcp-go/server"
)

// ErrInvalidServerImplementation indicates that the server
// implementation is not compatible
var ErrInvalidServerImplementation = errors.New(
	"invalid server implementation",
)

// NewStdioServer creates a new stdio transport server
func NewStdioServer(mcpServer Server) (*mark3labsStdioImpl, error) {
	sImpl, ok := mcpServer.(*Mark3labsImpl)
	if !ok {
		return nil, fmt.Errorf("%w: expected *Mark3labsImpl, got %T",
			ErrInvalidServerImplementation, mcpServer)
	}

	return &mark3labsStdioImpl{
		mcpStdioServer: server.NewStdioServer(sImpl.McpServer),
	}, nil
}

// mark3labsStdioImpl implements the TransportServer
// interface for stdio transport
type mark3labsStdioImpl struct {
	mcpStdioServer *server.StdioServer
}

// Listen implements the TransportServer interface
func (s *mark3labsStdioImpl) Listen(
	ctx context.Context, in io.Reader, out io.Writer) error {
	return s.mcpStdioServer.Listen(ctx, in, out)
}

```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
name: Release (GoReleaser)
on:
  push:
    tags:
      - "v*"
permissions:
  contents: write
  id-token: write
  attestations: write

jobs:
  release:
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

      - name: Set up Go
        uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b
        with:
          go-version-file: "go.mod"

      - name: Download dependencies
        run: go mod download

      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552
        with:
          distribution: goreleaser
          # GoReleaser version
          version: "~> v2"
          # Arguments to pass to GoReleaser
          args: release --clean
          workdir: .
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Generate signed build provenance attestations for workflow artifacts
        uses: actions/attest-build-provenance@v2
        with:
          subject-path: |
            dist/*.tar.gz
            dist/*.zip
            dist/*.txt
```

--------------------------------------------------------------------------------
/pkg/log/log.go:
--------------------------------------------------------------------------------

```go
package log

import (
	"context"
	"fmt"
	"os"
)

// Logger is an interface for logging, it is used internally
// at present but has scope for external implementations
//
//nolint:interfacebloat
type Logger interface {
	Infof(ctx context.Context, format string, args ...interface{})
	Errorf(ctx context.Context, format string, args ...interface{})
	Fatalf(ctx context.Context, format string, args ...interface{})
	Debugf(ctx context.Context, format string, args ...interface{})
	Warningf(ctx context.Context, format string, args ...interface{})
	Close() error
}

// New creates a new logger based on the provided configuration.
// It returns an enhanced context and a logger implementation.
// For stdio mode, it creates a file-based slog logger.
// For sse mode, it creates a stdout-based slog logger.
func New(ctx context.Context, config *Config) (context.Context, Logger) {
	var (
		logger Logger
		err    error
	)

	switch config.GetMode() {
	case ModeStdio:
		// For stdio mode, use slog logger that writes to file
		logger, err = NewSloggerWithFile(config.GetSlogConfig().GetPath())
		if err != nil {
			fmt.Printf("failed to initialize logger\n")
			os.Exit(1)
		}
	default:
		fmt.Printf("failed to initialize logger\n")
		os.Exit(1)
	}

	return ctx, logger
}

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
FROM golang:1.24.2-alpine AS builder

# Install git
RUN apk add --no-cache git

WORKDIR /app

COPY go.mod go.sum ./

RUN go mod download

COPY . .

# Build arguments with defaults
ARG VERSION="dev"
ARG COMMIT
ARG BUILD_DATE

# Use build args if provided, otherwise use fallbacks
RUN if [ -z "$COMMIT" ]; then \
        COMMIT=$(git rev-parse HEAD 2>/dev/null || echo 'unknown'); \
    fi && \
    if [ -z "$BUILD_DATE" ]; then \
        BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ); \
    fi && \
    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

FROM alpine:latest

RUN apk --no-cache add ca-certificates

# Create a non-root user to run the application
RUN addgroup -S rzpgroup && adduser -S rzp -G rzpgroup

WORKDIR /app

COPY --from=builder /app/razorpay-mcp-server .

# Change ownership of the application to the non-root user
RUN chown -R rzp:rzpgroup /app

ENV CONFIG="" \
    RAZORPAY_KEY_ID="" \
    RAZORPAY_KEY_SECRET="" \
    PORT="8090" \
    MODE="stdio" \
    LOG_FILE=""

# Switch to the non-root user
USER rzp

# Use shell form to allow variable substitution and conditional execution
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
## Description
<!-- Provide a brief summary of the changes introduced by this PR -->

## Related Issues
<!-- List any related issues that this PR addresses (e.g., "Fixes #123", "Resolves #456") -->

## Type of Change
<!-- Please delete options that are not relevant -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Refactoring (no functional changes, code improvements only)
- [ ] Documentation update
- [ ] Performance improvement

## Testing
<!-- Describe the tests you've performed to verify your changes -->
- [ ] Manual testing
- [ ] Added unit tests
- [ ] Added integration tests (if applicable)
- [ ] All tests pass locally

## Checklist
<!-- Please check all items that apply to this PR -->
- [ ] I have followed the code style of this project
- [ ] I have added comments to code where necessary, particularly in hard-to-understand areas
- [ ] I have updated the documentation where necessary
- [ ] I have verified that my changes do not introduce new warnings or errors
- [ ] I have checked for and resolved any merge conflicts
- [ ] I have considered the performance implications of my changes

## Additional Information
<!-- Add any additional context, screenshots, or information that might be helpful for reviewers --> 
```

--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------

```yaml
name: Lint
on:
  push:
  pull_request:

permissions:
  contents: read

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

      - name: Set up Go
        uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b
        with:
          go-version-file: 'go.mod'

      - name: Verify dependencies
        run: |
          go mod verify
          go mod download

          LINT_VERSION=1.64.8
          curl -fsSL https://github.com/golangci/golangci-lint/releases/download/v${LINT_VERSION}/golangci-lint-${LINT_VERSION}-linux-amd64.tar.gz | \
            tar xz --strip-components 1 --wildcards \*/golangci-lint
          mkdir -p bin && mv golangci-lint bin/

      - name: Run checks
        run: |
          STATUS=0
          assert-nothing-changed() {
            local diff
            "$@" >/dev/null || return 1
            if ! diff="$(git diff -U1 --color --exit-code)"; then
              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
              git checkout -- .
              STATUS=1
            fi
          }

          assert-nothing-changed go fmt ./...
          assert-nothing-changed go mod tidy

          bin/golangci-lint run --out-format=colored-line-number --timeout=3m || STATUS=$?

          exit $STATUS

```

--------------------------------------------------------------------------------
/pkg/razorpay/server.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	"context"
	"fmt"

	rzpsdk "github.com/razorpay/razorpay-go"

	"github.com/razorpay/razorpay-mcp-server/pkg/contextkey"
	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
)

func NewRzpMcpServer(
	obs *observability.Observability,
	client *rzpsdk.Client,
	enabledToolsets []string,
	readOnly bool,
	mcpOpts ...mcpgo.ServerOption,
) (mcpgo.Server, error) {
	// Validate required parameters
	if obs == nil {
		return nil, fmt.Errorf("observability is required")
	}
	if client == nil {
		return nil, fmt.Errorf("razorpay client is required")
	}

	// Set up default MCP options with Razorpay-specific hooks
	defaultOpts := []mcpgo.ServerOption{
		mcpgo.WithLogging(),
		mcpgo.WithResourceCapabilities(true, true),
		mcpgo.WithToolCapabilities(true),
		mcpgo.WithHooks(mcpgo.SetupHooks(obs)),
	}
	// Merge with user-provided options
	mcpOpts = append(defaultOpts, mcpOpts...)

	// Create server
	server := mcpgo.NewMcpServer("razorpay-mcp-server", "1.0.0", mcpOpts...)

	// Register Razorpay tools
	toolsets, err := NewToolSets(obs, client, enabledToolsets, readOnly)
	if err != nil {
		return nil, fmt.Errorf("failed to create toolsets: %w", err)
	}
	toolsets.RegisterTools(server)

	return server, nil
}

// getClientFromContextOrDefault returns either the provided default
// client or gets one from context.
func getClientFromContextOrDefault(
	ctx context.Context,
	defaultClient *rzpsdk.Client,
) (*rzpsdk.Client, error) {
	if defaultClient != nil {
		return defaultClient, nil
	}

	clientInterface := contextkey.ClientFromContext(ctx)
	if clientInterface == nil {
		return nil, fmt.Errorf("no client found in context")
	}

	client, ok := clientInterface.(*rzpsdk.Client)
	if !ok {
		return nil, fmt.Errorf("invalid client type in context")
	}

	return client, nil
}

```

--------------------------------------------------------------------------------
/.github/workflows/docker-publish.yml:
--------------------------------------------------------------------------------

```yaml
name: Docker Image Build & Push
on:
  push:
    branches: ["main", "sojinss4u/dockerimagebuild"]
    tags: ['v*.*.*']
  pull_request:
    branches: ["main"]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.PUBLIC_DOCKER_USERNAME }}
          password: ${{ secrets.PUBLIC_DOCKER_PASSWORD }}

      - name: Get Build Info
        id: build_info
        run: |
          TRIGGER_SHA=${{ github.event.pull_request.head.sha || github.sha }}
          echo "trigger_sha=${TRIGGER_SHA}" >> $GITHUB_OUTPUT
          
          # Generate build timestamp in UTC
          BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
          echo "build_date=${BUILD_DATE}" >> $GITHUB_OUTPUT

      - name: Determine Docker Tag
        id: vars
        run: |
          if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
            IMAGE_TAG="${GITHUB_REF#refs/tags/}"
            echo "tags=razorpay/mcp:${IMAGE_TAG},razorpay/mcp:latest" >> $GITHUB_OUTPUT
          else
            # Use the trigger SHA instead of the merge commit SHA
            IMAGE_TAG="${{ steps.build_info.outputs.trigger_sha }}"
            echo "tags=razorpay/mcp:${IMAGE_TAG}" >> $GITHUB_OUTPUT
          fi
      
      - name: Build & Push
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.vars.outputs.tags }}
          build-args: |
            VERSION=${{ github.ref_name }}
            COMMIT=${{ steps.build_info.outputs.trigger_sha }}
            BUILD_DATE=${{ steps.build_info.outputs.build_date }}

```

--------------------------------------------------------------------------------
/pkg/log/config.go:
--------------------------------------------------------------------------------

```go
package log

import (
	"log/slog"
)

// Logger modes
const (
	ModeStdio = "stdio"
)

// Config holds logger configuration with options pattern.
// Use NewConfig to create a new configuration with default values,
// then customize it using the With* option functions.
type Config struct {
	// mode determines the logger type (stdio or sse)
	mode string
	// Embedded configs for different logger types
	slog slogConfig
}

// slogConfig holds slog-specific configuration for stdio mode
type slogConfig struct {
	// path is the file path where logs will be written
	path string
	// logLevel sets the minimum log level to output
	logLevel slog.Leveler
}

// GetMode returns the logger mode (stdio or sse)
func (c Config) GetMode() string {
	return c.mode
}

// GetSlogConfig returns the slog logger configuration
func (c Config) GetSlogConfig() slogConfig {
	return c.slog
}

// GetLogLevel returns the log level
func (z Config) GetLogLevel() slog.Leveler {
	return z.slog.logLevel
}

// GetPath returns the log file path
func (s slogConfig) GetPath() string {
	return s.path
}

// ConfigOption represents a configuration option function
type ConfigOption func(*Config)

// WithMode sets the logger mode (stdio or sse)
func WithMode(mode string) ConfigOption {
	return func(c *Config) {
		c.mode = mode
	}
}

// WithLogPath sets the log file path
func WithLogPath(path string) ConfigOption {
	return func(c *Config) {
		c.slog.path = path
	}
}

// WithLogLevel sets the log level for the mode
func WithLogLevel(level slog.Level) ConfigOption {
	return func(c *Config) {
		c.slog.logLevel = level
	}
}

// NewConfig creates a new config with default values.
// By default, it uses stdio mode with info log level.
// Use With* options to customize the configuration.
func NewConfig(opts ...ConfigOption) *Config {
	config := &Config{
		mode: ModeStdio,
		slog: slogConfig{
			logLevel: slog.LevelInfo,
		},
	}

	for _, opt := range opts {
		opt(config)
	}

	return config
}

```

--------------------------------------------------------------------------------
/pkg/razorpay/mock/server.go:
--------------------------------------------------------------------------------

```go
package mock

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/gorilla/mux"
)

// Endpoint defines a route and its response
type Endpoint struct {
	Path     string
	Method   string
	Response interface{}
}

// NewHTTPClient creates and returns a mock HTTP client with configured
// endpoints
func NewHTTPClient(
	endpoints ...Endpoint,
) (*http.Client, *httptest.Server) {
	mockServer := NewServer(endpoints...)
	client := mockServer.Client()
	return client, mockServer
}

// NewServer creates a mock HTTP server for testing
func NewServer(endpoints ...Endpoint) *httptest.Server {
	router := mux.NewRouter()

	for _, endpoint := range endpoints {
		path := endpoint.Path
		method := endpoint.Method
		response := endpoint.Response

		router.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")

			if respMap, ok := response.(map[string]interface{}); ok {
				if _, hasError := respMap["error"]; hasError {
					w.WriteHeader(http.StatusBadRequest)
				} else {
					w.WriteHeader(http.StatusOK)
				}
			} else {
				w.WriteHeader(http.StatusOK)
			}

			switch resp := response.(type) {
			case []byte:
				_, err := w.Write(resp)
				if err != nil {
					http.Error(w, err.Error(), http.StatusInternalServerError)
				}
			case string:
				_, err := w.Write([]byte(resp))
				if err != nil {
					http.Error(w, err.Error(), http.StatusInternalServerError)
				}
			default:
				err := json.NewEncoder(w).Encode(resp)
				if err != nil {
					http.Error(w, err.Error(), http.StatusInternalServerError)
				}
			}
		}).Methods(method)
	}

	router.NotFoundHandler = http.HandlerFunc(
		func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusNotFound)

			_ = json.NewEncoder(w).Encode(map[string]interface{}{
				"error": map[string]interface{}{
					"code":        "NOT_FOUND",
					"description": fmt.Sprintf("No mock for %s %s", r.Method, r.URL.Path),
				},
			})
		})

	return httptest.NewServer(router)
}

```

--------------------------------------------------------------------------------
/cmd/razorpay-mcp-server/main.go:
--------------------------------------------------------------------------------

```go
//nolint:lll
package main

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

var (
	version = "version"
	commit  = "commit"
	date    = "date"
)

var cfgFile string

var rootCmd = &cobra.Command{
	Use:     "server",
	Short:   "Razorpay MCP Server",
	Version: fmt.Sprintf("%s\ncommit %s\ndate %s", version, commit, date),
}

// Execute runs the root command and handles any errors
func Execute() {
	err := rootCmd.Execute()
	if err != nil {
		os.Exit(1)
	}
}

func init() {
	cobra.OnInitialize(initConfig)

	// flags will be available for all subcommands
	rootCmd.PersistentFlags().StringP("key", "k", "", "your razorpay api key")
	rootCmd.PersistentFlags().StringP("secret", "s", "", "your razorpay api secret")
	rootCmd.PersistentFlags().StringP("log-file", "l", "", "path to the log file")
	rootCmd.PersistentFlags().StringSliceP("toolsets", "t", []string{}, "comma-separated list of toolsets to enable")
	rootCmd.PersistentFlags().Bool("read-only", false, "run server in read-only mode")

	// bind flags to viper
	_ = viper.BindPFlag("key", rootCmd.PersistentFlags().Lookup("key"))
	_ = viper.BindPFlag("secret", rootCmd.PersistentFlags().Lookup("secret"))
	_ = viper.BindPFlag("log_file", rootCmd.PersistentFlags().Lookup("log-file"))
	_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
	_ = viper.BindPFlag("read_only", rootCmd.PersistentFlags().Lookup("read-only"))

	// Set environment variable mappings
	_ = viper.BindEnv("key", "RAZORPAY_KEY_ID")        // Maps RAZORPAY_KEY_ID to key
	_ = viper.BindEnv("secret", "RAZORPAY_KEY_SECRET") // Maps RAZORPAY_KEY_SECRET to secret

	// Enable environment variable reading
	viper.AutomaticEnv()

	// subcommands
	rootCmd.AddCommand(stdioCmd)
}

// initConfig reads in config file and ENV variables if set.
func initConfig() {
	if cfgFile != "" {
		viper.SetConfigFile(cfgFile)
	} else {
		home, err := os.UserHomeDir()
		cobra.CheckErr(err)

		viper.AddConfigPath(home)
		viper.SetConfigType("yaml")
		viper.SetConfigName(".razorpay-mcp-server")
	}

	viper.AutomaticEnv()

	if err := viper.ReadInConfig(); err == nil {
		fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
	}
}

func main() {
	if err := rootCmd.Execute(); err != nil {
		os.Exit(1)
	}
}

```

--------------------------------------------------------------------------------
/pkg/log/slog_test.go:
--------------------------------------------------------------------------------

```go
package log

import (
	"context"
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestGetDefaultLogPath(t *testing.T) {
	path := getDefaultLogPath()

	assert.NotEmpty(t, path, "expected non-empty path")
	assert.True(t, filepath.IsAbs(path),
		"expected absolute path, got: %s", path)
}

func TestNewSlogger(t *testing.T) {
	logger, err := NewSlogger()
	require.NoError(t, err)
	require.NotNil(t, logger)

	// Test Close
	err = logger.Close()
	assert.NoError(t, err)
}

func TestNewSloggerWithFile(t *testing.T) {
	tests := []struct {
		name    string
		path    string
		wantErr bool
	}{
		{
			name:    "with empty path",
			path:    "",
			wantErr: false,
		},
		{
			name:    "with valid path",
			path:    filepath.Join(os.TempDir(), "test-log-file.log"),
			wantErr: false,
		},
		{
			name:    "with invalid path",
			path:    "/this/path/should/not/exist/log.txt",
			wantErr: false, // Should fallback to stderr
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Clean up test file after test
			if tt.path != "" {
				defer os.Remove(tt.path)
			}

			logger, err := NewSloggerWithFile(tt.path)
			if tt.wantErr {
				assert.Error(t, err)
				return
			}

			require.NoError(t, err)
			require.NotNil(t, logger)

			// Test logging
			ctx := context.Background()
			logger.Infof(ctx, "test message")
			logger.Debugf(ctx, "test debug")
			logger.Warningf(ctx, "test warning")
			logger.Errorf(ctx, "test error")

			// Test Close
			err = logger.Close()
			assert.NoError(t, err)

			// Verify file was created if path was specified
			if tt.path != "" && tt.path != "/this/path/should/not/exist/log.txt" {
				_, err := os.Stat(tt.path)
				assert.NoError(t, err, "log file should exist")
			}
		})
	}
}

func TestNew(t *testing.T) {
	tests := []struct {
		name   string
		config *Config
	}{
		{
			name: "stdio mode",
			config: NewConfig(
				WithMode(ModeStdio),
				WithLogPath(""),
			),
		},
		{
			name:   "default mode",
			config: NewConfig(),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			newCtx, logger := New(ctx, tt.config)

			require.NotNil(t, newCtx)
			require.NotNil(t, logger)

			// Test logging
			logger.Infof(ctx, "test message")
			logger.Debugf(ctx, "test debug")
			logger.Warningf(ctx, "test warning")
			logger.Errorf(ctx, "test error")

			// Test Close
			err := logger.Close()
			assert.NoError(t, err)
		})
	}
}

```

--------------------------------------------------------------------------------
/pkg/toolsets/toolsets.go:
--------------------------------------------------------------------------------

```go
package toolsets

import (
	"fmt"

	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
)

// Toolset represents a group of related tools
type Toolset struct {
	Name        string
	Description string
	Enabled     bool
	readOnly    bool
	writeTools  []mcpgo.Tool
	readTools   []mcpgo.Tool
}

// ToolsetGroup manages multiple toolsets
type ToolsetGroup struct {
	Toolsets     map[string]*Toolset
	everythingOn bool
	readOnly     bool
}

// NewToolset creates a new toolset with the given name and description
func NewToolset(name string, description string) *Toolset {
	return &Toolset{
		Name:        name,
		Description: description,
		Enabled:     false,
		readOnly:    false,
	}
}

// NewToolsetGroup creates a new toolset group
func NewToolsetGroup(readOnly bool) *ToolsetGroup {
	return &ToolsetGroup{
		Toolsets:     make(map[string]*Toolset),
		everythingOn: false,
		readOnly:     readOnly,
	}
}

// AddWriteTools adds write tools to the toolset
func (t *Toolset) AddWriteTools(tools ...mcpgo.Tool) *Toolset {
	if !t.readOnly {
		t.writeTools = append(t.writeTools, tools...)
	}
	return t
}

// AddReadTools adds read tools to the toolset
func (t *Toolset) AddReadTools(tools ...mcpgo.Tool) *Toolset {
	t.readTools = append(t.readTools, tools...)
	return t
}

// RegisterTools registers all active tools with the server
func (t *Toolset) RegisterTools(s mcpgo.Server) {
	if !t.Enabled {
		return
	}
	for _, tool := range t.readTools {
		s.AddTools(tool)
	}
	if !t.readOnly {
		for _, tool := range t.writeTools {
			s.AddTools(tool)
		}
	}
}

// AddToolset adds a toolset to the group
func (tg *ToolsetGroup) AddToolset(ts *Toolset) {
	if tg.readOnly {
		ts.readOnly = true
	}
	tg.Toolsets[ts.Name] = ts
}

// EnableToolset enables a specific toolset
func (tg *ToolsetGroup) EnableToolset(name string) error {
	toolset, exists := tg.Toolsets[name]
	if !exists {
		return fmt.Errorf("toolset %s does not exist", name)
	}
	toolset.Enabled = true
	return nil
}

// EnableToolsets enables multiple toolsets
func (tg *ToolsetGroup) EnableToolsets(names []string) error {
	if len(names) == 0 {
		tg.everythingOn = true
	}

	for _, name := range names {
		err := tg.EnableToolset(name)
		if err != nil {
			return err
		}
	}

	if tg.everythingOn {
		for name := range tg.Toolsets {
			err := tg.EnableToolset(name)
			if err != nil {
				return err
			}
		}
		return nil
	}

	return nil
}

// RegisterTools registers all active toolsets with the server
func (tg *ToolsetGroup) RegisterTools(s mcpgo.Server) {
	for _, toolset := range tg.Toolsets {
		toolset.RegisterTools(s)
	}
}

```

--------------------------------------------------------------------------------
/cmd/razorpay-mcp-server/stdio.go:
--------------------------------------------------------------------------------

```go
package main

import (
	"context"
	"fmt"
	"io"
	stdlog "log"
	"log/slog"
	"os"
	"os/signal"
	"syscall"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"

	rzpsdk "github.com/razorpay/razorpay-go"

	"github.com/razorpay/razorpay-mcp-server/pkg/log"
	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
	"github.com/razorpay/razorpay-mcp-server/pkg/razorpay"
)

// stdioCmd starts the mcp server in stdio transport mode
var stdioCmd = &cobra.Command{
	Use:   "stdio",
	Short: "start the stdio server",
	Run: func(cmd *cobra.Command, args []string) {
		logPath := viper.GetString("log_file")

		config := log.NewConfig(
			log.WithMode(log.ModeStdio),
			log.WithLogLevel(slog.LevelInfo),
			log.WithLogPath(logPath),
		)

		ctx, logger := log.New(context.Background(), config)

		// Create observability with SSE mode
		obs := observability.New(
			observability.WithLoggingService(logger),
		)

		key := viper.GetString("key")
		secret := viper.GetString("secret")
		client := rzpsdk.NewClient(key, secret)

		client.SetUserAgent("razorpay-mcp" + version + "/stdio")

		// Get toolsets to enable from config
		enabledToolsets := viper.GetStringSlice("toolsets")

		// Get read-only mode from config
		readOnly := viper.GetBool("read_only")

		err := runStdioServer(ctx, obs, client, enabledToolsets, readOnly)
		if err != nil {
			obs.Logger.Errorf(ctx,
				"error running stdio server", "error", err)
			stdlog.Fatalf("failed to run stdio server: %v", err)
		}
	},
}

func runStdioServer(
	ctx context.Context,
	obs *observability.Observability,
	client *rzpsdk.Client,
	enabledToolsets []string,
	readOnly bool,
) error {
	ctx, stop := signal.NotifyContext(
		ctx,
		os.Interrupt,
		syscall.SIGTERM,
	)
	defer stop()

	srv, err := razorpay.NewRzpMcpServer(obs, client, enabledToolsets, readOnly)
	if err != nil {
		return fmt.Errorf("failed to create server: %w", err)
	}

	stdioSrv, err := mcpgo.NewStdioServer(srv)
	if err != nil {
		return fmt.Errorf("failed to create stdio server: %w", err)
	}

	in, out := io.Reader(os.Stdin), io.Writer(os.Stdout)
	errC := make(chan error, 1)
	go func() {
		obs.Logger.Infof(ctx, "starting server")
		errC <- stdioSrv.Listen(ctx, in, out)
	}()

	_, _ = fmt.Fprintf(
		os.Stderr,
		"Razorpay MCP Server running on stdio\n",
	)

	// Wait for shutdown signal
	select {
	case <-ctx.Done():
		obs.Logger.Infof(ctx, "shutting down server...")
		return nil
	case err := <-errC:
		if err != nil {
			obs.Logger.Errorf(ctx, "server error", "error", err)
			return err
		}
		return nil
	}
}

```

--------------------------------------------------------------------------------
/pkg/razorpay/test_helpers.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/go-test/deep"
	"github.com/stretchr/testify/assert"

	rzpsdk "github.com/razorpay/razorpay-go"

	"github.com/razorpay/razorpay-mcp-server/pkg/log"
	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
)

// RazorpayToolTestCase defines a common structure for Razorpay tool tests
type RazorpayToolTestCase struct {
	Name           string
	Request        map[string]interface{}
	MockHttpClient func() (*http.Client, *httptest.Server)
	ExpectError    bool
	ExpectedResult map[string]interface{}
	ExpectedErrMsg string
}

// CreateTestObservability creates an observability stack suitable for testing
func CreateTestObservability() *observability.Observability {
	// Create a logger that discards output
	_, logger := log.New(context.Background(), log.NewConfig(
		log.WithMode(log.ModeStdio)),
	)
	return &observability.Observability{
		Logger: logger,
	}
}

// createMCPRequest creates a CallToolRequest with the given arguments
func createMCPRequest(args any) mcpgo.CallToolRequest {
	return mcpgo.CallToolRequest{
		Arguments: args,
	}
}

// newMockRzpClient configures a Razorpay client with a mock
// HTTP client for testing. It returns the configured client
// and the mock server (which should be closed by the caller)
func newMockRzpClient(
	mockHttpClient func() (*http.Client, *httptest.Server),
) (*rzpsdk.Client, *httptest.Server) {
	rzpMockClient := rzpsdk.NewClient("sample_key", "sample_secret")

	var mockServer *httptest.Server
	if mockHttpClient != nil {
		var client *http.Client
		client, mockServer = mockHttpClient()

		// This Request object is shared by reference across all
		// API resources in the client
		req := rzpMockClient.Order.Request
		req.BaseURL = mockServer.URL
		req.HTTPClient = client
	}

	return rzpMockClient, mockServer
}

// runToolTest executes a common test pattern for Razorpay tools
func runToolTest(
	t *testing.T,
	tc RazorpayToolTestCase,
	toolCreator func(*observability.Observability, *rzpsdk.Client) mcpgo.Tool,
	objectType string,
) {
	mockRzpClient, mockServer := newMockRzpClient(tc.MockHttpClient)
	if mockServer != nil {
		defer mockServer.Close()
	}

	obs := CreateTestObservability()
	tool := toolCreator(obs, mockRzpClient)

	request := createMCPRequest(tc.Request)
	result, err := tool.GetHandler()(context.Background(), request)

	assert.NoError(t, err)

	if tc.ExpectError {
		assert.NotNil(t, result)
		assert.Contains(t, result.Text, tc.ExpectedErrMsg)
		return
	}

	assert.NotNil(t, result)

	var returnedObj map[string]interface{}
	err = json.Unmarshal([]byte(result.Text), &returnedObj)
	assert.NoError(t, err)

	if diff := deep.Equal(tc.ExpectedResult, returnedObj); diff != nil {
		t.Errorf("%s mismatch: %s", objectType, diff)
	}
}

```

--------------------------------------------------------------------------------
/pkg/razorpay/payouts.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	"context"
	"fmt"

	rzpsdk "github.com/razorpay/razorpay-go"

	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
)

// FetchPayoutByID returns a tool that fetches a payout by its ID
func FetchPayout(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"payout_id",
			mcpgo.Description(
				"The unique identifier of the payout. For example, 'pout_00000000000001'",
			),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		FetchPayoutOptions := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(FetchPayoutOptions, "payout_id")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		payout, err := client.Payout.Fetch(
			FetchPayoutOptions["payout_id"].(string),
			nil,
			nil,
		)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching payout failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(payout)
	}

	return mcpgo.NewTool(
		"fetch_payout_with_id",
		"Fetch a payout's details using its ID",
		parameters,
		handler,
	)
}

// FetchAllPayouts returns a tool that fetches all payouts
func FetchAllPayouts(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"account_number",
			mcpgo.Description("The account from which the payouts were done."+
				"For example, 7878780080316316"),
			mcpgo.Required(),
		),
		mcpgo.WithNumber(
			"count",
			mcpgo.Description("Number of payouts to be fetched. Default value is 10."+
				"Maximum value is 100. This can be used for pagination,"+
				"in combination with the skip parameter"),
			mcpgo.Min(1),
		),
		mcpgo.WithNumber(
			"skip",
			mcpgo.Description("Numbers of payouts to be skipped. Default value is 0."+
				"This can be used for pagination, in combination with count"),
			mcpgo.Min(0),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		FetchAllPayoutsOptions := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(FetchAllPayoutsOptions, "account_number").
			ValidateAndAddPagination(FetchAllPayoutsOptions)

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		payout, err := client.Payout.All(FetchAllPayoutsOptions, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching payouts failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(payout)
	}

	return mcpgo.NewTool(
		"fetch_all_payouts",
		"Fetch all payouts for a bank account number",
		parameters,
		handler,
	)
}

```

--------------------------------------------------------------------------------
/pkg/razorpay/tools.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	rzpsdk "github.com/razorpay/razorpay-go"

	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
	"github.com/razorpay/razorpay-mcp-server/pkg/toolsets"
)

func NewToolSets(
	obs *observability.Observability,
	client *rzpsdk.Client,
	enabledToolsets []string,
	readOnly bool,
) (*toolsets.ToolsetGroup, error) {
	// Create a new toolset group
	toolsetGroup := toolsets.NewToolsetGroup(readOnly)

	// Create toolsets
	payments := toolsets.NewToolset("payments", "Razorpay Payments related tools").
		AddReadTools(
			FetchPayment(obs, client),
			FetchPaymentCardDetails(obs, client),
			FetchAllPayments(obs, client),
		).
		AddWriteTools(
			CapturePayment(obs, client),
			UpdatePayment(obs, client),
			InitiatePayment(obs, client),
			ResendOtp(obs, client),
			SubmitOtp(obs, client),
		)

	paymentLinks := toolsets.NewToolset(
		"payment_links",
		"Razorpay Payment Links related tools").
		AddReadTools(
			FetchPaymentLink(obs, client),
			FetchAllPaymentLinks(obs, client),
		).
		AddWriteTools(
			CreatePaymentLink(obs, client),
			CreateUpiPaymentLink(obs, client),
			ResendPaymentLinkNotification(obs, client),
			UpdatePaymentLink(obs, client),
		)

	orders := toolsets.NewToolset("orders", "Razorpay Orders related tools").
		AddReadTools(
			FetchOrder(obs, client),
			FetchAllOrders(obs, client),
			FetchOrderPayments(obs, client),
		).
		AddWriteTools(
			CreateOrder(obs, client),
			UpdateOrder(obs, client),
		)

	refunds := toolsets.NewToolset("refunds", "Razorpay Refunds related tools").
		AddReadTools(
			FetchRefund(obs, client),
			FetchMultipleRefundsForPayment(obs, client),
			FetchSpecificRefundForPayment(obs, client),
			FetchAllRefunds(obs, client),
		).
		AddWriteTools(
			CreateRefund(obs, client),
			UpdateRefund(obs, client),
		)

	payouts := toolsets.NewToolset("payouts", "Razorpay Payouts related tools").
		AddReadTools(
			FetchPayout(obs, client),
			FetchAllPayouts(obs, client),
		)

	qrCodes := toolsets.NewToolset("qr_codes", "Razorpay QR Codes related tools").
		AddReadTools(
			FetchQRCode(obs, client),
			FetchAllQRCodes(obs, client),
			FetchQRCodesByCustomerID(obs, client),
			FetchQRCodesByPaymentID(obs, client),
			FetchPaymentsForQRCode(obs, client),
		).
		AddWriteTools(
			CreateQRCode(obs, client),
			CloseQRCode(obs, client),
		)

	settlements := toolsets.NewToolset("settlements",
		"Razorpay Settlements related tools").
		AddReadTools(
			FetchSettlement(obs, client),
			FetchSettlementRecon(obs, client),
			FetchAllSettlements(obs, client),
			FetchAllInstantSettlements(obs, client),
			FetchInstantSettlement(obs, client),
		).
		AddWriteTools(
			CreateInstantSettlement(obs, client),
		)

	// Add the single custom tool to an existing toolset
	payments.AddReadTools(FetchSavedPaymentMethods(obs, client)).
		AddWriteTools(RevokeToken(obs, client))

	// Add toolsets to the group
	toolsetGroup.AddToolset(payments)
	toolsetGroup.AddToolset(paymentLinks)
	toolsetGroup.AddToolset(orders)
	toolsetGroup.AddToolset(refunds)
	toolsetGroup.AddToolset(payouts)
	toolsetGroup.AddToolset(qrCodes)
	toolsetGroup.AddToolset(settlements)

	// Enable the requested features
	if err := toolsetGroup.EnableToolsets(enabledToolsets); err != nil {
		return nil, err
	}

	return toolsetGroup, nil
}

```

--------------------------------------------------------------------------------
/pkg/mcpgo/server.go:
--------------------------------------------------------------------------------

```go
package mcpgo

import (
	"context"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"

	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
)

// Server defines the minimal MCP server interface needed by the application
type Server interface {
	// AddTools adds tools to the server
	AddTools(tools ...Tool)
}

// NewMcpServer creates a new MCP server
func NewMcpServer(name, version string, opts ...ServerOption) *Mark3labsImpl {
	// Create option setter to collect mcp options
	optSetter := &mark3labsOptionSetter{
		mcpOptions: []server.ServerOption{},
	}

	// Apply our options, which will populate the mcp options
	for _, opt := range opts {
		_ = opt(optSetter)
	}

	// Create the underlying mcp server
	mcpServer := server.NewMCPServer(
		name,
		version,
		optSetter.mcpOptions...,
	)

	return &Mark3labsImpl{
		McpServer: mcpServer,
		Name:      name,
		Version:   version,
	}
}

// Mark3labsImpl implements the Server interface using mark3labs/mcp-go
type Mark3labsImpl struct {
	McpServer *server.MCPServer
	Name      string
	Version   string
}

// mark3labsOptionSetter is used to apply options to the server
type mark3labsOptionSetter struct {
	mcpOptions []server.ServerOption
}

func (s *mark3labsOptionSetter) SetOption(option interface{}) error {
	if opt, ok := option.(server.ServerOption); ok {
		s.mcpOptions = append(s.mcpOptions, opt)
	}
	return nil
}

// AddTools adds tools to the server
func (s *Mark3labsImpl) AddTools(tools ...Tool) {
	// Convert our Tool to mcp's ServerTool
	var mcpTools []server.ServerTool
	for _, tool := range tools {
		mcpTools = append(mcpTools, tool.toMCPServerTool())
	}
	s.McpServer.AddTools(mcpTools...)
}

// OptionSetter is an interface for setting options on a configurable object
type OptionSetter interface {
	SetOption(option interface{}) error
}

// ServerOption is a function that configures a Server
type ServerOption func(OptionSetter) error

// WithLogging returns a server option that enables logging
func WithLogging() ServerOption {
	return func(s OptionSetter) error {
		return s.SetOption(server.WithLogging())
	}
}

func WithHooks(hooks *server.Hooks) ServerOption {
	return func(s OptionSetter) error {
		return s.SetOption(server.WithHooks(hooks))
	}
}

// WithResourceCapabilities returns a server option
// that enables resource capabilities
func WithResourceCapabilities(read, list bool) ServerOption {
	return func(s OptionSetter) error {
		return s.SetOption(server.WithResourceCapabilities(read, list))
	}
}

// WithToolCapabilities returns a server option that enables tool capabilities
func WithToolCapabilities(enabled bool) ServerOption {
	return func(s OptionSetter) error {
		return s.SetOption(server.WithToolCapabilities(enabled))
	}
}

// SetupHooks creates and configures the server hooks with logging
func SetupHooks(obs *observability.Observability) *server.Hooks {
	hooks := &server.Hooks{}
	hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod,
		message any) {
		obs.Logger.Infof(ctx, "MCP_METHOD_CALLED",
			"method", method,
			"id", id,
			"message", message)
	})

	hooks.AddOnSuccess(func(ctx context.Context, id any, method mcp.MCPMethod,
		message any, result any) {
		logResult := result
		if method == mcp.MethodToolsList {
			if r, ok := result.(*mcp.ListToolsResult); ok {
				simplifiedTools := make([]string, 0, len(r.Tools))
				for _, tool := range r.Tools {
					simplifiedTools = append(simplifiedTools, tool.Name)
				}
				// Create new map for logging with just the tool names
				logResult = map[string]interface{}{
					"tools": simplifiedTools,
				}
			}
		}

		obs.Logger.Infof(ctx, "MCP_METHOD_SUCCEEDED",
			"method", method,
			"id", id,
			"result", logResult)
	})

	hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod,
		message any, err error) {
		obs.Logger.Infof(ctx, "MCP_METHOD_FAILED",
			"method", method,
			"id", id,
			"message", message,
			"error", err)
	})

	hooks.AddBeforeCallTool(func(ctx context.Context, id any,
		message *mcp.CallToolRequest) {
		obs.Logger.Infof(ctx, "TOOL_CALL_STARTED",
			"id", id,
			"request", message)
	})

	hooks.AddAfterCallTool(func(ctx context.Context, id any,
		message *mcp.CallToolRequest, result *mcp.CallToolResult) {
		obs.Logger.Infof(ctx, "TOOL_CALL_COMPLETED",
			"id", id,
			"request", message,
			"result", result)
	})

	return hooks
}

```

--------------------------------------------------------------------------------
/pkg/log/slog.go:
--------------------------------------------------------------------------------

```go
package log

import (
	"context"
	"fmt"
	"log"
	"log/slog"
	"os"
	"path/filepath"
)

// slogLogger implements Logger interface using slog
type slogLogger struct {
	logger *slog.Logger
	closer func() error
}

// logWithLevel is a helper function that handles common logging functionality
func (s *slogLogger) logWithLevel(
	ctx context.Context,
	level slog.Level,
	format string,
	args ...interface{},
) {
	// Extract context fields and add them as slog attributes
	attrs := s.extractContextAttrs(ctx)

	// Convert args to slog attributes
	attrs = append(attrs, s.convertArgsToAttrs(args...)...)

	s.logger.LogAttrs(ctx, level, format, attrs...)
}

// Infof logs an info message with context fields
func (s *slogLogger) Infof(
	ctx context.Context, format string, args ...interface{}) {
	s.logWithLevel(ctx, slog.LevelInfo, format, args...)
}

// Errorf logs an error message with context fields
func (s *slogLogger) Errorf(
	ctx context.Context, format string, args ...interface{}) {
	s.logWithLevel(ctx, slog.LevelError, format, args...)
}

// Fatalf logs a fatal message with context fields and exits
func (s *slogLogger) Fatalf(
	ctx context.Context, format string, args ...interface{}) {
	s.logWithLevel(ctx, slog.LevelError, format, args...)
	os.Exit(1)
}

// Debugf logs a debug message with context fields
func (s *slogLogger) Debugf(
	ctx context.Context, format string, args ...interface{}) {
	s.logWithLevel(ctx, slog.LevelDebug, format, args...)
}

// Warningf logs a warning message with context fields
func (s *slogLogger) Warningf(
	ctx context.Context, format string, args ...interface{}) {
	s.logWithLevel(ctx, slog.LevelWarn, format, args...)
}

// extractContextAttrs extracts fields from context and converts to slog.Attr
func (s *slogLogger) extractContextAttrs(_ context.Context) []slog.Attr {
	// Always include all fields as attributes
	return []slog.Attr{}
}

// convertArgsToAttrs converts key-value pairs to slog.Attr
func (s *slogLogger) convertArgsToAttrs(args ...interface{}) []slog.Attr {
	if len(args) == 0 {
		return nil
	}

	var attrs []slog.Attr
	for i := 0; i < len(args)-1; i += 2 {
		if i+1 < len(args) {
			key, ok := args[i].(string)
			if !ok {
				continue
			}
			value := args[i+1]
			attrs = append(attrs, slog.Any(key, value))
		}
	}
	return attrs
}

// Close implements the Logger interface Close method
func (s *slogLogger) Close() error {
	if s.closer != nil {
		return s.closer()
	}
	return nil
}

// NewSlogger returns a new slog.Logger implementation of Logger interface.
// If path to log file is not provided then logger uses stderr for stdio mode
// If the log file cannot be opened, falls back to stderr
func NewSlogger() (*slogLogger, error) {
	// For stdio mode, always use stderr regardless of path
	// This ensures logs don't interfere with MCP protocol on stdout
	return &slogLogger{
		logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
			Level: slog.LevelInfo,
		})),
	}, nil
}

func NewSloggerWithStdout(config *Config) (*slogLogger, error) {
	// For stdio mode, always use Stdout regardless of path
	// This ensures logs don't interfere with MCP protocol on stdout
	return &slogLogger{
		logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
			Level: config.slog.logLevel,
		})),
	}, nil
}

// getDefaultLogPath returns an absolute path for the logs directory
func getDefaultLogPath() string {
	execPath, err := os.Executable()
	if err != nil {
		// Fallback to temp directory if we can't determine executable path
		return filepath.Join(os.TempDir(), "razorpay-mcp-server-logs")
	}

	execDir := filepath.Dir(execPath)

	return filepath.Join(execDir, "logs")
}

// NewSloggerWithFile returns a new slog.Logger.
// If path to log file is not provided then
// logger uses a default path next to the executable
// If the log file cannot be opened, falls back to stderr
//
// TODO: add redaction of sensitive data
func NewSloggerWithFile(path string) (*slogLogger, error) {
	if path == "" {
		path = getDefaultLogPath()
	}

	file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		// Fall back to stderr if we can't open the log file
		fmt.Fprintf(
			os.Stderr,
			"Warning: Failed to open log file: %v\nFalling back to stderr\n",
			err,
		)
		logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
		noop := func() error { return nil }
		return &slogLogger{
			logger: logger,
			closer: noop,
		}, nil
	}

	fmt.Fprintf(os.Stderr, "logs are stored in: %v\n", path)
	return &slogLogger{
		logger: slog.New(slog.NewTextHandler(file, nil)),
		closer: func() error {
			if err := file.Close(); err != nil {
				log.Printf("close log file: %v", err)
			}

			return nil
		},
	}, nil
}

```

--------------------------------------------------------------------------------
/pkg/razorpay/mock/server_test.go:
--------------------------------------------------------------------------------

```go
package mock

import (
	"encoding/json"
	"io"
	"net/http"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestNewHTTPClient(t *testing.T) {
	client, server := NewHTTPClient(
		Endpoint{
			Path:     "/test",
			Method:   "GET",
			Response: map[string]interface{}{"status": "ok"},
		},
	)
	defer server.Close()

	assert.NotNil(t, client)
	assert.NotNil(t, server)

	resp, err := client.Get(server.URL + "/test")
	assert.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))

	var result map[string]interface{}
	err = json.NewDecoder(resp.Body).Decode(&result)
	assert.NoError(t, err)
	assert.Equal(t, "ok", result["status"])
}

func TestNewServer(t *testing.T) {
	testCases := []struct {
		name           string
		endpoints      []Endpoint
		requestPath    string
		requestMethod  string
		expectedStatus int
		expectedBody   string
	}{
		{
			name: "successful GET with JSON response",
			endpoints: []Endpoint{
				{
					Path:     "/test",
					Method:   "GET",
					Response: map[string]interface{}{"result": "success"},
				},
			},
			requestPath:    "/test",
			requestMethod:  "GET",
			expectedStatus: http.StatusOK,
			expectedBody:   `{"result":"success"}`,
		},
		{
			name: "error response",
			endpoints: []Endpoint{
				{
					Path:   "/error",
					Method: "GET",
					Response: map[string]interface{}{
						"error": map[string]interface{}{
							"code":        "BAD_REQUEST",
							"description": "Test error",
						},
					},
				},
			},
			requestPath:    "/error",
			requestMethod:  "GET",
			expectedStatus: http.StatusBadRequest,
			expectedBody: `{"error":{"code":"BAD_REQUEST",` +
				`"description":"Test error"}}`,
		},
		{
			name: "string response",
			endpoints: []Endpoint{
				{
					Path:     "/string",
					Method:   "GET",
					Response: "plain text response",
				},
			},
			requestPath:    "/string",
			requestMethod:  "GET",
			expectedStatus: http.StatusOK,
			expectedBody:   "plain text response",
		},
		{
			name: "byte array response",
			endpoints: []Endpoint{
				{
					Path:     "/bytes",
					Method:   "POST",
					Response: []byte(`{"raw":"data"}`),
				},
			},
			requestPath:    "/bytes",
			requestMethod:  "POST",
			expectedStatus: http.StatusOK,
			expectedBody:   `{"raw":"data"}`,
		},
		{
			name:           "not found",
			endpoints:      []Endpoint{},
			requestPath:    "/nonexistent",
			requestMethod:  "GET",
			expectedStatus: http.StatusNotFound,
			expectedBody: `{"error":{"code":"NOT_FOUND",` +
				`"description":"No mock for GET /nonexistent"}}`,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			server := NewServer(tc.endpoints...)
			defer server.Close()

			var req *http.Request
			var err error
			if tc.requestMethod == "GET" {
				req, err = http.NewRequest(
					tc.requestMethod,
					server.URL+tc.requestPath,
					nil,
				)
			} else {
				req, err = http.NewRequest(
					tc.requestMethod,
					server.URL+tc.requestPath,
					strings.NewReader("test body"),
				)
			}
			assert.NoError(t, err)

			client := server.Client()
			resp, err := client.Do(req)
			assert.NoError(t, err)
			defer resp.Body.Close()

			assert.Equal(t, tc.expectedStatus, resp.StatusCode)

			body, err := io.ReadAll(resp.Body)
			assert.NoError(t, err)

			actualBody := strings.TrimSpace(string(body))
			if strings.HasPrefix(actualBody, "{") {
				var expected, actual interface{}
				err = json.Unmarshal([]byte(tc.expectedBody), &expected)
				assert.NoError(t, err)

				err = json.Unmarshal(body, &actual)
				assert.NoError(t, err)
				assert.Equal(t, expected, actual)
			} else {
				assert.Equal(t, tc.expectedBody, actualBody)
			}
		})
	}
}

func TestMultipleEndpoints(t *testing.T) {
	endpoints := []Endpoint{
		{
			Path:   "/path1",
			Method: "GET",
			Response: map[string]interface{}{
				"endpoint": "path1",
			},
		},
		{
			Path:   "/path2",
			Method: "POST",
			Response: map[string]interface{}{
				"endpoint": "path2",
			},
		},
	}

	server := NewServer(endpoints...)
	defer server.Close()

	client := server.Client()

	testCases := []struct {
		path          string
		method        string
		expectedValue string
	}{
		{"/path1", "GET", "path1"},
		{"/path2", "POST", "path2"},
	}
	for _, tc := range testCases {
		t.Run(tc.method+" "+tc.path, func(t *testing.T) {
			var (
				resp *http.Response
				err  error
			)

			if tc.method == "GET" {
				resp, err = client.Get(server.URL + tc.path)
			} else if tc.method == "POST" {
				resp, err = client.Post(server.URL+tc.path,
					"application/json", nil)
			}
			assert.NoError(t, err)
			defer resp.Body.Close()

			assert.Equal(t, http.StatusOK, resp.StatusCode)

			var result map[string]interface{}
			err = json.NewDecoder(resp.Body).Decode(&result)
			assert.NoError(t, err)
			assert.Equal(t, tc.expectedValue, result["endpoint"])
		})
	}
}

```

--------------------------------------------------------------------------------
/pkg/razorpay/tokens.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	"context"
	"fmt"

	rzpsdk "github.com/razorpay/razorpay-go"
	"github.com/razorpay/razorpay-go/constants"

	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
)

// FetchSavedPaymentMethods returns a tool that fetches saved cards
// using contact number
func FetchSavedPaymentMethods(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"contact",
			mcpgo.Description(
				"Contact number of the customer to fetch all saved payment methods for. "+
					"For example, 9876543210 or +919876543210"),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		// Get client from context or use default
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		validator := NewValidator(&r)

		// Validate required contact parameter
		contactValue, err := extractValueGeneric[string](&r, "contact", true)
		if err != nil {
			validator = validator.addError(err)
		} else if contactValue == nil || *contactValue == "" {
			validator = validator.addError(
				fmt.Errorf("missing required parameter: contact"))
		}
		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}
		contact := *contactValue
		customerData := map[string]interface{}{
			"contact":       contact,
			"fail_existing": "0", // Get existing customer if exists
		}

		// Create/get customer using Razorpay SDK
		customer, err := client.Customer.Create(customerData, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf(
					"Failed to create/fetch customer with contact %s: %v", contact, err,
				)), nil
		}

		customerID, ok := customer["id"].(string)
		if !ok {
			return mcpgo.NewToolResultError("Customer ID not found in response"), nil
		}

		url := fmt.Sprintf("/%s/customers/%s/tokens",
			constants.VERSION_V1, customerID)

		// Make the API request to get tokens
		tokensResponse, err := client.Request.Get(url, nil, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf(
					"Failed to fetch saved payment methods for customer %s: %v",
					customerID,
					err,
				)), nil
		}

		result := map[string]interface{}{
			"customer":              customer,
			"saved_payment_methods": tokensResponse,
		}
		return mcpgo.NewToolResultJSON(result)
	}

	return mcpgo.NewTool(
		"fetch_tokens",
		"Get all saved payment methods (cards, UPI)"+
			" for a contact number. "+
			"This tool first finds or creates a"+
			" customer with the given contact number, "+
			"then fetches all saved payment tokens "+
			"associated with that customer including "+
			"credit/debit cards, UPI IDs, digital wallets,"+
			" and other tokenized payment instruments.",
		parameters,
		handler,
	)
}

// RevokeToken returns a tool that revokes a saved payment token
func RevokeToken(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"customer_id",
			mcpgo.Description(
				"Customer ID for which the token should be revoked. "+
					"Must start with 'cust_' followed by alphanumeric characters. "+
					"Example: 'cust_xxx'"),
			mcpgo.Required(),
		),
		mcpgo.WithString(
			"token_id",
			mcpgo.Description(
				"Token ID of the saved payment method to be revoked. "+
					"Must start with 'token_' followed by alphanumeric characters. "+
					"Example: 'token_xxx'"),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		// Get client from context or use default
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		validator := NewValidator(&r)

		// Validate required customer_id parameter
		customerIDValue, err := extractValueGeneric[string](&r, "customer_id", true)
		if err != nil {
			validator = validator.addError(err)
		} else if customerIDValue == nil || *customerIDValue == "" {
			validator = validator.addError(
				fmt.Errorf("missing required parameter: customer_id"))
		}
		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}
		customerID := *customerIDValue

		// Validate required token_id parameter
		tokenIDValue, err := extractValueGeneric[string](&r, "token_id", true)
		if err != nil {
			validator = validator.addError(err)
		} else if tokenIDValue == nil || *tokenIDValue == "" {
			validator = validator.addError(
				fmt.Errorf("missing required parameter: token_id"))
		}
		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}
		tokenID := *tokenIDValue

		url := fmt.Sprintf(
			"/%s%s/%s/tokens/%s/cancel",
			constants.VERSION_V1,
			constants.CUSTOMER_URL,
			customerID,
			tokenID,
		)
		response, err := client.Token.Request.Put(url, nil, nil)

		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf(
					"Failed to revoke token %s for customer %s: %v",
					tokenID,
					customerID,
					err,
				)), nil
		}

		return mcpgo.NewToolResultJSON(response)
	}

	return mcpgo.NewTool(
		"revoke_token",
		"Revoke a saved payment method (token) for a customer. "+
			"This tool revokes the specified token "+
			"associated with the given customer ID. "+
			"Once revoked, the token cannot be used for future payments.",
		parameters,
		handler,
	)
}

```

--------------------------------------------------------------------------------
/pkg/razorpay/payouts_test.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/razorpay/razorpay-go/constants"

	"github.com/razorpay/razorpay-mcp-server/pkg/razorpay/mock"
)

func Test_FetchPayout(t *testing.T) {
	fetchPayoutPathFmt := fmt.Sprintf(
		"/%s%s/%%s",
		constants.VERSION_V1,
		constants.PAYOUT_URL,
	)

	successfulPayoutResp := map[string]interface{}{
		"id":     "pout_123",
		"entity": "payout",
		"fund_account": map[string]interface{}{
			"id":     "fa_123",
			"entity": "fund_account",
		},
		"amount":       float64(100000),
		"currency":     "INR",
		"notes":        map[string]interface{}{},
		"fees":         float64(0),
		"tax":          float64(0),
		"utr":          "123456789012345",
		"mode":         "IMPS",
		"purpose":      "payout",
		"processed_at": float64(1704067200),
		"created_at":   float64(1704067200),
		"updated_at":   float64(1704067200),
		"status":       "processed",
	}

	payoutNotFoundResp := map[string]interface{}{
		"error": map[string]interface{}{
			"code":        "BAD_REQUEST_ERROR",
			"description": "payout not found",
		},
	}

	tests := []RazorpayToolTestCase{
		{
			Name: "successful fetch",
			Request: map[string]interface{}{
				"payout_id": "pout_123",
			},
			MockHttpClient: func() (*http.Client, *httptest.Server) {
				return mock.NewHTTPClient(
					mock.Endpoint{
						Path:     fmt.Sprintf(fetchPayoutPathFmt, "pout_123"),
						Method:   "GET",
						Response: successfulPayoutResp,
					},
				)
			},
			ExpectError:    false,
			ExpectedResult: successfulPayoutResp,
		},
		{
			Name: "payout not found",
			Request: map[string]interface{}{
				"payout_id": "pout_invalid",
			},
			MockHttpClient: func() (*http.Client, *httptest.Server) {
				return mock.NewHTTPClient(
					mock.Endpoint{
						Path: fmt.Sprintf(
							fetchPayoutPathFmt,
							"pout_invalid",
						),
						Method:   "GET",
						Response: payoutNotFoundResp,
					},
				)
			},
			ExpectError:    true,
			ExpectedErrMsg: "fetching payout failed: payout not found",
		},
		{
			Name:           "missing payout_id parameter",
			Request:        map[string]interface{}{},
			MockHttpClient: nil, // No HTTP client needed for validation error
			ExpectError:    true,
			ExpectedErrMsg: "missing required parameter: payout_id",
		},
		{
			Name: "multiple validation errors",
			Request: map[string]interface{}{
				// Missing payout_id parameter
				"non_existent_param": 12345, // Additional parameter
			},
			MockHttpClient: nil, // No HTTP client needed for validation error
			ExpectError:    true,
			ExpectedErrMsg: "missing required parameter: payout_id",
		},
	}

	for _, tc := range tests {
		t.Run(tc.Name, func(t *testing.T) {
			runToolTest(t, tc, FetchPayout, "Payout")
		})
	}
}

func Test_FetchAllPayouts(t *testing.T) {
	fetchAllPayoutsPath := fmt.Sprintf(
		"/%s%s",
		constants.VERSION_V1,
		constants.PAYOUT_URL,
	)

	successfulPayoutsResp := map[string]interface{}{
		"entity": "collection",
		"count":  float64(2),
		"items": []interface{}{
			map[string]interface{}{
				"id":     "pout_1",
				"entity": "payout",
				"fund_account": map[string]interface{}{
					"id":     "fa_1",
					"entity": "fund_account",
				},
				"amount":       float64(100000),
				"currency":     "INR",
				"notes":        map[string]interface{}{},
				"fees":         float64(0),
				"tax":          float64(0),
				"utr":          "123456789012345",
				"mode":         "IMPS",
				"purpose":      "payout",
				"processed_at": float64(1704067200),
				"created_at":   float64(1704067200),
				"updated_at":   float64(1704067200),
				"status":       "processed",
			},
			map[string]interface{}{
				"id":     "pout_2",
				"entity": "payout",
				"fund_account": map[string]interface{}{
					"id":     "fa_2",
					"entity": "fund_account",
				},
				"amount":       float64(200000),
				"currency":     "INR",
				"notes":        map[string]interface{}{},
				"fees":         float64(0),
				"tax":          float64(0),
				"utr":          "123456789012346",
				"mode":         "IMPS",
				"purpose":      "payout",
				"processed_at": float64(1704067200),
				"created_at":   float64(1704067200),
				"updated_at":   float64(1704067200),
				"status":       "pending",
			},
		},
	}

	invalidAccountErrorResp := map[string]interface{}{
		"error": map[string]interface{}{
			"code":        "BAD_REQUEST_ERROR",
			"description": "Invalid account number",
		},
	}

	tests := []RazorpayToolTestCase{
		{
			Name: "successful fetch with pagination",
			Request: map[string]interface{}{
				"account_number": "409002173420",
				"count":          float64(10),
				"skip":           float64(0),
			},
			MockHttpClient: func() (*http.Client, *httptest.Server) {
				return mock.NewHTTPClient(
					mock.Endpoint{
						Path:     fetchAllPayoutsPath,
						Method:   "GET",
						Response: successfulPayoutsResp,
					},
				)
			},
			ExpectError:    false,
			ExpectedResult: successfulPayoutsResp,
		},
		{
			Name: "successful fetch without pagination",
			Request: map[string]interface{}{
				"account_number": "409002173420",
			},
			MockHttpClient: func() (*http.Client, *httptest.Server) {
				return mock.NewHTTPClient(
					mock.Endpoint{
						Path:     fetchAllPayoutsPath,
						Method:   "GET",
						Response: successfulPayoutsResp,
					},
				)
			},
			ExpectError:    false,
			ExpectedResult: successfulPayoutsResp,
		},
		{
			Name: "invalid account number",
			Request: map[string]interface{}{
				"account_number": "invalid_account",
			},
			MockHttpClient: func() (*http.Client, *httptest.Server) {
				return mock.NewHTTPClient(
					mock.Endpoint{
						Path:     fetchAllPayoutsPath,
						Method:   "GET",
						Response: invalidAccountErrorResp,
					},
				)
			},
			ExpectError:    true,
			ExpectedErrMsg: "fetching payouts failed: Invalid account number",
		},
		{
			Name: "missing account_number parameter",
			Request: map[string]interface{}{
				"count": float64(10),
				"skip":  float64(0),
			},
			MockHttpClient: nil, // No HTTP client needed for validation error
			ExpectError:    true,
			ExpectedErrMsg: "missing required parameter: account_number",
		},
		{
			Name: "multiple validation errors",
			Request: map[string]interface{}{
				// Missing account_number parameter
				"count": "10", // Wrong type for count
				"skip":  "0",  // Wrong type for skip
			},
			MockHttpClient: nil, // No HTTP client needed for validation error
			ExpectError:    true,
			ExpectedErrMsg: "Validation errors:\n- " +
				"missing required parameter: account_number\n- " +
				"invalid parameter type: count\n- " +
				"invalid parameter type: skip",
		},
	}

	for _, tc := range tests {
		t.Run(tc.Name, func(t *testing.T) {
			runToolTest(t, tc, FetchAllPayouts, "Payouts")
		})
	}
}

```

--------------------------------------------------------------------------------
/pkg/razorpay/refunds.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	"context"
	"fmt"

	rzpsdk "github.com/razorpay/razorpay-go"

	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
)

// CreateRefund returns a tool that creates a normal refund for a payment
func CreateRefund(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"payment_id",
			mcpgo.Description("Unique identifier of the payment which "+
				"needs to be refunded. ID should have a pay_ prefix."),
			mcpgo.Required(),
		),
		mcpgo.WithNumber(
			"amount",
			mcpgo.Description("Payment amount in the smallest currency unit "+
				"(e.g., for ₹295, use 29500)"),
			mcpgo.Required(),
			mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency)
		),
		mcpgo.WithString(
			"speed",
			mcpgo.Description("The speed at which the refund is to be "+
				"processed. Default is 'normal'. For instant refunds, speed "+
				"is set as 'optimum'."),
		),
		mcpgo.WithObject(
			"notes",
			mcpgo.Description("Key-value pairs used to store additional "+
				"information. A maximum of 15 key-value pairs can be included."),
		),
		mcpgo.WithString(
			"receipt",
			mcpgo.Description("A unique identifier provided by you for "+
				"your internal reference."),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		// Get client from context or use default
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		payload := make(map[string]interface{})
		data := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(payload, "payment_id").
			ValidateAndAddRequiredFloat(payload, "amount").
			ValidateAndAddOptionalString(data, "speed").
			ValidateAndAddOptionalString(data, "receipt").
			ValidateAndAddOptionalMap(data, "notes")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		refund, err := client.Payment.Refund(
			payload["payment_id"].(string),
			int(payload["amount"].(float64)), data, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("creating refund failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(refund)
	}

	return mcpgo.NewTool(
		"create_refund",
		"Use this tool to create a normal refund for a payment. "+
			"Amount should be in the smallest currency unit "+
			"(e.g., for ₹295, use 29500)",
		parameters,
		handler,
	)
}

// FetchRefund returns a tool that fetches a refund by ID
func FetchRefund(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"refund_id",
			mcpgo.Description(
				"Unique identifier of the refund which is to be retrieved. "+
					"ID should have a rfnd_ prefix."),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		// Get client from context or use default
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		payload := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(payload, "refund_id")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		refund, err := client.Refund.Fetch(payload["refund_id"].(string), nil, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching refund failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(refund)
	}

	return mcpgo.NewTool(
		"fetch_refund",
		"Use this tool to retrieve the details of a specific refund using its id.",
		parameters,
		handler,
	)
}

// UpdateRefund returns a tool that updates a refund's notes
func UpdateRefund(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"refund_id",
			mcpgo.Description("Unique identifier of the refund which "+
				"needs to be updated. ID should have a rfnd_ prefix."),
			mcpgo.Required(),
		),
		mcpgo.WithObject(
			"notes",
			mcpgo.Description("Key-value pairs used to store additional "+
				"information. A maximum of 15 key-value pairs can be included, "+
				"with each value not exceeding 256 characters."),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		// Get client from context or use default
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		payload := make(map[string]interface{})
		data := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(payload, "refund_id").
			ValidateAndAddRequiredMap(data, "notes")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		refund, err := client.Refund.Update(payload["refund_id"].(string), data, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("updating refund failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(refund)
	}

	return mcpgo.NewTool(
		"update_refund",
		"Use this tool to update the notes for a specific refund. "+
			"Only the notes field can be modified.",
		parameters,
		handler,
	)
}

// FetchMultipleRefundsForPayment returns a tool that fetches multiple refunds
// for a payment
func FetchMultipleRefundsForPayment(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"payment_id",
			mcpgo.Description("Unique identifier of the payment for which "+
				"refunds are to be retrieved. ID should have a pay_ prefix."),
			mcpgo.Required(),
		),
		mcpgo.WithNumber(
			"from",
			mcpgo.Description("Unix timestamp at which the refunds were created."),
		),
		mcpgo.WithNumber(
			"to",
			mcpgo.Description("Unix timestamp till which the refunds were created."),
		),
		mcpgo.WithNumber(
			"count",
			mcpgo.Description("The number of refunds to fetch for the payment."),
		),
		mcpgo.WithNumber(
			"skip",
			mcpgo.Description("The number of refunds to be skipped for the payment."),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		fetchReq := make(map[string]interface{})
		fetchOptions := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(fetchReq, "payment_id").
			ValidateAndAddOptionalInt(fetchOptions, "from").
			ValidateAndAddOptionalInt(fetchOptions, "to").
			ValidateAndAddPagination(fetchOptions)

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		refunds, err := client.Payment.FetchMultipleRefund(
			fetchReq["payment_id"].(string), fetchOptions, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching multiple refunds failed: %s",
					err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(refunds)
	}

	return mcpgo.NewTool(
		"fetch_multiple_refunds_for_payment",
		"Use this tool to retrieve multiple refunds for a payment. "+
			"By default, only the last 10 refunds are returned.",
		parameters,
		handler,
	)
}

// FetchSpecificRefundForPayment returns a tool that fetches a specific refund
// for a payment
func FetchSpecificRefundForPayment(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"payment_id",
			mcpgo.Description("Unique identifier of the payment for which "+
				"the refund has been made. ID should have a pay_ prefix."),
			mcpgo.Required(),
		),
		mcpgo.WithString(
			"refund_id",
			mcpgo.Description("Unique identifier of the refund to be retrieved. "+
				"ID should have a rfnd_ prefix."),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		params := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(params, "payment_id").
			ValidateAndAddRequiredString(params, "refund_id")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		refund, err := client.Payment.FetchRefund(
			params["payment_id"].(string),
			params["refund_id"].(string),
			nil, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching specific refund for payment failed: %s",
					err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(refund)
	}

	return mcpgo.NewTool(
		"fetch_specific_refund_for_payment",
		"Use this tool to retrieve details of a specific refund made for a payment.",
		parameters,
		handler,
	)
}

// FetchAllRefunds returns a tool that fetches all refunds with pagination
// support
func FetchAllRefunds(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithNumber(
			"from",
			mcpgo.Description("Unix timestamp at which the refunds were created"),
		),
		mcpgo.WithNumber(
			"to",
			mcpgo.Description("Unix timestamp till which the refunds were created"),
		),
		mcpgo.WithNumber(
			"count",
			mcpgo.Description("The number of refunds to fetch. "+
				"You can fetch a maximum of 100 refunds"),
		),
		mcpgo.WithNumber(
			"skip",
			mcpgo.Description("The number of refunds to be skipped"),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		queryParams := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddOptionalInt(queryParams, "from").
			ValidateAndAddOptionalInt(queryParams, "to").
			ValidateAndAddPagination(queryParams)

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		refunds, err := client.Refund.All(queryParams, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching refunds failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(refunds)
	}

	return mcpgo.NewTool(
		"fetch_all_refunds",
		"Use this tool to retrieve details of all refunds. "+
			"By default, only the last 10 refunds are returned.",
		parameters,
		handler,
	)
}

```

--------------------------------------------------------------------------------
/pkg/razorpay/settlements.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	"context"
	"fmt"

	rzpsdk "github.com/razorpay/razorpay-go"

	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
)

// FetchSettlement returns a tool that fetches a settlement by ID
func FetchSettlement(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"settlement_id",
			mcpgo.Description("The ID of the settlement to fetch."+
				"ID starts with the 'setl_'"),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		// Create a parameters map to collect validated parameters
		fetchSettlementOptions := make(map[string]interface{})

		// Validate using fluent validator
		validator := NewValidator(&r).
			ValidateAndAddRequiredString(fetchSettlementOptions, "settlement_id")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		settlementID := fetchSettlementOptions["settlement_id"].(string)
		settlement, err := client.Settlement.Fetch(settlementID, nil, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching settlement failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(settlement)
	}

	return mcpgo.NewTool(
		"fetch_settlement_with_id",
		"Fetch details of a specific settlement using its ID",
		parameters,
		handler,
	)
}

// FetchSettlementRecon returns a tool that fetches settlement
// reconciliation reports
func FetchSettlementRecon(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithNumber(
			"year",
			mcpgo.Description("Year for which the settlement report is "+
				"requested (YYYY format)"),
			mcpgo.Required(),
		),
		mcpgo.WithNumber(
			"month",
			mcpgo.Description("Month for which the settlement report is "+
				"requested (MM format)"),
			mcpgo.Required(),
		),
		mcpgo.WithNumber(
			"day",
			mcpgo.Description("Optional: Day for which the settlement report is "+
				"requested (DD format)"),
		),
		mcpgo.WithNumber(
			"count",
			mcpgo.Description("Optional: Number of records to fetch "+
				"(default: 10, max: 100)"),
		),
		mcpgo.WithNumber(
			"skip",
			mcpgo.Description("Optional: Number of records to skip for pagination"),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		// Create a parameters map to collect validated parameters
		fetchReconOptions := make(map[string]interface{})

		// Validate using fluent validator
		validator := NewValidator(&r).
			ValidateAndAddRequiredInt(fetchReconOptions, "year").
			ValidateAndAddRequiredInt(fetchReconOptions, "month").
			ValidateAndAddOptionalInt(fetchReconOptions, "day").
			ValidateAndAddPagination(fetchReconOptions)

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		report, err := client.Settlement.Reports(fetchReconOptions, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching settlement reconciliation report failed: %s",
					err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(report)
	}

	return mcpgo.NewTool(
		"fetch_settlement_recon_details",
		"Fetch settlement reconciliation report for a specific time period",
		parameters,
		handler,
	)
}

// FetchAllSettlements returns a tool to fetch multiple settlements with
// filtering and pagination
func FetchAllSettlements(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		// Pagination parameters
		mcpgo.WithNumber(
			"count",
			mcpgo.Description("Number of settlement records to fetch "+
				"(default: 10, max: 100)"),
			mcpgo.Min(1),
			mcpgo.Max(100),
		),
		mcpgo.WithNumber(
			"skip",
			mcpgo.Description("Number of settlement records to skip (default: 0)"),
			mcpgo.Min(0),
		),
		// Time range filters
		mcpgo.WithNumber(
			"from",
			mcpgo.Description("Unix timestamp (in seconds) from when "+
				"settlements are to be fetched"),
			mcpgo.Min(0),
		),
		mcpgo.WithNumber(
			"to",
			mcpgo.Description("Unix timestamp (in seconds) up till when "+
				"settlements are to be fetched"),
			mcpgo.Min(0),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		// Create parameters map to collect validated parameters
		fetchAllSettlementsOptions := make(map[string]interface{})

		// Validate using fluent validator
		validator := NewValidator(&r).
			ValidateAndAddPagination(fetchAllSettlementsOptions).
			ValidateAndAddOptionalInt(fetchAllSettlementsOptions, "from").
			ValidateAndAddOptionalInt(fetchAllSettlementsOptions, "to")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		// Fetch all settlements using Razorpay SDK
		settlements, err := client.Settlement.All(fetchAllSettlementsOptions, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching settlements failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(settlements)
	}

	return mcpgo.NewTool(
		"fetch_all_settlements",
		"Fetch all settlements with optional filtering and pagination",
		parameters,
		handler,
	)
}

// CreateInstantSettlement returns a tool that creates an instant settlement
func CreateInstantSettlement(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithNumber(
			"amount",
			mcpgo.Description("The amount you want to get settled instantly in amount in the smallest "+ //nolint:lll
				"currency sub-unit (e.g., for ₹295, use 29500)"),
			mcpgo.Required(),
			mcpgo.Min(200), // Minimum amount is 200 (₹2)
		),
		mcpgo.WithBoolean(
			"settle_full_balance",
			mcpgo.Description("If true, Razorpay will settle the maximum amount "+
				"possible and ignore amount parameter"),
			mcpgo.DefaultValue(false),
		),
		mcpgo.WithString(
			"description",
			mcpgo.Description("Custom note for the instant settlement."),
			mcpgo.Max(30),
			mcpgo.Pattern("^[a-zA-Z0-9 ]*$"),
		),
		mcpgo.WithObject(
			"notes",
			mcpgo.Description("Key-value pairs for additional information. "+
				"Max 15 pairs, 256 chars each"),
			mcpgo.MaxProperties(15),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		// Create parameters map to collect validated parameters
		createInstantSettlementReq := make(map[string]interface{})

		// Validate using fluent validator
		validator := NewValidator(&r).
			ValidateAndAddRequiredInt(createInstantSettlementReq, "amount").
			ValidateAndAddOptionalBool(createInstantSettlementReq, "settle_full_balance"). // nolint:lll
			ValidateAndAddOptionalString(createInstantSettlementReq, "description").
			ValidateAndAddOptionalMap(createInstantSettlementReq, "notes")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		// Create the instant settlement
		settlement, err := client.Settlement.CreateOnDemandSettlement(
			createInstantSettlementReq, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("creating instant settlement failed: %s",
					err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(settlement)
	}

	return mcpgo.NewTool(
		"create_instant_settlement",
		"Create an instant settlement to get funds transferred to your bank account", // nolint:lll
		parameters,
		handler,
	)
}

// FetchAllInstantSettlements returns a tool to fetch all instant settlements
// with filtering and pagination
func FetchAllInstantSettlements(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		// Pagination parameters
		mcpgo.WithNumber(
			"count",
			mcpgo.Description("Number of instant settlement records to fetch "+
				"(default: 10, max: 100)"),
			mcpgo.Min(1),
			mcpgo.Max(100),
		),
		mcpgo.WithNumber(
			"skip",
			mcpgo.Description("Number of instant settlement records to skip (default: 0)"), //nolint:lll
			mcpgo.Min(0),
		),
		// Time range filters
		mcpgo.WithNumber(
			"from",
			mcpgo.Description("Unix timestamp (in seconds) from when "+
				"instant settlements are to be fetched"),
			mcpgo.Min(0),
		),
		mcpgo.WithNumber(
			"to",
			mcpgo.Description("Unix timestamp (in seconds) up till when "+
				"instant settlements are to be fetched"),
			mcpgo.Min(0),
		),
		// Expand parameter for payout details
		mcpgo.WithArray(
			"expand",
			mcpgo.Description("Pass this if you want to fetch payout details "+
				"as part of the response for all instant settlements. "+
				"Supported values: ondemand_payouts"),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		// Create parameters map to collect validated parameters
		options := make(map[string]interface{})

		// Validate using fluent validator
		validator := NewValidator(&r).
			ValidateAndAddPagination(options).
			ValidateAndAddExpand(options).
			ValidateAndAddOptionalInt(options, "from").
			ValidateAndAddOptionalInt(options, "to")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		// Fetch all instant settlements using Razorpay SDK
		settlements, err := client.Settlement.FetchAllOnDemandSettlement(options, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching instant settlements failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(settlements)
	}

	return mcpgo.NewTool(
		"fetch_all_instant_settlements",
		"Fetch all instant settlements with optional filtering, pagination, and payout details", //nolint:lll
		parameters,
		handler,
	)
}

// FetchInstantSettlement returns a tool that fetches instant settlement by ID
func FetchInstantSettlement(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"settlement_id",
			mcpgo.Description("The ID of the instant settlement to fetch. "+
				"ID starts with 'setlod_'"),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		// Create parameters map to collect validated parameters
		params := make(map[string]interface{})

		// Validate using fluent validator
		validator := NewValidator(&r).
			ValidateAndAddRequiredString(params, "settlement_id")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		settlementID := params["settlement_id"].(string)

		// Fetch the instant settlement by ID using SDK
		settlement, err := client.Settlement.FetchOnDemandSettlementById(
			settlementID, nil, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching instant settlement failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(settlement)
	}

	return mcpgo.NewTool(
		"fetch_instant_settlement_with_id",
		"Fetch details of a specific instant settlement using its ID",
		parameters,
		handler,
	)
}

```

--------------------------------------------------------------------------------
/pkg/razorpay/orders.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	"context"
	"fmt"

	rzpsdk "github.com/razorpay/razorpay-go"

	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
)

// CreateOrder returns a tool that creates new orders in Razorpay
func CreateOrder(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithNumber(
			"amount",
			mcpgo.Description("Payment amount in the smallest "+
				"currency sub-unit (e.g., for ₹295, use 29500)"),
			mcpgo.Required(),
			mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency)
		),
		mcpgo.WithString(
			"currency",
			mcpgo.Description("ISO code for the currency "+
				"(e.g., INR, USD, SGD)"),
			mcpgo.Required(),
			mcpgo.Pattern("^[A-Z]{3}$"), // ISO currency codes are 3 uppercase letters
		),
		mcpgo.WithString(
			"receipt",
			mcpgo.Description("Receipt number for internal "+
				"reference (max 40 chars, must be unique)"),
			mcpgo.Max(40),
		),
		mcpgo.WithObject(
			"notes",
			mcpgo.Description("Key-value pairs for additional "+
				"information (max 15 pairs, 256 chars each)"),
			mcpgo.MaxProperties(15),
		),
		mcpgo.WithBoolean(
			"partial_payment",
			mcpgo.Description("Whether the customer can make partial payments"),
			mcpgo.DefaultValue(false),
		),
		mcpgo.WithNumber(
			"first_payment_min_amount",
			mcpgo.Description("Minimum amount for first partial "+
				"payment (only if partial_payment is true)"),
			mcpgo.Min(100),
		),
		mcpgo.WithArray(
			"transfers",
			mcpgo.Description("Array of transfer objects for distributing "+
				"payment amounts among multiple linked accounts. Each transfer "+
				"object should contain: account (linked account ID), amount "+
				"(in currency subunits), currency (ISO code), and optional fields "+
				"like notes, linked_account_notes, on_hold, on_hold_until"),
		),
		mcpgo.WithString(
			"method",
			mcpgo.Description("Payment method for mandate orders. "+
				"REQUIRED for mandate orders. Must be 'upi' when using "+
				"token.type='single_block_multiple_debit'. This field is used "+
				"only for mandate/recurring payment orders."),
		),
		mcpgo.WithString(
			"customer_id",
			mcpgo.Description("Customer ID for mandate orders. "+
				"REQUIRED for mandate orders. Must start with 'cust_' followed by "+
				"alphanumeric characters. Example: 'cust_xxx'. "+
				"This identifies the customer for recurring payments."),
		),
		mcpgo.WithObject(
			"token",
			mcpgo.Description("Token object for mandate orders. "+
				"REQUIRED for mandate orders. Must contain: max_amount "+
				"(positive number, maximum debit amount), frequency "+
				"(as_presented/monthly/one_time/yearly/weekly/daily), "+
				"type='single_block_multiple_debit' (only supported type), "+
				"and optionally expire_at (Unix timestamp, defaults to today+60days). "+
				"Example: {\"max_amount\": 100, \"frequency\": \"as_presented\", "+
				"\"type\": \"single_block_multiple_debit\"}"),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		// Get client from context or use default
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		payload := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredFloat(payload, "amount").
			ValidateAndAddRequiredString(payload, "currency").
			ValidateAndAddOptionalString(payload, "receipt").
			ValidateAndAddOptionalMap(payload, "notes").
			ValidateAndAddOptionalBool(payload, "partial_payment").
			ValidateAndAddOptionalArray(payload, "transfers").
			ValidateAndAddOptionalString(payload, "method").
			ValidateAndAddOptionalString(payload, "customer_id").
			ValidateAndAddToken(payload, "token")

		// Add first_payment_min_amount only if partial_payment is true
		if payload["partial_payment"] == true {
			validator.ValidateAndAddOptionalFloat(payload, "first_payment_min_amount")
		}

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		order, err := client.Order.Create(payload, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("creating order failed: %s", err.Error()),
			), nil
		}

		return mcpgo.NewToolResultJSON(order)
	}

	return mcpgo.NewTool(
		"create_order",
		"Create a new order in Razorpay. Supports both regular orders and "+
			"mandate orders. "+
			"\n\nFor REGULAR ORDERS: Provide amount, currency, and optional "+
			"receipt/notes. "+
			"\n\nFor MANDATE ORDERS (recurring payments): You MUST provide ALL "+
			"of these fields: "+
			"amount, currency, method='upi', customer_id (starts with 'cust_'), "+
			"and token object. "+
			"\n\nThe token object is required for mandate orders and must contain: "+
			"max_amount (positive number), frequency "+
			"(as_presented/monthly/one_time/yearly/weekly/daily), "+
			"type='single_block_multiple_debit', and optionally expire_at "+
			"(defaults to today+60days). "+
			"\n\nIMPORTANT: When token.type is 'single_block_multiple_debit', "+
			"the method MUST be 'upi'. "+
			"\n\nExample mandate order payload: "+
			`{"amount": 100, "currency": "INR", "method": "upi", `+
			`"customer_id": "cust_abc123", `+
			`"token": {"max_amount": 100, "frequency": "as_presented", `+
			`"type": "single_block_multiple_debit"}, `+
			`"receipt": "Receipt No. 1", "notes": {"key": "value"}}`,
		parameters,
		handler,
	)
}

// FetchOrder returns a tool to fetch order details by ID
func FetchOrder(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"order_id",
			mcpgo.Description("Unique identifier of the order to be retrieved"),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		// Get client from context or use default
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		payload := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(payload, "order_id")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		order, err := client.Order.Fetch(payload["order_id"].(string), nil, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching order failed: %s", err.Error()),
			), nil
		}

		return mcpgo.NewToolResultJSON(order)
	}

	return mcpgo.NewTool(
		"fetch_order",
		"Fetch an order's details using its ID",
		parameters,
		handler,
	)
}

// FetchAllOrders returns a tool to fetch all orders with optional filtering
func FetchAllOrders(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithNumber(
			"count",
			mcpgo.Description("Number of orders to be fetched "+
				"(default: 10, max: 100)"),
			mcpgo.Min(1),
			mcpgo.Max(100),
		),
		mcpgo.WithNumber(
			"skip",
			mcpgo.Description("Number of orders to be skipped (default: 0)"),
			mcpgo.Min(0),
		),
		mcpgo.WithNumber(
			"from",
			mcpgo.Description("Timestamp (in Unix format) from when "+
				"the orders should be fetched"),
			mcpgo.Min(0),
		),
		mcpgo.WithNumber(
			"to",
			mcpgo.Description("Timestamp (in Unix format) up till "+
				"when orders are to be fetched"),
			mcpgo.Min(0),
		),
		mcpgo.WithNumber(
			"authorized",
			mcpgo.Description("Filter orders based on payment authorization status. "+
				"Values: 0 (orders with unauthorized payments), "+
				"1 (orders with authorized payments)"),
			mcpgo.Min(0),
			mcpgo.Max(1),
		),
		mcpgo.WithString(
			"receipt",
			mcpgo.Description("Filter orders that contain the "+
				"provided value for receipt"),
		),
		mcpgo.WithArray(
			"expand",
			mcpgo.Description("Used to retrieve additional information. "+
				"Supported values: payments, payments.card, transfers, virtual_account"),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		// Get client from context or use default
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		queryParams := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddPagination(queryParams).
			ValidateAndAddOptionalInt(queryParams, "from").
			ValidateAndAddOptionalInt(queryParams, "to").
			ValidateAndAddOptionalInt(queryParams, "authorized").
			ValidateAndAddOptionalString(queryParams, "receipt").
			ValidateAndAddExpand(queryParams)

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		orders, err := client.Order.All(queryParams, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching orders failed: %s", err.Error()),
			), nil
		}

		return mcpgo.NewToolResultJSON(orders)
	}

	return mcpgo.NewTool(
		"fetch_all_orders",
		"Fetch all orders with optional filtering and pagination",
		parameters,
		handler,
	)
}

// FetchOrderPayments returns a tool to fetch all payments for a specific order
func FetchOrderPayments(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"order_id",
			mcpgo.Description(
				"Unique identifier of the order for which payments should"+
					" be retrieved. Order id should start with `order_`"),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		// Get client from context or use default
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		orderPaymentsReq := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(orderPaymentsReq, "order_id")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		// Fetch payments for the order using Razorpay SDK
		// Note: Using the Order.Payments method from SDK
		orderID := orderPaymentsReq["order_id"].(string)
		payments, err := client.Order.Payments(orderID, nil, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf(
					"fetching payments for order failed: %s",
					err.Error(),
				),
			), nil
		}

		// Return the result as JSON
		return mcpgo.NewToolResultJSON(payments)
	}

	return mcpgo.NewTool(
		"fetch_order_payments",
		"Fetch all payments made for a specific order in Razorpay",
		parameters,
		handler,
	)
}

// UpdateOrder returns a tool to update an order
// only the order's notes can be updated
func UpdateOrder(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"order_id",
			mcpgo.Description("Unique identifier of the order which "+
				"needs to be updated. ID should have an order_ prefix."),
			mcpgo.Required(),
		),
		mcpgo.WithObject(
			"notes",
			mcpgo.Description("Key-value pairs used to store additional "+
				"information about the order. A maximum of 15 key-value pairs "+
				"can be included, with each value not exceeding 256 characters."),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		orderUpdateReq := make(map[string]interface{})
		data := make(map[string]interface{})

		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(orderUpdateReq, "order_id").
			ValidateAndAddRequiredMap(orderUpdateReq, "notes")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		data["notes"] = orderUpdateReq["notes"]
		orderID := orderUpdateReq["order_id"].(string)

		order, err := client.Order.Update(orderID, data, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("updating order failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(order)
	}

	return mcpgo.NewTool(
		"update_order",
		"Use this tool to update the notes for a specific order. "+
			"Only the notes field can be modified.",
		parameters,
		handler,
	)
}

```

--------------------------------------------------------------------------------
/pkg/razorpay/tools_params.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	"encoding/json"
	"errors"
	"strings"
	"time"

	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
)

// Validator provides a fluent interface for validating parameters
// and collecting errors
type Validator struct {
	request *mcpgo.CallToolRequest
	errors  []error
}

// NewValidator creates a new validator for the given request
func NewValidator(r *mcpgo.CallToolRequest) *Validator {
	return &Validator{
		request: r,
		errors:  []error{},
	}
}

// addError adds a non-nil error to the collection
func (v *Validator) addError(err error) *Validator {
	if err != nil {
		v.errors = append(v.errors, err)
	}
	return v
}

// HasErrors returns true if there are any validation errors
func (v *Validator) HasErrors() bool {
	return len(v.errors) > 0
}

// HandleErrorsIfAny formats all errors and returns an appropriate tool result
func (v *Validator) HandleErrorsIfAny() (*mcpgo.ToolResult, error) {
	if v.HasErrors() {
		messages := make([]string, 0, len(v.errors))
		for _, err := range v.errors {
			messages = append(messages, err.Error())
		}
		errorMsg := "Validation errors:\n- " + strings.Join(messages, "\n- ")
		return mcpgo.NewToolResultError(errorMsg), nil
	}
	return nil, nil
}

// extractValueGeneric is a standalone generic function to extract a parameter
// of type T
func extractValueGeneric[T any](
	request *mcpgo.CallToolRequest,
	name string,
	required bool,
) (*T, error) {
	// Type assert Arguments from any to map[string]interface{}
	args, ok := request.Arguments.(map[string]interface{})
	if !ok {
		return nil, errors.New("invalid arguments type")
	}

	val, ok := args[name]
	if !ok || val == nil {
		if required {
			return nil, errors.New("missing required parameter: " + name)
		}
		return nil, nil // Not an error for optional params
	}

	var result T
	data, err := json.Marshal(val)
	if err != nil {
		return nil, errors.New("invalid parameter type: " + name)
	}

	err = json.Unmarshal(data, &result)
	if err != nil {
		return nil, errors.New("invalid parameter type: " + name)
	}

	return &result, nil
}

// Generic validation functions

// validateAndAddRequired validates and adds a required parameter of any type
func validateAndAddRequired[T any](
	v *Validator,
	params map[string]interface{},
	name string,
) *Validator {
	value, err := extractValueGeneric[T](v.request, name, true)
	if err != nil {
		return v.addError(err)
	}

	if value == nil {
		return v
	}

	params[name] = *value
	return v
}

// validateAndAddOptional validates and adds an optional parameter of any type
// if not empty
func validateAndAddOptional[T any](
	v *Validator,
	params map[string]interface{},
	name string,
) *Validator {
	value, err := extractValueGeneric[T](v.request, name, false)
	if err != nil {
		return v.addError(err)
	}

	if value == nil {
		return v
	}

	params[name] = *value

	return v
}

// validateAndAddToPath is a generic helper to extract a value and write it into
// `target[targetKey]` if non-empty
func validateAndAddToPath[T any](
	v *Validator,
	target map[string]interface{},
	paramName string,
	targetKey string,
) *Validator {
	value, err := extractValueGeneric[T](v.request, paramName, false)
	if err != nil {
		return v.addError(err)
	}

	if value == nil {
		return v
	}

	target[targetKey] = *value

	return v
}

// ValidateAndAddOptionalStringToPath validates an optional string
// and writes it into target[targetKey]
func (v *Validator) ValidateAndAddOptionalStringToPath(
	target map[string]interface{},
	paramName, targetKey string,
) *Validator {
	return validateAndAddToPath[string](v, target, paramName, targetKey) // nolint:lll
}

// ValidateAndAddOptionalBoolToPath validates an optional bool
// and writes it into target[targetKey]
// only if it was explicitly provided in the request
func (v *Validator) ValidateAndAddOptionalBoolToPath(
	target map[string]interface{},
	paramName, targetKey string,
) *Validator {
	// Now validate and add the parameter
	value, err := extractValueGeneric[bool](v.request, paramName, false)
	if err != nil {
		return v.addError(err)
	}

	if value == nil {
		return v
	}

	target[targetKey] = *value
	return v
}

// ValidateAndAddOptionalIntToPath validates an optional integer
// and writes it into target[targetKey]
func (v *Validator) ValidateAndAddOptionalIntToPath(
	target map[string]interface{},
	paramName, targetKey string,
) *Validator {
	return validateAndAddToPath[int64](v, target, paramName, targetKey)
}

// Type-specific validator methods

// ValidateAndAddRequiredString validates and adds a required string parameter
func (v *Validator) ValidateAndAddRequiredString(
	params map[string]interface{},
	name string,
) *Validator {
	return validateAndAddRequired[string](v, params, name)
}

// ValidateAndAddOptionalString validates and adds an optional string parameter
func (v *Validator) ValidateAndAddOptionalString(
	params map[string]interface{},
	name string,
) *Validator {
	return validateAndAddOptional[string](v, params, name)
}

// ValidateAndAddRequiredMap validates and adds a required map parameter
func (v *Validator) ValidateAndAddRequiredMap(
	params map[string]interface{},
	name string,
) *Validator {
	return validateAndAddRequired[map[string]interface{}](v, params, name)
}

// ValidateAndAddOptionalMap validates and adds an optional map parameter
func (v *Validator) ValidateAndAddOptionalMap(
	params map[string]interface{},
	name string,
) *Validator {
	return validateAndAddOptional[map[string]interface{}](v, params, name)
}

// ValidateAndAddRequiredArray validates and adds a required array parameter
func (v *Validator) ValidateAndAddRequiredArray(
	params map[string]interface{},
	name string,
) *Validator {
	return validateAndAddRequired[[]interface{}](v, params, name)
}

// ValidateAndAddOptionalArray validates and adds an optional array parameter
func (v *Validator) ValidateAndAddOptionalArray(
	params map[string]interface{},
	name string,
) *Validator {
	return validateAndAddOptional[[]interface{}](v, params, name)
}

// ValidateAndAddPagination validates and adds pagination parameters
// (count and skip)
func (v *Validator) ValidateAndAddPagination(
	params map[string]interface{},
) *Validator {
	return v.ValidateAndAddOptionalInt(params, "count").
		ValidateAndAddOptionalInt(params, "skip")
}

// ValidateAndAddExpand validates and adds expand parameters
func (v *Validator) ValidateAndAddExpand(
	params map[string]interface{},
) *Validator {
	expand, err := extractValueGeneric[[]string](v.request, "expand", false)
	if err != nil {
		return v.addError(err)
	}

	if expand == nil {
		return v
	}

	if len(*expand) > 0 {
		for _, val := range *expand {
			params["expand[]"] = val
		}
	}
	return v
}

// ValidateAndAddRequiredInt validates and adds a required integer parameter
func (v *Validator) ValidateAndAddRequiredInt(
	params map[string]interface{},
	name string,
) *Validator {
	return validateAndAddRequired[int64](v, params, name)
}

// ValidateAndAddOptionalInt validates and adds an optional integer parameter
func (v *Validator) ValidateAndAddOptionalInt(
	params map[string]interface{},
	name string,
) *Validator {
	return validateAndAddOptional[int64](v, params, name)
}

// ValidateAndAddRequiredFloat validates and adds a required float parameter
func (v *Validator) ValidateAndAddRequiredFloat(
	params map[string]interface{},
	name string,
) *Validator {
	return validateAndAddRequired[float64](v, params, name)
}

// ValidateAndAddOptionalFloat validates and adds an optional float parameter
func (v *Validator) ValidateAndAddOptionalFloat(
	params map[string]interface{},
	name string,
) *Validator {
	return validateAndAddOptional[float64](v, params, name)
}

// ValidateAndAddRequiredBool validates and adds a required boolean parameter
func (v *Validator) ValidateAndAddRequiredBool(
	params map[string]interface{},
	name string,
) *Validator {
	return validateAndAddRequired[bool](v, params, name)
}

// ValidateAndAddOptionalBool validates and adds an optional boolean parameter
// Note: This adds the boolean value only
// if it was explicitly provided in the request
func (v *Validator) ValidateAndAddOptionalBool(
	params map[string]interface{},
	name string,
) *Validator {
	// Now validate and add the parameter
	value, err := extractValueGeneric[bool](v.request, name, false)
	if err != nil {
		return v.addError(err)
	}

	if value == nil {
		return v
	}

	params[name] = *value
	return v
}

// validateTokenMaxAmount validates the max_amount field in token.
// max_amount is required and must be a positive number representing
// the maximum amount that can be debited from the customer's account.
func (v *Validator) validateTokenMaxAmount(
	token map[string]interface{}) *Validator {
	if maxAmount, exists := token["max_amount"]; exists {
		switch amt := maxAmount.(type) {
		case float64:
			if amt <= 0 {
				return v.addError(errors.New("token.max_amount must be greater than 0"))
			}
		case int:
			if amt <= 0 {
				return v.addError(errors.New("token.max_amount must be greater than 0"))
			}
			token["max_amount"] = float64(amt) // Convert int to float64
		default:
			return v.addError(errors.New("token.max_amount must be a number"))
		}
	} else {
		return v.addError(errors.New("token.max_amount is required"))
	}
	return v
}

// validateTokenExpireAt validates the expire_at field in token.
// expire_at is optional and defaults to today + 60 days if not provided.
// If provided, it must be a positive Unix timestamp indicating when the
// mandate/token should expire.
func (v *Validator) validateTokenExpireAt(
	token map[string]interface{}) *Validator {
	if expireAt, exists := token["expire_at"]; exists {
		switch exp := expireAt.(type) {
		case float64:
			if exp <= 0 {
				return v.addError(errors.New("token.expire_at must be greater than 0"))
			}
		case int:
			if exp <= 0 {
				return v.addError(errors.New("token.expire_at must be greater than 0"))
			}
			token["expire_at"] = float64(exp) // Convert int to float64
		default:
			return v.addError(errors.New("token.expire_at must be a number"))
		}
	} else {
		// Set default value to today + 60 days
		defaultExpireAt := time.Now().AddDate(0, 0, 60).Unix()
		token["expire_at"] = float64(defaultExpireAt)
	}
	return v
}

// validateTokenFrequency validates the frequency field in token.
// frequency is required and must be one of the allowed values:
// "as_presented", "monthly", "one_time", "yearly", "weekly", "daily".
func (v *Validator) validateTokenFrequency(
	token map[string]interface{}) *Validator {
	if frequency, exists := token["frequency"]; exists {
		if freqStr, ok := frequency.(string); ok {
			validFrequencies := []string{
				"as_presented", "monthly", "one_time", "yearly", "weekly", "daily"}
			for _, validFreq := range validFrequencies {
				if freqStr == validFreq {
					return v
				}
			}
			return v.addError(errors.New(
				"token.frequency must be one of: as_presented, " +
					"monthly, one_time, yearly, weekly, daily"))
		}
		return v.addError(errors.New("token.frequency must be a string"))
	}
	return v.addError(errors.New("token.frequency is required"))
}

// validateTokenType validates the type field in token.
// type is required and must be "single_block_multiple_debit" for SBMD mandates.
func (v *Validator) validateTokenType(token map[string]interface{}) *Validator {
	if tokenType, exists := token["type"]; exists {
		if typeStr, ok := tokenType.(string); ok {
			validTypes := []string{"single_block_multiple_debit"}
			for _, validType := range validTypes {
				if typeStr == validType {
					return v
				}
			}
			return v.addError(errors.New(
				"token.type must be one of: single_block_multiple_debit"))
		}
		return v.addError(errors.New("token.type must be a string"))
	}
	return v.addError(errors.New("token.type is required"))
}

// ValidateAndAddToken validates and adds a token object with proper structure.
// The token object is used for mandate orders and must contain:
//   - max_amount: positive number (maximum debit amount)
//   - expire_at: optional Unix timestamp (mandate expiry,
//     defaults to today + 60 days)
//   - frequency: string (debit frequency: as_presented, monthly, one_time,
//     yearly, weekly, daily)
//   - type: string (mandate type: single_block_multiple_debit)
func (v *Validator) ValidateAndAddToken(
	params map[string]interface{}, name string) *Validator {
	value, err := extractValueGeneric[map[string]interface{}](
		v.request, name, false)
	if err != nil {
		return v.addError(err)
	}

	if value == nil {
		return v
	}

	token := *value

	// Validate all token fields
	v.validateTokenMaxAmount(token).
		validateTokenExpireAt(token).
		validateTokenFrequency(token).
		validateTokenType(token)

	if v.HasErrors() {
		return v
	}

	params[name] = token
	return v
}

```

--------------------------------------------------------------------------------
/pkg/razorpay/qr_codes.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	"context"
	"fmt"

	rzpsdk "github.com/razorpay/razorpay-go"

	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
)

// CreateQRCode returns a tool that creates QR codes in Razorpay
func CreateQRCode(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"type",
			mcpgo.Description(
				"The type of the QR Code. Currently only supports 'upi_qr'",
			),
			mcpgo.Required(),
			mcpgo.Pattern("^upi_qr$"),
		),
		mcpgo.WithString(
			"name",
			mcpgo.Description(
				"Label to identify the QR Code (e.g., 'Store Front Display')",
			),
		),
		mcpgo.WithString(
			"usage",
			mcpgo.Description(
				"Whether QR should accept single or multiple payments. "+
					"Possible values: 'single_use', 'multiple_use'",
			),
			mcpgo.Required(),
			mcpgo.Enum("single_use", "multiple_use"),
		),
		mcpgo.WithBoolean(
			"fixed_amount",
			mcpgo.Description(
				"Whether QR should accept only specific amount (true) or any "+
					"amount (false)",
			),
			mcpgo.DefaultValue(false),
		),
		mcpgo.WithNumber(
			"payment_amount",
			mcpgo.Description(
				"The specific amount allowed for transaction in smallest "+
					"currency unit",
			),
			mcpgo.Min(1),
		),
		mcpgo.WithString(
			"description",
			mcpgo.Description("A brief description about the QR Code"),
		),
		mcpgo.WithString(
			"customer_id",
			mcpgo.Description(
				"The unique identifier of the customer to link with the QR Code",
			),
		),
		mcpgo.WithNumber(
			"close_by",
			mcpgo.Description(
				"Unix timestamp at which QR Code should be automatically "+
					"closed (min 2 mins after current time)",
			),
		),
		mcpgo.WithObject(
			"notes",
			mcpgo.Description(
				"Key-value pairs for additional information "+
					"(max 15 pairs, 256 chars each)",
			),
			mcpgo.MaxProperties(15),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		qrData := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(qrData, "type").
			ValidateAndAddRequiredString(qrData, "usage").
			ValidateAndAddOptionalString(qrData, "name").
			ValidateAndAddOptionalBool(qrData, "fixed_amount").
			ValidateAndAddOptionalFloat(qrData, "payment_amount").
			ValidateAndAddOptionalString(qrData, "description").
			ValidateAndAddOptionalString(qrData, "customer_id").
			ValidateAndAddOptionalFloat(qrData, "close_by").
			ValidateAndAddOptionalMap(qrData, "notes")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		// Check if fixed_amount is true, then payment_amount is required
		if fixedAmount, exists := qrData["fixed_amount"]; exists &&
			fixedAmount.(bool) {
			if _, exists := qrData["payment_amount"]; !exists {
				return mcpgo.NewToolResultError(
					"payment_amount is required when fixed_amount is true"), nil
			}
		}

		// Create QR code using Razorpay SDK
		qrCode, err := client.QrCode.Create(qrData, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("creating QR code failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(qrCode)
	}

	return mcpgo.NewTool(
		"create_qr_code",
		"Create a new QR code in Razorpay that can be used to accept UPI payments",
		parameters,
		handler,
	)
}

// FetchQRCode returns a tool that fetches a specific QR code by ID
func FetchQRCode(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"qr_code_id",
			mcpgo.Description(
				"Unique identifier of the QR Code to be retrieved"+
					"The QR code id should start with 'qr_'",
			),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		params := make(map[string]interface{})
		validator := NewValidator(&r).
			ValidateAndAddRequiredString(params, "qr_code_id")
		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}
		qrCodeID := params["qr_code_id"].(string)

		// Fetch QR code by ID using Razorpay SDK
		qrCode, err := client.QrCode.Fetch(qrCodeID, nil, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching QR code failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(qrCode)
	}

	return mcpgo.NewTool(
		"fetch_qr_code",
		"Fetch a QR code's details using it's ID",
		parameters,
		handler,
	)
}

// FetchAllQRCodes returns a tool that fetches all QR codes
// with pagination support
func FetchAllQRCodes(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithNumber(
			"from",
			mcpgo.Description(
				"Unix timestamp, in seconds, from when QR Codes are to be retrieved",
			),
			mcpgo.Min(0),
		),
		mcpgo.WithNumber(
			"to",
			mcpgo.Description(
				"Unix timestamp, in seconds, till when QR Codes are to be retrieved",
			),
			mcpgo.Min(0),
		),
		mcpgo.WithNumber(
			"count",
			mcpgo.Description(
				"Number of QR Codes to be retrieved (default: 10, max: 100)",
			),
			mcpgo.Min(1),
			mcpgo.Max(100),
		),
		mcpgo.WithNumber(
			"skip",
			mcpgo.Description(
				"Number of QR Codes to be skipped (default: 0)",
			),
			mcpgo.Min(0),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		fetchQROptions := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddOptionalInt(fetchQROptions, "from").
			ValidateAndAddOptionalInt(fetchQROptions, "to").
			ValidateAndAddPagination(fetchQROptions)

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		// Fetch QR codes using Razorpay SDK
		qrCodes, err := client.QrCode.All(fetchQROptions, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching QR codes failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(qrCodes)
	}

	return mcpgo.NewTool(
		"fetch_all_qr_codes",
		"Fetch all QR codes with optional filtering and pagination",
		parameters,
		handler,
	)
}

// FetchQRCodesByCustomerID returns a tool that fetches QR codes
// for a specific customer ID
func FetchQRCodesByCustomerID(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"customer_id",
			mcpgo.Description(
				"The unique identifier of the customer",
			),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		fetchQROptions := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(fetchQROptions, "customer_id")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		// Fetch QR codes by customer ID using Razorpay SDK
		qrCodes, err := client.QrCode.All(fetchQROptions, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching QR codes failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(qrCodes)
	}

	return mcpgo.NewTool(
		"fetch_qr_codes_by_customer_id",
		"Fetch all QR codes for a specific customer",
		parameters,
		handler,
	)
}

// FetchQRCodesByPaymentID returns a tool that fetches QR codes
// for a specific payment ID
func FetchQRCodesByPaymentID(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"payment_id",
			mcpgo.Description(
				"The unique identifier of the payment"+
					"The payment id always should start with 'pay_'",
			),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		fetchQROptions := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(fetchQROptions, "payment_id")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		// Fetch QR codes by payment ID using Razorpay SDK
		qrCodes, err := client.QrCode.All(fetchQROptions, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching QR codes failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(qrCodes)
	}

	return mcpgo.NewTool(
		"fetch_qr_codes_by_payment_id",
		"Fetch all QR codes for a specific payment",
		parameters,
		handler,
	)
}

// FetchPaymentsForQRCode returns a tool that fetches payments made on a QR code
func FetchPaymentsForQRCode(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"qr_code_id",
			mcpgo.Description(
				"The unique identifier of the QR Code to fetch payments for"+
					"The QR code id should start with 'qr_'",
			),
			mcpgo.Required(),
		),
		mcpgo.WithNumber(
			"from",
			mcpgo.Description(
				"Unix timestamp, in seconds, from when payments are to be retrieved",
			),
			mcpgo.Min(0),
		),
		mcpgo.WithNumber(
			"to",
			mcpgo.Description(
				"Unix timestamp, in seconds, till when payments are to be fetched",
			),
			mcpgo.Min(0),
		),
		mcpgo.WithNumber(
			"count",
			mcpgo.Description(
				"Number of payments to be fetched (default: 10, max: 100)",
			),
			mcpgo.Min(1),
			mcpgo.Max(100),
		),
		mcpgo.WithNumber(
			"skip",
			mcpgo.Description(
				"Number of records to be skipped while fetching the payments",
			),
			mcpgo.Min(0),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		params := make(map[string]interface{})
		fetchQROptions := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(params, "qr_code_id").
			ValidateAndAddOptionalInt(fetchQROptions, "from").
			ValidateAndAddOptionalInt(fetchQROptions, "to").
			ValidateAndAddOptionalInt(fetchQROptions, "count").
			ValidateAndAddOptionalInt(fetchQROptions, "skip")

		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}

		qrCodeID := params["qr_code_id"].(string)

		// Fetch payments for QR code using Razorpay SDK
		payments, err := client.QrCode.FetchPayments(qrCodeID, fetchQROptions, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching payments for QR code failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(payments)
	}

	return mcpgo.NewTool(
		"fetch_payments_for_qr_code",
		"Fetch all payments made on a QR code",
		parameters,
		handler,
	)
}

// CloseQRCode returns a tool that closes a specific QR code
func CloseQRCode(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"qr_code_id",
			mcpgo.Description(
				"Unique identifier of the QR Code to be closed"+
					"The QR code id should start with 'qr_'",
			),
			mcpgo.Required(),
		),
	}

	handler := func(
		ctx context.Context,
		r mcpgo.CallToolRequest,
	) (*mcpgo.ToolResult, error) {
		client, err := getClientFromContextOrDefault(ctx, client)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		params := make(map[string]interface{})
		validator := NewValidator(&r).
			ValidateAndAddRequiredString(params, "qr_code_id")
		if result, err := validator.HandleErrorsIfAny(); result != nil {
			return result, err
		}
		qrCodeID := params["qr_code_id"].(string)

		// Close QR code by ID using Razorpay SDK
		qrCode, err := client.QrCode.Close(qrCodeID, nil, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("closing QR code failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(qrCode)
	}

	return mcpgo.NewTool(
		"close_qr_code",
		"Close a QR Code that's no longer needed",
		parameters,
		handler,
	)
}

```
Page 1/4FirstPrevNextLast