#
tokens: 9168/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .github
│   └── workflows
│       └── go.yml
├── .gitignore
├── go.mod
├── go.sum
├── internal
│   ├── tools.go
│   └── var.go
├── main.go
└── README.md
```

# Files

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

```
vendor/
.DS_Store

.idea/
```

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

```markdown
# Zerodha MCP Server

<p align="center">
  <strong>Protocol to communicate with your Zerodha data written in Golang</strong>
</p>

<p align="center">
  <img src="https://raw.githubusercontent.com/sukeesh/sukeesh.github.io/refs/heads/master/assets/img/Zerodha_MCP.png" alt="Zerodha MCP Logo" width="200" />
</p>

[![Go](https://github.com/sukeesh/zerodha-mcp-go/workflows/Go/badge.svg)](https://github.com/sukeesh/zerodha-mcp-go/actions)

## Overview
Zerodha MCP Server provides an implementation of the Claude MCP (Model Completion Protocol) interface for Zerodha trading data. This allows Claude AI to access your Zerodha trading account information directly.

## Prerequisites
- [Go](https://go.dev/doc/install) (version 1.21 or later)
- A [Zerodha Kite](https://kite.zerodha.com) trading account
- [Claude Desktop App](https://claude.ai/download)
- API credentials from the [Kite Connect developer portal](https://developers.kite.trade/apps)

## Installation

### Option 1: Using Go Install
```bash
go install github.com/sukeesh/zerodha-mcp@latest
```

### Option 2: Build from Source
```bash
git clone https://github.com/sukeesh/zerodha-mcp.git
cd zerodha-mcp
go install
```

The binary will be installed to your GOBIN directory, which should be in your PATH.

## Usage with an MCP Client

### GPT 4o mini

https://github.com/user-attachments/assets/849c4aca-0ca2-4aed-a9be-3df135f8a5c5

### Claude Sonnet 3.7

<img width="500" alt="Claude as MCP Client" src="https://github.com/user-attachments/assets/932ec561-d7b4-4e58-8f1c-1f33b9c99896" />


## Configuration

1. Get your `ZERODHA_API_KEY` and `ZERODHA_API_SECRET` from the [Kite Connect developer portal](https://developers.kite.trade/apps)

2. Set up a redirect URL in the Kite developer portal:
   ```
   http://127.0.0.1:5888/auth
   ```

3. Configure Claude Desktop:
   - Open Claude Desktop → Settings → Developer → Edit Config
   - Add the following to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "zerodha": {
      "command": "<path-to-zerodha-mcp-binary>",
      "env": {
       "ZERODHA_API_KEY": "<api_key>",
       "ZERODHA_API_SECRET": "<api_secret>"
      }
    }
  }
}
```

4. Restart Claude Desktop. When prompted, authenticate with your Zerodha Kite credentials.

## Debugging

The logs for MCP Server are available at `~/Library/Logs/Claude`

### Known Bugs

When the Claude desktop is shutdown, the underlying MCP Server is not getting killed.
```bash
kill -9 $(lsof -t -i:5888)
```

## Available Tools

| Category | Tool | Status | Description |
|----------|------|--------|-------------|
| **Account Information** | `get_user_profile` | ✅ | Get basic user profile information |
| | `get_user_margins` | ✅ | Get all user margins |
| | `get_user_segment_margins` | ✅ | Get segment-wise user margins |
| **Portfolio & Positions** | `get_kite_holdings` | ✅ | Get current holdings in Zerodha Kite account |
| | `get_positions` | ✅ | Get current day and net positions |
| | `get_order_margins` | ✅ | Get margin requirements for specific orders |
| **Market Data** | `get_ltp` | ✅ | Get Last Traded Price for specific instruments |
| | `get_quote` | ✅ | Get detailed quotes for specific instruments |
| | `get_ohlc` | ✅ | Get Open, High, Low, Close quotes |
| **Instruments** | `get_instruments` | ✅ | Get list of all available instruments on Zerodha |
| | `get_instruments_by_exchange` | ✅ | Get instruments filtered by exchange |
| | `get_auction_instruments` | ✅ | Get instruments available for auction sessions |
| **Mutual Funds** | `get_mf_instruments` | ✅ | Get list of all available mutual fund instruments |
| | `get_mf_holdings` | ✅ | Get list of mutual fund holdings |
| | `get_mf_holdings_info` | ✅ | Get detailed information about mutual fund holdings |
| | `get_mf_orders` | ✅ | Get list of all mutual fund orders |
| | `get_mf_order_info` | ✅ | Get detailed information about specific mutual fund orders |
| | `get_mf_sip_info` | ✅ | Get information about mutual fund SIPs |
| | `get_mf_allotted_isins` | ✅ | Get allotted mutual fund ISINs |


## Usage

After setup, you can interact with your Zerodha account data directly through Claude. For example:

- "Show me my current portfolio holdings"
- "What's my current margin availability?"
- "Give me the latest price for RELIANCE"
- "Show me my open positions with P&L"


## Limitations

- Only read operations are supported; trading is not yet available
- Authentication token expires daily and requires re-login


```

--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------

```yaml
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

jobs:

  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: '1.24.2'

    - name: Build
      run: go build -v ./...

```

--------------------------------------------------------------------------------
/internal/var.go:
--------------------------------------------------------------------------------

```go
package internal

const (
	HTMLHeaderTemplate = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Zerodha MCP Authentication</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: #f5f5f5;
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
        }
        
        .container {
            background-color: white;
            border-radius: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            padding: 30px;
            text-align: center;
            max-width: 500px;
            width: 100%;
        }
        
        .success {
            color: #28a745;
        }
        
        .error {
            color: #dc3545;
        }
        
        h1 {
            margin-bottom: 20px;
            font-weight: 600;
        }
        
        p {
            margin-bottom: 25px;
            color: #6c757d;
            line-height: 1.6;
        }
        
        .icon {
            font-size: 64px;
            margin-bottom: 20px;
        }
        
        .btn {
            display: inline-block;
            background-color: #007bff;
            color: white;
            text-decoration: none;
            padding: 10px 20px;
            border-radius: 5px;
            transition: background-color 0.3s;
        }
        
        .btn:hover {
            background-color: #0069d9;
        }
        
        .zerodha-logo {
            margin-bottom: 20px;
            max-width: 150px;
        }
    </style>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
</head>
<body>`

	HTMLFooterTemplate = `</body>
</html>`

	SuccessContentTemplate = `    <div class="container">
        <img src="https://zerodha.com/static/images/logo.svg" alt="Zerodha Logo" class="zerodha-logo">
        <div class="icon success">
            <i class="fas fa-check-circle"></i>
        </div>
        <h1 class="success">Authentication Successful!</h1>
        <p>Your Zerodha account has been successfully authenticated with MCP Server.</p>
        <p>You can now close this window and continue using the Zerodha MCP tools.</p>
    </div>`

	ErrorContentTemplate = `    <div class="container">
        <img src="https://zerodha.com/static/images/logo.svg" alt="Zerodha Logo" class="zerodha-logo">
        <div class="icon error">
            <i class="fas fa-times-circle"></i>
        </div>
        <h1 class="error">Authentication Failed</h1>
        <p>There was a problem authenticating your Zerodha account with MCP Server.</p>
        <p>Please try again or contact support if the issue persists.</p>
        <a href="javascript:window.close();" class="btn">Close Window</a>
    </div>`
)

// RenderHTMLResponse combines HTML parts and returns the complete HTML string
func RenderHTMLResponse(content string) string {
	return HTMLHeaderTemplate + content + HTMLFooterTemplate
}

```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/toqueteos/webbrowser"

	"github.com/gin-gonic/gin"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"github.com/sukeesh/zerodha-mcp/internal"
	kiteconnect "github.com/zerodha/gokiteconnect/v4"
)

var (
	requestToken    = ""
	isAuthenticated = false
	z               *internal.ZerodhaMcpServer

	// Command line flags
	apiKey    string
	apiSecret string
)

func setEnvs() {
	eApiKey := os.Getenv("ZERODHA_API_KEY")
	eApiSecret := os.Getenv("ZERODHA_API_SECRET")

	// Validate required flags
	if eApiKey == "" || eApiSecret == "" {
		fmt.Println("Error: apikey and apisecret flags are required")
		fmt.Println("Usage example: ./zerodha-mcp -apikey=YOUR_API_KEY -apisecret=YOUR_API_SECRET")
		os.Exit(1)
	}

	apiKey = eApiKey
	apiSecret = eApiSecret
}

func renderHTMLResponse(c *gin.Context, content string, status int) {
	html := internal.RenderHTMLResponse(content)
	c.Data(status, "text/html; charset=utf-8", []byte(html))
}

func startRouter() (*http.Server, func()) {
	gin.DefaultWriter = os.Stderr
	gin.DefaultErrorWriter = os.Stderr

	// use a fresh Engine so we don't get the default stdout logger
	r := gin.New()
	r.Use(
		gin.LoggerWithWriter(os.Stderr),
		gin.RecoveryWithWriter(os.Stderr),
	)

	r.GET("/ping", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "pong",
		})
	})

	r.GET("/auth", func(c *gin.Context) {
		fmt.Fprintln(os.Stderr, "Request received on auth request")
		paramRequestToken := c.Query("request_token")
		if paramRequestToken == "" {
			// Render error template
			renderHTMLResponse(c, internal.ErrorContentTemplate, http.StatusBadRequest)
			return
		}

		requestToken = paramRequestToken
		if requestToken != "" {
			isAuthenticated = true
			// Render success template
			renderHTMLResponse(c, internal.SuccessContentTemplate, http.StatusOK)
		} else {
			// Render error template
			renderHTMLResponse(c, internal.ErrorContentTemplate, http.StatusBadRequest)
		}
	})

	srv := &http.Server{
		Addr:    ":5888",
		Handler: r,
	}

	// Create a shutdown function
	shutdownFn := func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		if err := srv.Shutdown(ctx); err != nil {
			log.Printf("HTTP server shutdown error: %v", err)
		}
		log.Println("HTTP server stopped")
	}

	go func() {
		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Fatalf("listen: %s\n", err)
		}
	}()

	return srv, shutdownFn
}

func kiteAuthenticate() *kiteconnect.Client {
	isAuthenticated = false

	kc := kiteconnect.New(apiKey)
	webbrowser.Open(kc.GetLoginURL())

	curTime := time.Now()

	for {
		time.Sleep(5 * time.Second)
		if isAuthenticated {
			break
		} else {
			fmt.Fprintln(os.Stderr, fmt.Sprintf("Waiting for authentication from user. Please authenticate from %s", kc.GetLoginURL()))
		}

		if time.Since(curTime) > time.Minute*2 {
			fmt.Fprintln(os.Stderr, "2 mins and no auth yet for Zerodha, Exiting...")
			os.Exit(1)
		}
	}

	data, err := kc.GenerateSession(requestToken, apiSecret)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		return nil
	}

	kc.SetAccessToken(data.AccessToken)
	return kc
}

func mcpMain(ctx context.Context, s *server.MCPServer, kc *kiteconnect.Client) {
	z = internal.NewZerodhaMcpServer(kc)

	kiteHoldingsTool := mcp.NewTool("get_kite_holdings",
		mcp.WithDescription("Get current holdings in Zerodha Kite account. This includes stocks, ETFs, and other securities traded on NSE/BSE exchanges. Does not include mutual fund holdings."),
	)
	s.AddTool(kiteHoldingsTool, z.KiteHoldingsTool())

	auctionInstrumentsTool := mcp.NewTool("get_auction_instruments",
		mcp.WithDescription("Retrieves list of available instruments for a auction session"),
	)
	s.AddTool(auctionInstrumentsTool, z.AuctionInstrumentsTool())

	positionsTool := mcp.NewTool("get_positions",
		mcp.WithDescription("Get current day and net positions in your Zerodha account. Day positions show intraday trades, while net positions show delivery holdings and carried forward F&O positions. Includes quantity, average price, PnL and more details for each position."),
	)
	s.AddTool(positionsTool, z.Positions())

	orderMarginsTool := mcp.NewTool("get_order_margins",
		mcp.WithDescription("Get order margins for a specific instrument. This tool helps you check the margin requirements for placing orders on Zerodha. It provides the necessary information to ensure you have enough margin to execute trades."),
		mcp.WithString("exchange",
			mcp.Required(),
			mcp.Description("The exchange value"),
			mcp.Enum("nse", "bse"),
		),
		mcp.WithString("tradingSymbol",
			mcp.Required(),
			mcp.Description("The trading symbol"),
		),
		mcp.WithString("transactionType",
			mcp.Required(),
			mcp.Description("The transaction type"),
		),
		mcp.WithString("variety",
			mcp.Required(),
			mcp.Description("Variety"),
		),
		mcp.WithString("product",
			mcp.Required(),
			mcp.Description("Product"),
		),
		mcp.WithString("orderType",
			mcp.Required(),
			mcp.Description("Order Type"),
		),
		mcp.WithNumber("quantity",
			mcp.Required(),
			mcp.Description("Quantity"),
		),
		mcp.WithNumber("price",
			mcp.Required(),
			mcp.Description("Price"),
		),
		mcp.WithNumber("triggerPrice",
			mcp.Required(),
			mcp.Description("Trigger Price"),
		),
	)
	s.AddTool(orderMarginsTool, z.OrderMargins())

	quoteTool := mcp.NewTool("get_quote",
		mcp.WithDescription("Get quote for a specific instrument. This tool provides real-time market data for stocks, ETFs, and other securities traded on NSE/BSE exchanges."),
		mcp.WithString("instrument",
			mcp.Required(),
			mcp.Description("format of `exchange:tradingsymbol`"),
		),
	)
	s.AddTool(quoteTool, z.Quote())

	ltpTool := mcp.NewTool("get_ltp",
		mcp.WithDescription("Get Last Traded Price (LTP) for a specific instrument. This tool provides the latest price at which the instrument was traded in the market."),
		mcp.WithString("instrument",
			mcp.Required(),
			mcp.Description("format of `exchange:tradingsymbol`"),
		),
	)
	s.AddTool(ltpTool, z.LTP())

	ohlcTool := mcp.NewTool("get_ohlc",
		mcp.WithDescription("Get Open, High, Low, Close (OHLC) quotes for a specific instrument. This tool provides the historical price data for the instrument over a specific time period."),
		mcp.WithString("instrument",
			mcp.Required(),
			mcp.Description("format of `exchange:tradingsymbol`"),
		),
	)
	s.AddTool(ohlcTool, z.OHLC())

	// TODO: Complete Historical data tool. Need a way to consume huge amount of data.

	instrumentsTool := mcp.NewTool("get_instruments",
		mcp.WithDescription("Get list of all available instruments on Zerodha. This tool provides a comprehensive list of all the instruments that can be traded on Zerodha, including stocks, ETFs, futures, options, and more."),
	)
	s.AddTool(instrumentsTool, z.Instruments())

	instrumentsByExchange := mcp.NewTool("get_instruments_by_exchange",
		mcp.WithDescription("Get list of instruments by exchange. This tool allows you to filter and retrieve specific instruments based on the exchange they are traded on."),
		mcp.WithString("exchange",
			mcp.Required(),
			mcp.Description("The exchange value"),
			mcp.Enum("nse", "bse"),
		),
	)
	s.AddTool(instrumentsByExchange, z.InstrumentsByExchange())

	mfInstruments := mcp.NewTool("get_mf_instruments",
		mcp.WithDescription("Get list of all available mutual fund instruments on Zerodha. This tool provides a comprehensive list of all the mutual fund instruments that can be traded on Zerodha."),
	)
	s.AddTool(mfInstruments, z.MFInstruments())

	mfOrders := mcp.NewTool("get_mf_orders",
		mcp.WithDescription("Get list of all Mutual Fund orders. This tool provides a comprehensive list of all the mutual fund orders that can be traded on Zerodha."),
	)
	s.AddTool(mfOrders, z.MFOrders())

	mfOrderInfo := mcp.NewTool("get_mf_order_info",
		mcp.WithDescription("Get individual mutual fund order info. This tool provides detailed information about a specific mutual fund order, including the order ID, status, and other relevant details."),
		mcp.WithString("orderId",
			mcp.Required(),
			mcp.Description("The Order ID of the mutual fund"),
		))
	s.AddTool(mfOrderInfo, z.MFOrderInfo())

	mfSipInfo := mcp.NewTool("get_mf_sip_info",
		mcp.WithDescription("Get individual mutual fund SIP info. This tool provides detailed information about a specific mutual fund SIP, including the SIP ID, status, and other relevant details."),
		mcp.WithString("sipId",
			mcp.Required(),
			mcp.Description("The SIP ID of the mutual fund"),
		))
	s.AddTool(mfSipInfo, z.MfSipInfo())

	mfHoldings := mcp.NewTool("get_mf_holdings",
		mcp.WithDescription("Get list of Mutual fund holdings for a user. This tool provides a comprehensive list of all the mutual fund holdings that can be traded on Zerodha."),
	)
	s.AddTool(mfHoldings, z.MFHoldings())

	mfHoldingsInfo := mcp.NewTool("get_mf_holdings_info",
		mcp.WithDescription("Get individual mutual fund holdings info. This tool provides detailed information about a specific mutual fund holding, including the holding ID, status, and other relevant details."),
		mcp.WithString("isin",
			mcp.Required(),
			mcp.Description("The ISIN of the mutual fund holding"),
		))
	s.AddTool(mfHoldingsInfo, z.MFHoldingInfo())

	mfAllottedIsins := mcp.NewTool("get_mf_allotted_isins",
		mcp.WithDescription("Get Allotted mutual fund ISINs. This tool provides a comprehensive list of all the mutual fund ISINs that can be traded on Zerodha."))
	s.AddTool(mfAllottedIsins, z.MFAllottedISINs())

	userProfile := mcp.NewTool("get_user_profile",
		mcp.WithDescription("Get basic user profile. This tool provides basic information about the user, including the user ID, name, and other relevant details."),
	)
	s.AddTool(userProfile, z.UserProfile())

	// TODO: Figure out the right permissions for this
	//fullUserProfile := mcp.NewTool("get_full_user_profile",
	//	mcp.WithDescription("get full user profile"))
	//s.AddTool(fullUserProfile, z.FullUserProfile())

	userMargins := mcp.NewTool("get_user_margins",
		mcp.WithDescription("Get all user margins. This tool provides a comprehensive list of all the margins that can be traded on Zerodha."))
	s.AddTool(userMargins, z.UserMargins())

	userSegmentMargins := mcp.NewTool("get_user_segment_margins",
		mcp.WithDescription("Get segment wise user margins. This tool provides a comprehensive list of all the margins that can be traded on Zerodha."),
		mcp.WithString("segment",
			mcp.Required(),
			mcp.Description("segment of the mutual fund holding"),
		),
	)
	s.AddTool(userSegmentMargins, z.UserSegmentMargins())

	// Start the server and handle interruption via context
	go func() {
		<-ctx.Done()
		// This will only happen when ctx is cancelled - implement any cleanup needed here
		log.Println("MCP server received shutdown signal")
	}()

	if err := server.ServeStdio(s); err != nil {
		log.Fatalf("Server error: %v", err)
	}
}

func main() {
	setEnvs()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

	s := server.NewMCPServer(
		"Zerodha MCP Server",
		"0.0.1",
		server.WithResourceCapabilities(true, true),
		server.WithLogging(),
		server.WithRecovery(),
	)

	// Start the router and get the shutdown function
	_, httpShutdownFn := startRouter()

	kc := kiteAuthenticate()
	fmt.Fprintln(os.Stderr, "Zerodha authentication successful, starting MCP Server...")

	// Create a context that can be cancelled
	ctx, cancel := context.WithCancel(context.Background())

	// Start the MCP server in a goroutine
	mcpDone := make(chan struct{})
	go func() {
		defer close(mcpDone)
		mcpMain(ctx, s, kc)
	}()

	// Wait for quit signal
	<-quit
	log.Println("Shutting down server...")

	// Cancel the context to signal all operations to stop
	cancel()

	// Call the HTTP server shutdown function
	httpShutdownFn()

	// Wait for the MCP server to finish or timeout
	select {
	case <-mcpDone:
		log.Println("MCP server stopped")
	case <-time.After(5 * time.Second):
		log.Println("MCP server shutdown timed out")
	}

	log.Println("Server exiting")
	os.Exit(0)
}

```

--------------------------------------------------------------------------------
/internal/tools.go:
--------------------------------------------------------------------------------

```go
package internal

import (
	"context"
	"fmt"
	"reflect"
	"strconv"
	"time"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	kiteconnect "github.com/zerodha/gokiteconnect/v4"
)

const (
	timeLayout = "2006-01-02 15:04:05"
)

type ZerodhaMcpServer struct {
	kc *kiteconnect.Client
}

func NewZerodhaMcpServer(kc *kiteconnect.Client) *ZerodhaMcpServer {
	return &ZerodhaMcpServer{
		kc: kc,
	}
}

func (z *ZerodhaMcpServer) SetKc(kc *kiteconnect.Client) {
	z.kc = kc
}

func printStruct(s interface{}) string {
	val := reflect.ValueOf(s)
	typ := reflect.TypeOf(s)

	if val.Kind() == reflect.Ptr {
		val = val.Elem()
		typ = typ.Elem()
	}

	returnVal := "<start> "

	for i := 0; i < val.NumField(); i++ {
		field := val.Field(i)
		fieldName := typ.Field(i).Name
		returnVal += fmt.Sprintf("%s: %v, ", fieldName, field.Interface())
	}
	returnVal += " <end>"

	return returnVal
}

func getHoldingText(holding kiteconnect.Holding) string {
	holdingTemplate := "Holding: Tradingsymbol: %s, Exchange: %s, InstrumentToken %d, ISIN %s, Product %s, Price %.2f, UsedQuantity %d, Quantity %d, T1Quantity %d, RealisedQuantity %d, Average Price %.2f, Last Price %.2f, Close Price %.2f, PnL %.2f, DayChange %.2f, DayChangePercentage %.2f, Buy Value: %.2f, Current Total Value: %.2f, MTFHolding: %x"
	return fmt.Sprintf(holdingTemplate, holding.Tradingsymbol, holding.Exchange, holding.InstrumentToken, holding.ISIN, holding.Product, holding.Price, holding.UsedQuantity, holding.Quantity, holding.T1Quantity, holding.RealisedQuantity, holding.AveragePrice, holding.LastPrice, holding.ClosePrice, holding.PnL, holding.DayChange, holding.DayChangePercentage, holding.AveragePrice*float64(holding.Quantity), holding.LastPrice*float64(holding.Quantity), holding.MTF)
}

func (z *ZerodhaMcpServer) KiteHoldingsTool() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		holdings, err := z.kc.GetHoldings()
		if err != nil {
			return nil, err
		}
		holdingsText := ""
		for _, holding := range holdings {
			eachHolding := getHoldingText(holding)
			holdingsText += eachHolding + "\n"
		}

		return mcp.NewToolResultText(holdingsText), nil
	}
}

func (z *ZerodhaMcpServer) AuctionInstrumentsTool() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		auctionInstruments, err := z.kc.GetAuctionInstruments()
		if err != nil {
			return nil, err
		}
		auctionInstrumentsText := ""
		for _, auctionInstrument := range auctionInstruments {
			eachAuctionInstrument := printStruct(auctionInstrument)
			auctionInstrumentsText += eachAuctionInstrument + "\n"
		}

		return mcp.NewToolResultText(auctionInstrumentsText), nil
	}
}

func (z *ZerodhaMcpServer) Positions() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		positions, err := z.kc.GetPositions()
		if err != nil {
			return nil, err
		}
		dayPositions := "DAY POSITIONS --- "
		for _, eachPosition := range positions.Day {
			eachPositionText := printStruct(eachPosition)
			dayPositions += eachPositionText + "\n"
		}
		netPositions := "NET POSITIONS --- "
		for _, position := range positions.Net {
			eachPosition := printStruct(position)
			netPositions += eachPosition + "\n"
		}

		positionsText := dayPositions + " \n \n " + netPositions
		return mcp.NewToolResultText(positionsText), nil
	}
}

func (z *ZerodhaMcpServer) OrderMargins() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		// TODO: Currently accepting only single object, figure out a way with mcp.WithObject to deal with slice of objects

		exchange := request.Params.Arguments["exchange"].(string)
		tradingSymbol := request.Params.Arguments["tradingSymbol"].(string)
		transactionType := request.Params.Arguments["transactionType"].(string)
		variety := request.Params.Arguments["variety"].(string)
		product := request.Params.Arguments["product"].(string)
		orderType := request.Params.Arguments["orderType"].(string)
		quantity := request.Params.Arguments["quantity"].(float64)
		price := request.Params.Arguments["price"].(float64)
		triggerPrice := request.Params.Arguments["triggerPrice"].(float64)

		orderMargins, err := z.kc.GetOrderMargins(kiteconnect.GetMarginParams{
			OrderParams: []kiteconnect.OrderMarginParam{
				{
					Exchange:        exchange,
					Tradingsymbol:   tradingSymbol,
					TransactionType: transactionType,
					Variety:         variety,
					Product:         product,
					OrderType:       orderType,
					Quantity:        quantity,
					Price:           price,
					TriggerPrice:    triggerPrice,
				},
			},
		})
		if err != nil {
			return nil, err
		}

		orderMarginsText := ""
		for _, orderMargin := range orderMargins {
			eachOrderMargin := printStruct(orderMargin)
			orderMarginsText += eachOrderMargin + "\n"
		}
		return mcp.NewToolResultText(orderMarginsText), nil
	}
}

func (z *ZerodhaMcpServer) Quote() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		instrument := request.Params.Arguments["instrument"].(string)
		quote, err := z.kc.GetQuote(instrument)
		if err != nil {
			return nil, err
		}
		quoteStr := printStruct(quote)
		return mcp.NewToolResultText(quoteStr), nil
	}
}

func (z *ZerodhaMcpServer) LTP() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		instrumentInterface, ok := request.Params.Arguments["instrument"]
		if !ok || instrumentInterface == nil {
			return nil, fmt.Errorf("instrument parameter is required")
		}

		instrument, ok := instrumentInterface.(string)
		if !ok {
			return nil, fmt.Errorf("instrument must be a string")
		}

		ltp, err := z.kc.GetLTP(instrument)
		if err != nil {
			return nil, err
		}
		ltpStr := printStruct(ltp)
		return mcp.NewToolResultText(ltpStr), nil
	}
}

func (z *ZerodhaMcpServer) OHLC() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		instrument := request.Params.Arguments["instrument"].(string)
		ohlc, err := z.kc.GetOHLC(instrument)
		if err != nil {
			return nil, err
		}
		ohlcStr := printStruct(ohlc)
		return mcp.NewToolResultText(ohlcStr), nil
	}
}

func (z *ZerodhaMcpServer) HistoricalData() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		instrumentToken := request.Params.Arguments["instrumentToken"].(int)
		interval := request.Params.Arguments["interval"].(string)
		fromDateStr := request.Params.Arguments["fromDate"].(string)
		toDateStr := request.Params.Arguments["toDate"].(string)
		continuousStr := request.Params.Arguments["continuous"].(string)
		oiStr := request.Params.Arguments["oi"].(string)

		fromDate, err := time.Parse(timeLayout, fromDateStr)
		if err != nil {
			return nil, err
		}

		toDate, err := time.Parse(timeLayout, toDateStr)
		if err != nil {
			return nil, err
		}

		continuous, err := strconv.ParseBool(continuousStr)
		if err != nil {
			return nil, err
		}

		oi, err := strconv.ParseBool(oiStr)
		if err != nil {
			return nil, err
		}

		historicalData, err := z.kc.GetHistoricalData(instrumentToken, interval, fromDate, toDate, continuous, oi)
		if err != nil {
			return nil, err
		}

		historicalDataStr := ""
		for _, candle := range historicalData {
			eachCandle := printStruct(candle)
			historicalDataStr += eachCandle + "\n"
		}
		return mcp.NewToolResultText(historicalDataStr), nil
	}
}

func (z *ZerodhaMcpServer) Instruments() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		instruments, err := z.kc.GetInstruments()
		if err != nil {
			return nil, err
		}
		instrumentsText := ""
		for _, instrument := range instruments {
			eachInstrument := printStruct(instrument)
			instrumentsText += eachInstrument + "\n"
		}
		return mcp.NewToolResultText(instrumentsText), nil
	}
}

func (z *ZerodhaMcpServer) InstrumentsByExchange() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		exchange := request.Params.Arguments["exchange"].(string)
		instruments, err := z.kc.GetInstrumentsByExchange(exchange)
		if err != nil {
			return nil, err
		}
		instrumentsText := ""
		for _, instrument := range instruments {
			eachInstrument := printStruct(instrument)
			instrumentsText += eachInstrument + "\n"
		}
		return mcp.NewToolResultText(instrumentsText), nil
	}
}

func (z *ZerodhaMcpServer) MFInstruments() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		instruments, err := z.kc.GetMFInstruments()
		if err != nil {
			return nil, err
		}
		instrumentsText := ""
		for _, instrument := range instruments {
			eachInstrument := printStruct(instrument)
			instrumentsText += eachInstrument + "\n"
		}
		return mcp.NewToolResultText(instrumentsText), nil
	}
}

func (z *ZerodhaMcpServer) MFOrders() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		mfOrders, err := z.kc.GetMFOrders()
		if err != nil {
			return nil, err
		}
		mfOrdersText := ""
		for _, order := range mfOrders {
			eachOrder := printStruct(order)
			mfOrdersText += eachOrder + "\n"
		}
		return mcp.NewToolResultText(mfOrdersText), nil
	}
}

func (z *ZerodhaMcpServer) MFOrderInfo() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		orderId := request.Params.Arguments["orderId"].(string)
		mfOrderInfo, err := z.kc.GetMFOrderInfo(orderId)
		if err != nil {
			return nil, err
		}
		mfOrderInfoStr := printStruct(mfOrderInfo)
		return mcp.NewToolResultText(mfOrderInfoStr), nil
	}
}

func (z *ZerodhaMcpServer) MfSipInfo() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		sipId := request.Params.Arguments["sipId"].(string)
		mfSipInfo, err := z.kc.GetMFSIPInfo(sipId)
		if err != nil {
			return nil, err
		}
		mfSipInfoStr := printStruct(mfSipInfo)
		return mcp.NewToolResultText(mfSipInfoStr), nil
	}
}

func (z *ZerodhaMcpServer) MFHoldings() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		holdings, err := z.kc.GetMFHoldings()
		if err != nil {
			return nil, err
		}
		holdingsText := ""
		for _, holding := range holdings {
			eachHolding := printStruct(holding)
			holdingsText += eachHolding + "\n"
		}
		return mcp.NewToolResultText(holdingsText), nil
	}
}

func (z *ZerodhaMcpServer) MFHoldingInfo() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		isin := request.Params.Arguments["isin"].(string)
		holdingInfo, err := z.kc.GetMFHoldingInfo(isin)
		if err != nil {
			return nil, err
		}
		holdingInfoStr := printStruct(holdingInfo)
		return mcp.NewToolResultText(holdingInfoStr), nil
	}
}

func (z *ZerodhaMcpServer) MFAllottedISINs() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		allottedISINs, err := z.kc.GetMFAllottedISINs()
		if err != nil {
			return nil, err
		}
		allottedISINsText := fmt.Sprintf("%s", allottedISINs)
		return mcp.NewToolResultText(allottedISINsText), nil
	}
}

func (z *ZerodhaMcpServer) UserProfile() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		userProfile, err := z.kc.GetUserProfile()
		if err != nil {
			return nil, err
		}
		userProfileStr := printStruct(userProfile)
		return mcp.NewToolResultText(userProfileStr), nil
	}
}

func (z *ZerodhaMcpServer) FullUserProfile() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		userProfile, err := z.kc.GetFullUserProfile()
		if err != nil {
			return nil, err
		}
		userProfileStr := printStruct(userProfile)
		return mcp.NewToolResultText(userProfileStr), nil
	}
}

func (z *ZerodhaMcpServer) UserMargins() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		userMargins, err := z.kc.GetUserMargins()
		if err != nil {
			return nil, err
		}
		userMarginsText := printStruct(userMargins)
		return mcp.NewToolResultText(userMarginsText), nil
	}
}

func (z *ZerodhaMcpServer) UserSegmentMargins() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		segment := request.Params.Arguments["segment"].(string)
		userSegmentMargins, err := z.kc.GetUserSegmentMargins(segment)
		if err != nil {
			return nil, err
		}
		userSegmentMarginsText := printStruct(userSegmentMargins)
		return mcp.NewToolResultText(userSegmentMarginsText), nil
	}
}

```