#
tokens: 12104/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

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

# Files

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

```
1 | vendor/
2 | .DS_Store
3 | 
4 | .idea/
```

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

```markdown
  1 | # Zerodha MCP Server
  2 | 
  3 | <p align="center">
  4 |   <strong>Protocol to communicate with your Zerodha data written in Golang</strong>
  5 | </p>
  6 | 
  7 | <p align="center">
  8 |   <img src="https://raw.githubusercontent.com/sukeesh/sukeesh.github.io/refs/heads/master/assets/img/Zerodha_MCP.png" alt="Zerodha MCP Logo" width="200" />
  9 | </p>
 10 | 
 11 | [![Go](https://github.com/sukeesh/zerodha-mcp-go/workflows/Go/badge.svg)](https://github.com/sukeesh/zerodha-mcp-go/actions)
 12 | 
 13 | ## Overview
 14 | 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.
 15 | 
 16 | ## Prerequisites
 17 | - [Go](https://go.dev/doc/install) (version 1.21 or later)
 18 | - A [Zerodha Kite](https://kite.zerodha.com) trading account
 19 | - [Claude Desktop App](https://claude.ai/download)
 20 | - API credentials from the [Kite Connect developer portal](https://developers.kite.trade/apps)
 21 | 
 22 | ## Installation
 23 | 
 24 | ### Option 1: Using Go Install
 25 | ```bash
 26 | go install github.com/sukeesh/zerodha-mcp@latest
 27 | ```
 28 | 
 29 | ### Option 2: Build from Source
 30 | ```bash
 31 | git clone https://github.com/sukeesh/zerodha-mcp.git
 32 | cd zerodha-mcp
 33 | go install
 34 | ```
 35 | 
 36 | The binary will be installed to your GOBIN directory, which should be in your PATH.
 37 | 
 38 | ## Usage with an MCP Client
 39 | 
 40 | ### GPT 4o mini
 41 | 
 42 | https://github.com/user-attachments/assets/849c4aca-0ca2-4aed-a9be-3df135f8a5c5
 43 | 
 44 | ### Claude Sonnet 3.7
 45 | 
 46 | <img width="500" alt="Claude as MCP Client" src="https://github.com/user-attachments/assets/932ec561-d7b4-4e58-8f1c-1f33b9c99896" />
 47 | 
 48 | 
 49 | ## Configuration
 50 | 
 51 | 1. Get your `ZERODHA_API_KEY` and `ZERODHA_API_SECRET` from the [Kite Connect developer portal](https://developers.kite.trade/apps)
 52 | 
 53 | 2. Set up a redirect URL in the Kite developer portal:
 54 |    ```
 55 |    http://127.0.0.1:5888/auth
 56 |    ```
 57 | 
 58 | 3. Configure Claude Desktop:
 59 |    - Open Claude Desktop → Settings → Developer → Edit Config
 60 |    - Add the following to your `claude_desktop_config.json`:
 61 | 
 62 | ```json
 63 | {
 64 |   "mcpServers": {
 65 |     "zerodha": {
 66 |       "command": "<path-to-zerodha-mcp-binary>",
 67 |       "env": {
 68 |        "ZERODHA_API_KEY": "<api_key>",
 69 |        "ZERODHA_API_SECRET": "<api_secret>"
 70 |       }
 71 |     }
 72 |   }
 73 | }
 74 | ```
 75 | 
 76 | 4. Restart Claude Desktop. When prompted, authenticate with your Zerodha Kite credentials.
 77 | 
 78 | ## Debugging
 79 | 
 80 | The logs for MCP Server are available at `~/Library/Logs/Claude`
 81 | 
 82 | ### Known Bugs
 83 | 
 84 | When the Claude desktop is shutdown, the underlying MCP Server is not getting killed.
 85 | ```bash
 86 | kill -9 $(lsof -t -i:5888)
 87 | ```
 88 | 
 89 | ## Available Tools
 90 | 
 91 | | Category | Tool | Status | Description |
 92 | |----------|------|--------|-------------|
 93 | | **Account Information** | `get_user_profile` | ✅ | Get basic user profile information |
 94 | | | `get_user_margins` | ✅ | Get all user margins |
 95 | | | `get_user_segment_margins` | ✅ | Get segment-wise user margins |
 96 | | **Portfolio & Positions** | `get_kite_holdings` | ✅ | Get current holdings in Zerodha Kite account |
 97 | | | `get_positions` | ✅ | Get current day and net positions |
 98 | | | `get_order_margins` | ✅ | Get margin requirements for specific orders |
 99 | | **Market Data** | `get_ltp` | ✅ | Get Last Traded Price for specific instruments |
100 | | | `get_quote` | ✅ | Get detailed quotes for specific instruments |
101 | | | `get_ohlc` | ✅ | Get Open, High, Low, Close quotes |
102 | | **Instruments** | `get_instruments` | ✅ | Get list of all available instruments on Zerodha |
103 | | | `get_instruments_by_exchange` | ✅ | Get instruments filtered by exchange |
104 | | | `get_auction_instruments` | ✅ | Get instruments available for auction sessions |
105 | | **Mutual Funds** | `get_mf_instruments` | ✅ | Get list of all available mutual fund instruments |
106 | | | `get_mf_holdings` | ✅ | Get list of mutual fund holdings |
107 | | | `get_mf_holdings_info` | ✅ | Get detailed information about mutual fund holdings |
108 | | | `get_mf_orders` | ✅ | Get list of all mutual fund orders |
109 | | | `get_mf_order_info` | ✅ | Get detailed information about specific mutual fund orders |
110 | | | `get_mf_sip_info` | ✅ | Get information about mutual fund SIPs |
111 | | | `get_mf_allotted_isins` | ✅ | Get allotted mutual fund ISINs |
112 | 
113 | 
114 | ## Usage
115 | 
116 | After setup, you can interact with your Zerodha account data directly through Claude. For example:
117 | 
118 | - "Show me my current portfolio holdings"
119 | - "What's my current margin availability?"
120 | - "Give me the latest price for RELIANCE"
121 | - "Show me my open positions with P&L"
122 | 
123 | 
124 | ## Limitations
125 | 
126 | - Only read operations are supported; trading is not yet available
127 | - Authentication token expires daily and requires re-login
128 | 
129 | 
```

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

```yaml
 1 | # This workflow will build a golang project
 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
 3 | 
 4 | name: Go
 5 | 
 6 | on:
 7 |   push:
 8 |     branches: [ "master" ]
 9 |   pull_request:
10 |     branches: [ "master" ]
11 | 
12 | jobs:
13 | 
14 |   build:
15 |     runs-on: ubuntu-latest
16 |     steps:
17 |     - uses: actions/checkout@v4
18 | 
19 |     - name: Set up Go
20 |       uses: actions/setup-go@v4
21 |       with:
22 |         go-version: '1.24.2'
23 | 
24 |     - name: Build
25 |       run: go build -v ./...
26 | 
```

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

```go
  1 | package internal
  2 | 
  3 | const (
  4 | 	HTMLHeaderTemplate = `<!DOCTYPE html>
  5 | <html lang="en">
  6 | <head>
  7 |     <meta charset="UTF-8">
  8 |     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  9 |     <title>Zerodha MCP Authentication</title>
 10 |     <style>
 11 |         body {
 12 |             font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 13 |             background-color: #f5f5f5;
 14 |             margin: 0;
 15 |             padding: 0;
 16 |             display: flex;
 17 |             justify-content: center;
 18 |             align-items: center;
 19 |             height: 100vh;
 20 |         }
 21 |         
 22 |         .container {
 23 |             background-color: white;
 24 |             border-radius: 10px;
 25 |             box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
 26 |             padding: 30px;
 27 |             text-align: center;
 28 |             max-width: 500px;
 29 |             width: 100%;
 30 |         }
 31 |         
 32 |         .success {
 33 |             color: #28a745;
 34 |         }
 35 |         
 36 |         .error {
 37 |             color: #dc3545;
 38 |         }
 39 |         
 40 |         h1 {
 41 |             margin-bottom: 20px;
 42 |             font-weight: 600;
 43 |         }
 44 |         
 45 |         p {
 46 |             margin-bottom: 25px;
 47 |             color: #6c757d;
 48 |             line-height: 1.6;
 49 |         }
 50 |         
 51 |         .icon {
 52 |             font-size: 64px;
 53 |             margin-bottom: 20px;
 54 |         }
 55 |         
 56 |         .btn {
 57 |             display: inline-block;
 58 |             background-color: #007bff;
 59 |             color: white;
 60 |             text-decoration: none;
 61 |             padding: 10px 20px;
 62 |             border-radius: 5px;
 63 |             transition: background-color 0.3s;
 64 |         }
 65 |         
 66 |         .btn:hover {
 67 |             background-color: #0069d9;
 68 |         }
 69 |         
 70 |         .zerodha-logo {
 71 |             margin-bottom: 20px;
 72 |             max-width: 150px;
 73 |         }
 74 |     </style>
 75 |     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
 76 | </head>
 77 | <body>`
 78 | 
 79 | 	HTMLFooterTemplate = `</body>
 80 | </html>`
 81 | 
 82 | 	SuccessContentTemplate = `    <div class="container">
 83 |         <img src="https://zerodha.com/static/images/logo.svg" alt="Zerodha Logo" class="zerodha-logo">
 84 |         <div class="icon success">
 85 |             <i class="fas fa-check-circle"></i>
 86 |         </div>
 87 |         <h1 class="success">Authentication Successful!</h1>
 88 |         <p>Your Zerodha account has been successfully authenticated with MCP Server.</p>
 89 |         <p>You can now close this window and continue using the Zerodha MCP tools.</p>
 90 |     </div>`
 91 | 
 92 | 	ErrorContentTemplate = `    <div class="container">
 93 |         <img src="https://zerodha.com/static/images/logo.svg" alt="Zerodha Logo" class="zerodha-logo">
 94 |         <div class="icon error">
 95 |             <i class="fas fa-times-circle"></i>
 96 |         </div>
 97 |         <h1 class="error">Authentication Failed</h1>
 98 |         <p>There was a problem authenticating your Zerodha account with MCP Server.</p>
 99 |         <p>Please try again or contact support if the issue persists.</p>
100 |         <a href="javascript:window.close();" class="btn">Close Window</a>
101 |     </div>`
102 | )
103 | 
104 | // RenderHTMLResponse combines HTML parts and returns the complete HTML string
105 | func RenderHTMLResponse(content string) string {
106 | 	return HTMLHeaderTemplate + content + HTMLFooterTemplate
107 | }
108 | 
```

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

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"errors"
  6 | 	"fmt"
  7 | 	"log"
  8 | 	"net/http"
  9 | 	"os"
 10 | 	"os/signal"
 11 | 	"syscall"
 12 | 	"time"
 13 | 
 14 | 	"github.com/toqueteos/webbrowser"
 15 | 
 16 | 	"github.com/gin-gonic/gin"
 17 | 	"github.com/mark3labs/mcp-go/mcp"
 18 | 	"github.com/mark3labs/mcp-go/server"
 19 | 	"github.com/sukeesh/zerodha-mcp/internal"
 20 | 	kiteconnect "github.com/zerodha/gokiteconnect/v4"
 21 | )
 22 | 
 23 | var (
 24 | 	requestToken    = ""
 25 | 	isAuthenticated = false
 26 | 	z               *internal.ZerodhaMcpServer
 27 | 
 28 | 	// Command line flags
 29 | 	apiKey    string
 30 | 	apiSecret string
 31 | )
 32 | 
 33 | func setEnvs() {
 34 | 	eApiKey := os.Getenv("ZERODHA_API_KEY")
 35 | 	eApiSecret := os.Getenv("ZERODHA_API_SECRET")
 36 | 
 37 | 	// Validate required flags
 38 | 	if eApiKey == "" || eApiSecret == "" {
 39 | 		fmt.Println("Error: apikey and apisecret flags are required")
 40 | 		fmt.Println("Usage example: ./zerodha-mcp -apikey=YOUR_API_KEY -apisecret=YOUR_API_SECRET")
 41 | 		os.Exit(1)
 42 | 	}
 43 | 
 44 | 	apiKey = eApiKey
 45 | 	apiSecret = eApiSecret
 46 | }
 47 | 
 48 | func renderHTMLResponse(c *gin.Context, content string, status int) {
 49 | 	html := internal.RenderHTMLResponse(content)
 50 | 	c.Data(status, "text/html; charset=utf-8", []byte(html))
 51 | }
 52 | 
 53 | func startRouter() (*http.Server, func()) {
 54 | 	gin.DefaultWriter = os.Stderr
 55 | 	gin.DefaultErrorWriter = os.Stderr
 56 | 
 57 | 	// use a fresh Engine so we don't get the default stdout logger
 58 | 	r := gin.New()
 59 | 	r.Use(
 60 | 		gin.LoggerWithWriter(os.Stderr),
 61 | 		gin.RecoveryWithWriter(os.Stderr),
 62 | 	)
 63 | 
 64 | 	r.GET("/ping", func(c *gin.Context) {
 65 | 		c.JSON(http.StatusOK, gin.H{
 66 | 			"message": "pong",
 67 | 		})
 68 | 	})
 69 | 
 70 | 	r.GET("/auth", func(c *gin.Context) {
 71 | 		fmt.Fprintln(os.Stderr, "Request received on auth request")
 72 | 		paramRequestToken := c.Query("request_token")
 73 | 		if paramRequestToken == "" {
 74 | 			// Render error template
 75 | 			renderHTMLResponse(c, internal.ErrorContentTemplate, http.StatusBadRequest)
 76 | 			return
 77 | 		}
 78 | 
 79 | 		requestToken = paramRequestToken
 80 | 		if requestToken != "" {
 81 | 			isAuthenticated = true
 82 | 			// Render success template
 83 | 			renderHTMLResponse(c, internal.SuccessContentTemplate, http.StatusOK)
 84 | 		} else {
 85 | 			// Render error template
 86 | 			renderHTMLResponse(c, internal.ErrorContentTemplate, http.StatusBadRequest)
 87 | 		}
 88 | 	})
 89 | 
 90 | 	srv := &http.Server{
 91 | 		Addr:    ":5888",
 92 | 		Handler: r,
 93 | 	}
 94 | 
 95 | 	// Create a shutdown function
 96 | 	shutdownFn := func() {
 97 | 		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 98 | 		defer cancel()
 99 | 
100 | 		if err := srv.Shutdown(ctx); err != nil {
101 | 			log.Printf("HTTP server shutdown error: %v", err)
102 | 		}
103 | 		log.Println("HTTP server stopped")
104 | 	}
105 | 
106 | 	go func() {
107 | 		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
108 | 			log.Fatalf("listen: %s\n", err)
109 | 		}
110 | 	}()
111 | 
112 | 	return srv, shutdownFn
113 | }
114 | 
115 | func kiteAuthenticate() *kiteconnect.Client {
116 | 	isAuthenticated = false
117 | 
118 | 	kc := kiteconnect.New(apiKey)
119 | 	webbrowser.Open(kc.GetLoginURL())
120 | 
121 | 	curTime := time.Now()
122 | 
123 | 	for {
124 | 		time.Sleep(5 * time.Second)
125 | 		if isAuthenticated {
126 | 			break
127 | 		} else {
128 | 			fmt.Fprintln(os.Stderr, fmt.Sprintf("Waiting for authentication from user. Please authenticate from %s", kc.GetLoginURL()))
129 | 		}
130 | 
131 | 		if time.Since(curTime) > time.Minute*2 {
132 | 			fmt.Fprintln(os.Stderr, "2 mins and no auth yet for Zerodha, Exiting...")
133 | 			os.Exit(1)
134 | 		}
135 | 	}
136 | 
137 | 	data, err := kc.GenerateSession(requestToken, apiSecret)
138 | 	if err != nil {
139 | 		fmt.Fprintln(os.Stderr, err)
140 | 		return nil
141 | 	}
142 | 
143 | 	kc.SetAccessToken(data.AccessToken)
144 | 	return kc
145 | }
146 | 
147 | func mcpMain(ctx context.Context, s *server.MCPServer, kc *kiteconnect.Client) {
148 | 	z = internal.NewZerodhaMcpServer(kc)
149 | 
150 | 	kiteHoldingsTool := mcp.NewTool("get_kite_holdings",
151 | 		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."),
152 | 	)
153 | 	s.AddTool(kiteHoldingsTool, z.KiteHoldingsTool())
154 | 
155 | 	auctionInstrumentsTool := mcp.NewTool("get_auction_instruments",
156 | 		mcp.WithDescription("Retrieves list of available instruments for a auction session"),
157 | 	)
158 | 	s.AddTool(auctionInstrumentsTool, z.AuctionInstrumentsTool())
159 | 
160 | 	positionsTool := mcp.NewTool("get_positions",
161 | 		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."),
162 | 	)
163 | 	s.AddTool(positionsTool, z.Positions())
164 | 
165 | 	orderMarginsTool := mcp.NewTool("get_order_margins",
166 | 		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."),
167 | 		mcp.WithString("exchange",
168 | 			mcp.Required(),
169 | 			mcp.Description("The exchange value"),
170 | 			mcp.Enum("nse", "bse"),
171 | 		),
172 | 		mcp.WithString("tradingSymbol",
173 | 			mcp.Required(),
174 | 			mcp.Description("The trading symbol"),
175 | 		),
176 | 		mcp.WithString("transactionType",
177 | 			mcp.Required(),
178 | 			mcp.Description("The transaction type"),
179 | 		),
180 | 		mcp.WithString("variety",
181 | 			mcp.Required(),
182 | 			mcp.Description("Variety"),
183 | 		),
184 | 		mcp.WithString("product",
185 | 			mcp.Required(),
186 | 			mcp.Description("Product"),
187 | 		),
188 | 		mcp.WithString("orderType",
189 | 			mcp.Required(),
190 | 			mcp.Description("Order Type"),
191 | 		),
192 | 		mcp.WithNumber("quantity",
193 | 			mcp.Required(),
194 | 			mcp.Description("Quantity"),
195 | 		),
196 | 		mcp.WithNumber("price",
197 | 			mcp.Required(),
198 | 			mcp.Description("Price"),
199 | 		),
200 | 		mcp.WithNumber("triggerPrice",
201 | 			mcp.Required(),
202 | 			mcp.Description("Trigger Price"),
203 | 		),
204 | 	)
205 | 	s.AddTool(orderMarginsTool, z.OrderMargins())
206 | 
207 | 	quoteTool := mcp.NewTool("get_quote",
208 | 		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."),
209 | 		mcp.WithString("instrument",
210 | 			mcp.Required(),
211 | 			mcp.Description("format of `exchange:tradingsymbol`"),
212 | 		),
213 | 	)
214 | 	s.AddTool(quoteTool, z.Quote())
215 | 
216 | 	ltpTool := mcp.NewTool("get_ltp",
217 | 		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."),
218 | 		mcp.WithString("instrument",
219 | 			mcp.Required(),
220 | 			mcp.Description("format of `exchange:tradingsymbol`"),
221 | 		),
222 | 	)
223 | 	s.AddTool(ltpTool, z.LTP())
224 | 
225 | 	ohlcTool := mcp.NewTool("get_ohlc",
226 | 		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."),
227 | 		mcp.WithString("instrument",
228 | 			mcp.Required(),
229 | 			mcp.Description("format of `exchange:tradingsymbol`"),
230 | 		),
231 | 	)
232 | 	s.AddTool(ohlcTool, z.OHLC())
233 | 
234 | 	// TODO: Complete Historical data tool. Need a way to consume huge amount of data.
235 | 
236 | 	instrumentsTool := mcp.NewTool("get_instruments",
237 | 		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."),
238 | 	)
239 | 	s.AddTool(instrumentsTool, z.Instruments())
240 | 
241 | 	instrumentsByExchange := mcp.NewTool("get_instruments_by_exchange",
242 | 		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."),
243 | 		mcp.WithString("exchange",
244 | 			mcp.Required(),
245 | 			mcp.Description("The exchange value"),
246 | 			mcp.Enum("nse", "bse"),
247 | 		),
248 | 	)
249 | 	s.AddTool(instrumentsByExchange, z.InstrumentsByExchange())
250 | 
251 | 	mfInstruments := mcp.NewTool("get_mf_instruments",
252 | 		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."),
253 | 	)
254 | 	s.AddTool(mfInstruments, z.MFInstruments())
255 | 
256 | 	mfOrders := mcp.NewTool("get_mf_orders",
257 | 		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."),
258 | 	)
259 | 	s.AddTool(mfOrders, z.MFOrders())
260 | 
261 | 	mfOrderInfo := mcp.NewTool("get_mf_order_info",
262 | 		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."),
263 | 		mcp.WithString("orderId",
264 | 			mcp.Required(),
265 | 			mcp.Description("The Order ID of the mutual fund"),
266 | 		))
267 | 	s.AddTool(mfOrderInfo, z.MFOrderInfo())
268 | 
269 | 	mfSipInfo := mcp.NewTool("get_mf_sip_info",
270 | 		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."),
271 | 		mcp.WithString("sipId",
272 | 			mcp.Required(),
273 | 			mcp.Description("The SIP ID of the mutual fund"),
274 | 		))
275 | 	s.AddTool(mfSipInfo, z.MfSipInfo())
276 | 
277 | 	mfHoldings := mcp.NewTool("get_mf_holdings",
278 | 		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."),
279 | 	)
280 | 	s.AddTool(mfHoldings, z.MFHoldings())
281 | 
282 | 	mfHoldingsInfo := mcp.NewTool("get_mf_holdings_info",
283 | 		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."),
284 | 		mcp.WithString("isin",
285 | 			mcp.Required(),
286 | 			mcp.Description("The ISIN of the mutual fund holding"),
287 | 		))
288 | 	s.AddTool(mfHoldingsInfo, z.MFHoldingInfo())
289 | 
290 | 	mfAllottedIsins := mcp.NewTool("get_mf_allotted_isins",
291 | 		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."))
292 | 	s.AddTool(mfAllottedIsins, z.MFAllottedISINs())
293 | 
294 | 	userProfile := mcp.NewTool("get_user_profile",
295 | 		mcp.WithDescription("Get basic user profile. This tool provides basic information about the user, including the user ID, name, and other relevant details."),
296 | 	)
297 | 	s.AddTool(userProfile, z.UserProfile())
298 | 
299 | 	// TODO: Figure out the right permissions for this
300 | 	//fullUserProfile := mcp.NewTool("get_full_user_profile",
301 | 	//	mcp.WithDescription("get full user profile"))
302 | 	//s.AddTool(fullUserProfile, z.FullUserProfile())
303 | 
304 | 	userMargins := mcp.NewTool("get_user_margins",
305 | 		mcp.WithDescription("Get all user margins. This tool provides a comprehensive list of all the margins that can be traded on Zerodha."))
306 | 	s.AddTool(userMargins, z.UserMargins())
307 | 
308 | 	userSegmentMargins := mcp.NewTool("get_user_segment_margins",
309 | 		mcp.WithDescription("Get segment wise user margins. This tool provides a comprehensive list of all the margins that can be traded on Zerodha."),
310 | 		mcp.WithString("segment",
311 | 			mcp.Required(),
312 | 			mcp.Description("segment of the mutual fund holding"),
313 | 		),
314 | 	)
315 | 	s.AddTool(userSegmentMargins, z.UserSegmentMargins())
316 | 
317 | 	// Start the server and handle interruption via context
318 | 	go func() {
319 | 		<-ctx.Done()
320 | 		// This will only happen when ctx is cancelled - implement any cleanup needed here
321 | 		log.Println("MCP server received shutdown signal")
322 | 	}()
323 | 
324 | 	if err := server.ServeStdio(s); err != nil {
325 | 		log.Fatalf("Server error: %v", err)
326 | 	}
327 | }
328 | 
329 | func main() {
330 | 	setEnvs()
331 | 
332 | 	quit := make(chan os.Signal, 1)
333 | 	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
334 | 
335 | 	s := server.NewMCPServer(
336 | 		"Zerodha MCP Server",
337 | 		"0.0.1",
338 | 		server.WithResourceCapabilities(true, true),
339 | 		server.WithLogging(),
340 | 		server.WithRecovery(),
341 | 	)
342 | 
343 | 	// Start the router and get the shutdown function
344 | 	_, httpShutdownFn := startRouter()
345 | 
346 | 	kc := kiteAuthenticate()
347 | 	fmt.Fprintln(os.Stderr, "Zerodha authentication successful, starting MCP Server...")
348 | 
349 | 	// Create a context that can be cancelled
350 | 	ctx, cancel := context.WithCancel(context.Background())
351 | 
352 | 	// Start the MCP server in a goroutine
353 | 	mcpDone := make(chan struct{})
354 | 	go func() {
355 | 		defer close(mcpDone)
356 | 		mcpMain(ctx, s, kc)
357 | 	}()
358 | 
359 | 	// Wait for quit signal
360 | 	<-quit
361 | 	log.Println("Shutting down server...")
362 | 
363 | 	// Cancel the context to signal all operations to stop
364 | 	cancel()
365 | 
366 | 	// Call the HTTP server shutdown function
367 | 	httpShutdownFn()
368 | 
369 | 	// Wait for the MCP server to finish or timeout
370 | 	select {
371 | 	case <-mcpDone:
372 | 		log.Println("MCP server stopped")
373 | 	case <-time.After(5 * time.Second):
374 | 		log.Println("MCP server shutdown timed out")
375 | 	}
376 | 
377 | 	log.Println("Server exiting")
378 | 	os.Exit(0)
379 | }
380 | 
```

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

```go
  1 | package internal
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"reflect"
  7 | 	"strconv"
  8 | 	"time"
  9 | 
 10 | 	"github.com/mark3labs/mcp-go/mcp"
 11 | 	"github.com/mark3labs/mcp-go/server"
 12 | 	kiteconnect "github.com/zerodha/gokiteconnect/v4"
 13 | )
 14 | 
 15 | const (
 16 | 	timeLayout = "2006-01-02 15:04:05"
 17 | )
 18 | 
 19 | type ZerodhaMcpServer struct {
 20 | 	kc *kiteconnect.Client
 21 | }
 22 | 
 23 | func NewZerodhaMcpServer(kc *kiteconnect.Client) *ZerodhaMcpServer {
 24 | 	return &ZerodhaMcpServer{
 25 | 		kc: kc,
 26 | 	}
 27 | }
 28 | 
 29 | func (z *ZerodhaMcpServer) SetKc(kc *kiteconnect.Client) {
 30 | 	z.kc = kc
 31 | }
 32 | 
 33 | func printStruct(s interface{}) string {
 34 | 	val := reflect.ValueOf(s)
 35 | 	typ := reflect.TypeOf(s)
 36 | 
 37 | 	if val.Kind() == reflect.Ptr {
 38 | 		val = val.Elem()
 39 | 		typ = typ.Elem()
 40 | 	}
 41 | 
 42 | 	returnVal := "<start> "
 43 | 
 44 | 	for i := 0; i < val.NumField(); i++ {
 45 | 		field := val.Field(i)
 46 | 		fieldName := typ.Field(i).Name
 47 | 		returnVal += fmt.Sprintf("%s: %v, ", fieldName, field.Interface())
 48 | 	}
 49 | 	returnVal += " <end>"
 50 | 
 51 | 	return returnVal
 52 | }
 53 | 
 54 | func getHoldingText(holding kiteconnect.Holding) string {
 55 | 	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"
 56 | 	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)
 57 | }
 58 | 
 59 | func (z *ZerodhaMcpServer) KiteHoldingsTool() server.ToolHandlerFunc {
 60 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 61 | 		holdings, err := z.kc.GetHoldings()
 62 | 		if err != nil {
 63 | 			return nil, err
 64 | 		}
 65 | 		holdingsText := ""
 66 | 		for _, holding := range holdings {
 67 | 			eachHolding := getHoldingText(holding)
 68 | 			holdingsText += eachHolding + "\n"
 69 | 		}
 70 | 
 71 | 		return mcp.NewToolResultText(holdingsText), nil
 72 | 	}
 73 | }
 74 | 
 75 | func (z *ZerodhaMcpServer) AuctionInstrumentsTool() server.ToolHandlerFunc {
 76 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 77 | 		auctionInstruments, err := z.kc.GetAuctionInstruments()
 78 | 		if err != nil {
 79 | 			return nil, err
 80 | 		}
 81 | 		auctionInstrumentsText := ""
 82 | 		for _, auctionInstrument := range auctionInstruments {
 83 | 			eachAuctionInstrument := printStruct(auctionInstrument)
 84 | 			auctionInstrumentsText += eachAuctionInstrument + "\n"
 85 | 		}
 86 | 
 87 | 		return mcp.NewToolResultText(auctionInstrumentsText), nil
 88 | 	}
 89 | }
 90 | 
 91 | func (z *ZerodhaMcpServer) Positions() server.ToolHandlerFunc {
 92 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 93 | 		positions, err := z.kc.GetPositions()
 94 | 		if err != nil {
 95 | 			return nil, err
 96 | 		}
 97 | 		dayPositions := "DAY POSITIONS --- "
 98 | 		for _, eachPosition := range positions.Day {
 99 | 			eachPositionText := printStruct(eachPosition)
100 | 			dayPositions += eachPositionText + "\n"
101 | 		}
102 | 		netPositions := "NET POSITIONS --- "
103 | 		for _, position := range positions.Net {
104 | 			eachPosition := printStruct(position)
105 | 			netPositions += eachPosition + "\n"
106 | 		}
107 | 
108 | 		positionsText := dayPositions + " \n \n " + netPositions
109 | 		return mcp.NewToolResultText(positionsText), nil
110 | 	}
111 | }
112 | 
113 | func (z *ZerodhaMcpServer) OrderMargins() server.ToolHandlerFunc {
114 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
115 | 		// TODO: Currently accepting only single object, figure out a way with mcp.WithObject to deal with slice of objects
116 | 
117 | 		exchange := request.Params.Arguments["exchange"].(string)
118 | 		tradingSymbol := request.Params.Arguments["tradingSymbol"].(string)
119 | 		transactionType := request.Params.Arguments["transactionType"].(string)
120 | 		variety := request.Params.Arguments["variety"].(string)
121 | 		product := request.Params.Arguments["product"].(string)
122 | 		orderType := request.Params.Arguments["orderType"].(string)
123 | 		quantity := request.Params.Arguments["quantity"].(float64)
124 | 		price := request.Params.Arguments["price"].(float64)
125 | 		triggerPrice := request.Params.Arguments["triggerPrice"].(float64)
126 | 
127 | 		orderMargins, err := z.kc.GetOrderMargins(kiteconnect.GetMarginParams{
128 | 			OrderParams: []kiteconnect.OrderMarginParam{
129 | 				{
130 | 					Exchange:        exchange,
131 | 					Tradingsymbol:   tradingSymbol,
132 | 					TransactionType: transactionType,
133 | 					Variety:         variety,
134 | 					Product:         product,
135 | 					OrderType:       orderType,
136 | 					Quantity:        quantity,
137 | 					Price:           price,
138 | 					TriggerPrice:    triggerPrice,
139 | 				},
140 | 			},
141 | 		})
142 | 		if err != nil {
143 | 			return nil, err
144 | 		}
145 | 
146 | 		orderMarginsText := ""
147 | 		for _, orderMargin := range orderMargins {
148 | 			eachOrderMargin := printStruct(orderMargin)
149 | 			orderMarginsText += eachOrderMargin + "\n"
150 | 		}
151 | 		return mcp.NewToolResultText(orderMarginsText), nil
152 | 	}
153 | }
154 | 
155 | func (z *ZerodhaMcpServer) Quote() server.ToolHandlerFunc {
156 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
157 | 		instrument := request.Params.Arguments["instrument"].(string)
158 | 		quote, err := z.kc.GetQuote(instrument)
159 | 		if err != nil {
160 | 			return nil, err
161 | 		}
162 | 		quoteStr := printStruct(quote)
163 | 		return mcp.NewToolResultText(quoteStr), nil
164 | 	}
165 | }
166 | 
167 | func (z *ZerodhaMcpServer) LTP() server.ToolHandlerFunc {
168 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
169 | 		instrumentInterface, ok := request.Params.Arguments["instrument"]
170 | 		if !ok || instrumentInterface == nil {
171 | 			return nil, fmt.Errorf("instrument parameter is required")
172 | 		}
173 | 
174 | 		instrument, ok := instrumentInterface.(string)
175 | 		if !ok {
176 | 			return nil, fmt.Errorf("instrument must be a string")
177 | 		}
178 | 
179 | 		ltp, err := z.kc.GetLTP(instrument)
180 | 		if err != nil {
181 | 			return nil, err
182 | 		}
183 | 		ltpStr := printStruct(ltp)
184 | 		return mcp.NewToolResultText(ltpStr), nil
185 | 	}
186 | }
187 | 
188 | func (z *ZerodhaMcpServer) OHLC() server.ToolHandlerFunc {
189 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
190 | 		instrument := request.Params.Arguments["instrument"].(string)
191 | 		ohlc, err := z.kc.GetOHLC(instrument)
192 | 		if err != nil {
193 | 			return nil, err
194 | 		}
195 | 		ohlcStr := printStruct(ohlc)
196 | 		return mcp.NewToolResultText(ohlcStr), nil
197 | 	}
198 | }
199 | 
200 | func (z *ZerodhaMcpServer) HistoricalData() server.ToolHandlerFunc {
201 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
202 | 		instrumentToken := request.Params.Arguments["instrumentToken"].(int)
203 | 		interval := request.Params.Arguments["interval"].(string)
204 | 		fromDateStr := request.Params.Arguments["fromDate"].(string)
205 | 		toDateStr := request.Params.Arguments["toDate"].(string)
206 | 		continuousStr := request.Params.Arguments["continuous"].(string)
207 | 		oiStr := request.Params.Arguments["oi"].(string)
208 | 
209 | 		fromDate, err := time.Parse(timeLayout, fromDateStr)
210 | 		if err != nil {
211 | 			return nil, err
212 | 		}
213 | 
214 | 		toDate, err := time.Parse(timeLayout, toDateStr)
215 | 		if err != nil {
216 | 			return nil, err
217 | 		}
218 | 
219 | 		continuous, err := strconv.ParseBool(continuousStr)
220 | 		if err != nil {
221 | 			return nil, err
222 | 		}
223 | 
224 | 		oi, err := strconv.ParseBool(oiStr)
225 | 		if err != nil {
226 | 			return nil, err
227 | 		}
228 | 
229 | 		historicalData, err := z.kc.GetHistoricalData(instrumentToken, interval, fromDate, toDate, continuous, oi)
230 | 		if err != nil {
231 | 			return nil, err
232 | 		}
233 | 
234 | 		historicalDataStr := ""
235 | 		for _, candle := range historicalData {
236 | 			eachCandle := printStruct(candle)
237 | 			historicalDataStr += eachCandle + "\n"
238 | 		}
239 | 		return mcp.NewToolResultText(historicalDataStr), nil
240 | 	}
241 | }
242 | 
243 | func (z *ZerodhaMcpServer) Instruments() server.ToolHandlerFunc {
244 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
245 | 		instruments, err := z.kc.GetInstruments()
246 | 		if err != nil {
247 | 			return nil, err
248 | 		}
249 | 		instrumentsText := ""
250 | 		for _, instrument := range instruments {
251 | 			eachInstrument := printStruct(instrument)
252 | 			instrumentsText += eachInstrument + "\n"
253 | 		}
254 | 		return mcp.NewToolResultText(instrumentsText), nil
255 | 	}
256 | }
257 | 
258 | func (z *ZerodhaMcpServer) InstrumentsByExchange() server.ToolHandlerFunc {
259 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
260 | 		exchange := request.Params.Arguments["exchange"].(string)
261 | 		instruments, err := z.kc.GetInstrumentsByExchange(exchange)
262 | 		if err != nil {
263 | 			return nil, err
264 | 		}
265 | 		instrumentsText := ""
266 | 		for _, instrument := range instruments {
267 | 			eachInstrument := printStruct(instrument)
268 | 			instrumentsText += eachInstrument + "\n"
269 | 		}
270 | 		return mcp.NewToolResultText(instrumentsText), nil
271 | 	}
272 | }
273 | 
274 | func (z *ZerodhaMcpServer) MFInstruments() server.ToolHandlerFunc {
275 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
276 | 		instruments, err := z.kc.GetMFInstruments()
277 | 		if err != nil {
278 | 			return nil, err
279 | 		}
280 | 		instrumentsText := ""
281 | 		for _, instrument := range instruments {
282 | 			eachInstrument := printStruct(instrument)
283 | 			instrumentsText += eachInstrument + "\n"
284 | 		}
285 | 		return mcp.NewToolResultText(instrumentsText), nil
286 | 	}
287 | }
288 | 
289 | func (z *ZerodhaMcpServer) MFOrders() server.ToolHandlerFunc {
290 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
291 | 		mfOrders, err := z.kc.GetMFOrders()
292 | 		if err != nil {
293 | 			return nil, err
294 | 		}
295 | 		mfOrdersText := ""
296 | 		for _, order := range mfOrders {
297 | 			eachOrder := printStruct(order)
298 | 			mfOrdersText += eachOrder + "\n"
299 | 		}
300 | 		return mcp.NewToolResultText(mfOrdersText), nil
301 | 	}
302 | }
303 | 
304 | func (z *ZerodhaMcpServer) MFOrderInfo() server.ToolHandlerFunc {
305 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
306 | 		orderId := request.Params.Arguments["orderId"].(string)
307 | 		mfOrderInfo, err := z.kc.GetMFOrderInfo(orderId)
308 | 		if err != nil {
309 | 			return nil, err
310 | 		}
311 | 		mfOrderInfoStr := printStruct(mfOrderInfo)
312 | 		return mcp.NewToolResultText(mfOrderInfoStr), nil
313 | 	}
314 | }
315 | 
316 | func (z *ZerodhaMcpServer) MfSipInfo() server.ToolHandlerFunc {
317 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
318 | 		sipId := request.Params.Arguments["sipId"].(string)
319 | 		mfSipInfo, err := z.kc.GetMFSIPInfo(sipId)
320 | 		if err != nil {
321 | 			return nil, err
322 | 		}
323 | 		mfSipInfoStr := printStruct(mfSipInfo)
324 | 		return mcp.NewToolResultText(mfSipInfoStr), nil
325 | 	}
326 | }
327 | 
328 | func (z *ZerodhaMcpServer) MFHoldings() server.ToolHandlerFunc {
329 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
330 | 		holdings, err := z.kc.GetMFHoldings()
331 | 		if err != nil {
332 | 			return nil, err
333 | 		}
334 | 		holdingsText := ""
335 | 		for _, holding := range holdings {
336 | 			eachHolding := printStruct(holding)
337 | 			holdingsText += eachHolding + "\n"
338 | 		}
339 | 		return mcp.NewToolResultText(holdingsText), nil
340 | 	}
341 | }
342 | 
343 | func (z *ZerodhaMcpServer) MFHoldingInfo() server.ToolHandlerFunc {
344 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
345 | 		isin := request.Params.Arguments["isin"].(string)
346 | 		holdingInfo, err := z.kc.GetMFHoldingInfo(isin)
347 | 		if err != nil {
348 | 			return nil, err
349 | 		}
350 | 		holdingInfoStr := printStruct(holdingInfo)
351 | 		return mcp.NewToolResultText(holdingInfoStr), nil
352 | 	}
353 | }
354 | 
355 | func (z *ZerodhaMcpServer) MFAllottedISINs() server.ToolHandlerFunc {
356 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
357 | 		allottedISINs, err := z.kc.GetMFAllottedISINs()
358 | 		if err != nil {
359 | 			return nil, err
360 | 		}
361 | 		allottedISINsText := fmt.Sprintf("%s", allottedISINs)
362 | 		return mcp.NewToolResultText(allottedISINsText), nil
363 | 	}
364 | }
365 | 
366 | func (z *ZerodhaMcpServer) UserProfile() server.ToolHandlerFunc {
367 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
368 | 		userProfile, err := z.kc.GetUserProfile()
369 | 		if err != nil {
370 | 			return nil, err
371 | 		}
372 | 		userProfileStr := printStruct(userProfile)
373 | 		return mcp.NewToolResultText(userProfileStr), nil
374 | 	}
375 | }
376 | 
377 | func (z *ZerodhaMcpServer) FullUserProfile() server.ToolHandlerFunc {
378 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
379 | 		userProfile, err := z.kc.GetFullUserProfile()
380 | 		if err != nil {
381 | 			return nil, err
382 | 		}
383 | 		userProfileStr := printStruct(userProfile)
384 | 		return mcp.NewToolResultText(userProfileStr), nil
385 | 	}
386 | }
387 | 
388 | func (z *ZerodhaMcpServer) UserMargins() server.ToolHandlerFunc {
389 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
390 | 		userMargins, err := z.kc.GetUserMargins()
391 | 		if err != nil {
392 | 			return nil, err
393 | 		}
394 | 		userMarginsText := printStruct(userMargins)
395 | 		return mcp.NewToolResultText(userMarginsText), nil
396 | 	}
397 | }
398 | 
399 | func (z *ZerodhaMcpServer) UserSegmentMargins() server.ToolHandlerFunc {
400 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
401 | 		segment := request.Params.Arguments["segment"].(string)
402 | 		userSegmentMargins, err := z.kc.GetUserSegmentMargins(segment)
403 | 		if err != nil {
404 | 			return nil, err
405 | 		}
406 | 		userSegmentMarginsText := printStruct(userSegmentMargins)
407 | 		return mcp.NewToolResultText(userSegmentMarginsText), nil
408 | 	}
409 | }
410 | 
```