# Directory Structure
```
├── .env.example
├── .gitignore
├── .python-version
├── Dockerfile
├── image.png
├── LICENSE
├── main.py
├── pyproject.toml
├── README.md
├── smithery.yaml
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
1 | 3.13
2 |
```
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | # Zerodha API credentials
2 | # Get these from https://developers.kite.trade/ after creating your app
3 | KITE_API_KEY=your_api_key_here
4 | KITE_API_SECRET=your_api_secret_here
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 |
9 | # Virtual environments
10 | .venv
11 |
12 | # Environment variables
13 | .env
14 | *.env
15 |
16 | # Authentication tokens
17 | .tokens
18 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Zerodha MCP Integration
2 | [](https://smithery.ai/server/@aptro/zerodha-mcp)
3 |
4 | This project integrates Zerodha's trading platform with Claude AI using the Multi-Cloud Plugin (MCP) framework, allowing you to interact with your Zerodha trading account directly through Claude.
5 |
6 | ## Setup Instructions
7 |
8 | ### Installing via Smithery
9 |
10 | To install zerodha-mcp for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@aptro/zerodha-mcp):
11 |
12 | ```bash
13 | npx -y @smithery/cli install @aptro/zerodha-mcp --client claude
14 | ```
15 |
16 | ### 1. Create a Zerodha Developer Account
17 |
18 | 1. Go to [Kite Connect](https://developers.kite.trade/) and sign up for a developer account
19 | 2. Log in to your account at [developers.kite.trade](https://developers.kite.trade/)
20 |
21 | ### 2. Create a New App
22 |
23 | 1. Navigate to the "Apps" section in your Kite Developer dashboard
24 | 2. Click on "Create a new app"
25 | 3. Fill in the required details:
26 | - App Name: Choose a descriptive name (e.g., "Claude Zerodha Integration")
27 | - App Category: Select "Personal" or appropriate category
28 | - Redirect URL: Set to `http://127.0.0.1:5000/zerodha/auth/redirect`
29 | - Description: Briefly describe your application's purpose
30 | 4. Submit the form to create your app
31 |
32 | ### 3. Get API Credentials
33 |
34 | After creating your app, you'll receive:
35 | - API Key (also called Consumer Key)
36 | - API Secret (also called Consumer Secret)
37 |
38 | These credentials will be displayed on your app's details page.
39 |
40 | ### 4. Configure Environment Variables
41 |
42 | 1. Create a `.env` file in the root directory of this project
43 | 2. Add your API credentials to the file:
44 |
45 | ```
46 | KITE_API_KEY=your_api_key_here
47 | KITE_API_SECRET=your_api_secret_here
48 | ```
49 |
50 | Replace `your_api_key_here` and `your_api_secret_here` with the actual credentials from step 3.
51 |
52 | ### 5. Install Dependencies
53 |
54 | Make sure you have all required dependencies installed:
55 |
56 | ```bash
57 | uv pip install kiteconnect fastapi uvicorn python-dotenv httpx
58 | ```
59 |
60 | ### 6. Install MCP config on your Claude desktop app
61 |
62 | Install the MCP config on your Claude desktop app:
63 |
64 | ```bash
65 | mcp install main.py
66 | ```
67 |
68 | This command registers the Zerodha plugin with Claude, making all trading functionality available to the AI.
69 |
70 | ## Usage
71 |
72 | After setup, you can interact with your Zerodha account via Claude using the following features:
73 |
74 | ### Authentication
75 |
76 | ```
77 | Can you please check if I'm logged into my Zerodha account and authenticate if needed?
78 | ```
79 |
80 | ### Stocks and General Trading
81 |
82 | - Check account margins: `What are my current margins on Zerodha?`
83 | - View portfolio holdings: `Show me my current holdings on Zerodha`
84 | - Check current positions: `What positions do I currently have open on Zerodha?`
85 | - Get quotes for symbols: `What's the current price of RELIANCE and INFY on NSE?`
86 | - Place an order: `Place a buy order for 10 shares of INFY at market price on NSE`
87 | - Get historical data: `Can you show me the historical price data for SBIN for the last 30 days?`
88 |
89 | ### Mutual Funds
90 |
91 | - View mutual fund holdings: `Show me my mutual fund holdings on Zerodha`
92 | - Get mutual fund orders: `List all my mutual fund orders on Zerodha`
93 | - Place a mutual fund order: `Place a buy order for ₹5000 in the mutual fund with symbol INF090I01239`
94 | - Cancel a mutual fund order: `Cancel my mutual fund order with order ID 123456789`
95 | - View SIP details: `Show all my active SIPs on Zerodha`
96 | - Create a new SIP: `Set up a monthly SIP of ₹2000 for the fund with symbol INF090I01239 for 12 installments`
97 | - Modify an existing SIP: `Change my SIP with ID 987654321 to ₹3000 per month`
98 | - Cancel a SIP: `Cancel my SIP with ID 987654321`
99 | - Browse available mutual funds: `Show me a list of available mutual funds on Zerodha`
100 |
101 | ## Authentication Flow
102 |
103 | The first time you use any Zerodha functionality, Claude will:
104 | 1. Start a local server on port 5000
105 | 2. Open a browser window for Zerodha login
106 | 3. After successful login, store the access token for future sessions
107 |
108 | Your session will remain active until the token expires (typically 24 hours). When the token expires, Claude will automatically initiate the login flow again.
109 |
110 | ## Available MCP Tools
111 |
112 | This plugin offers the following MCP tools that Claude can use:
113 |
114 | ### Authentication
115 | - `check_and_authenticate` - Verifies authentication status and initiates login if needed
116 | - `initiate_login` - Starts the Zerodha login flow
117 | - `get_request_token` - Retrieves the request token after login
118 |
119 | ### Stock/General Trading
120 | - `get_holdings` - Retrieves portfolio holdings
121 | - `get_positions` - Gets current positions
122 | - `get_margins` - Retrieves account margins
123 | - `place_order` - Places a trading order
124 | - `get_quote` - Gets quotes for specified symbols
125 | - `get_historical_data` - Retrieves historical price data
126 |
127 | ### Mutual Funds
128 | - `get_mf_orders` - Retrieves mutual fund orders
129 | - `place_mf_order` - Places a mutual fund order
130 | - `cancel_mf_order` - Cancels a mutual fund order
131 | - `get_mf_instruments` - Gets available mutual fund instruments
132 | - `get_mf_holdings` - Retrieves mutual fund holdings
133 | - `get_mf_sips` - Gets active SIPs
134 | - `place_mf_sip` - Creates a new SIP
135 | - `modify_mf_sip` - Modifies an existing SIP
136 | - `cancel_mf_sip` - Cancels a SIP
137 |
138 | ## Troubleshooting
139 |
140 | - If you encounter authentication issues, try removing the `.tokens` file and restart the authentication process
141 | - Make sure your Zerodha credentials in the `.env` file are correct
142 | - Ensure port 5000 is not being used by another application
143 | - For persistent issues, check Zerodha's API status at [status.zerodha.com](https://status.zerodha.com)
144 |
145 | ## Security Notes
146 |
147 | - Your Zerodha API credentials are stored only in your local `.env` file
148 | - Access tokens are stored in the `.tokens` file in the project directory
149 | - No credentials are transmitted to Claude or any third parties
150 | - All authentication happens directly between you and Zerodha's servers
151 |
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "zerodha-mcp"
3 | version = "0.1.0"
4 | description = "Add your description here"
5 | readme = "README.md"
6 | requires-python = ">=3.13"
7 | dependencies = [
8 | "fastapi>=0.115.11",
9 | "httpx>=0.28.1",
10 | "kiteconnect>=5.0.1",
11 | "mcp[cli]>=1.3.0",
12 | "python-dotenv>=1.0.1",
13 | "uvicorn>=0.34.0",
14 | ]
15 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
2 | FROM python:3.11-slim
3 | WORKDIR /app
4 |
5 | # Install system dependencies
6 | RUN apt-get update && apt-get install -y --no-install-recommends \
7 | build-essential \
8 | && rm -rf /var/lib/apt/lists/*
9 |
10 | # Copy project files
11 | COPY . /app
12 |
13 | # Install Python dependencies while ignoring the requires-python check
14 | RUN pip install --no-cache-dir --upgrade pip \
15 | && pip install --no-cache-dir --ignore-requires-python .
16 |
17 | # Expose port for FastAPI callback
18 | EXPOSE 5000
19 |
20 | # Default command
21 | CMD ["python", "main.py"]
22 |
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | required:
9 | - kiteApiKey
10 | - kiteApiSecret
11 | properties:
12 | kiteApiKey:
13 | type: string
14 | description: Zerodha Kite API Key
15 | kiteApiSecret:
16 | type: string
17 | description: Zerodha Kite API Secret
18 | commandFunction:
19 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
20 | |-
21 | (config) => ({
22 | command: 'python',
23 | args: ['main.py'],
24 | env: {
25 | KITE_API_KEY: config.kiteApiKey,
26 | KITE_API_SECRET: config.kiteApiSecret
27 | }
28 | })
29 | exampleConfig:
30 | kiteApiKey: your_kite_api_key_here
31 | kiteApiSecret: your_kite_api_secret_here
32 |
```
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
```python
1 | from typing import Any, Dict, List, Optional, AsyncIterator
2 | import os
3 | import httpx
4 | from contextlib import asynccontextmanager
5 | from dataclasses import dataclass
6 | from threading import Thread
7 | import webbrowser
8 | import uvicorn
9 | from fastapi import FastAPI, HTTPException
10 | from fastapi.responses import HTMLResponse
11 | from mcp.server.fastmcp import FastMCP, Context
12 | from kiteconnect import KiteConnect
13 | from dotenv import load_dotenv
14 |
15 | # Load environment variables from .env file
16 | load_dotenv()
17 |
18 | # Constants
19 | KITE_API_KEY = os.getenv("KITE_API_KEY")
20 | KITE_API_SECRET = os.getenv("KITE_API_SECRET")
21 | REDIRECT_URL = "http://127.0.0.1:5000/zerodha/auth/redirect"
22 | TOKEN_STORE_PATH = os.path.join(os.path.dirname(__file__), ".tokens")
23 |
24 | # Initialize FastAPI app for handling redirect
25 | app = FastAPI(title="Zerodha Login Handler")
26 |
27 | # Global variables for auth flow
28 | _request_token: Optional[str] = None
29 |
30 |
31 | @dataclass
32 | class ZerodhaContext:
33 | """Typed context for the Zerodha MCP server"""
34 |
35 | kite: KiteConnect
36 | api_key: str
37 | api_secret: str
38 | app: FastAPI
39 | server_thread: Optional[Thread] = None
40 |
41 |
42 | def load_stored_token() -> Optional[str]:
43 | """Load stored access token if it exists"""
44 | try:
45 | if os.path.exists(TOKEN_STORE_PATH):
46 | with open(TOKEN_STORE_PATH, "r") as f:
47 | return f.read().strip()
48 | except Exception:
49 | return None
50 | return None
51 |
52 |
53 | def save_access_token(token: str):
54 | """Save access token to file"""
55 | try:
56 | with open(TOKEN_STORE_PATH, "w") as f:
57 | f.write(token)
58 | except Exception as e:
59 | print(f"Warning: Could not save access token: {e}")
60 |
61 |
62 | def start_server():
63 | """Start the FastAPI server"""
64 | print("Starting FastAPI server on http://127.0.0.1:5000")
65 | uvicorn.run(app, host="127.0.0.1", port=5000, log_level="error")
66 |
67 |
68 | @asynccontextmanager
69 | async def zerodha_lifespan(server: FastMCP) -> AsyncIterator[ZerodhaContext]:
70 | """Manage application lifecycle for Zerodha integration"""
71 | # Initialize Kite Connect
72 | print("Initializing Zerodha context...")
73 |
74 | if not KITE_API_KEY or not KITE_API_SECRET:
75 | raise ValueError(
76 | "KITE_API_KEY and KITE_API_SECRET must be set in the .env file"
77 | )
78 |
79 | kite = KiteConnect(api_key=KITE_API_KEY)
80 |
81 | # Try to load existing token
82 | stored_token = load_stored_token()
83 | if stored_token:
84 | try:
85 | kite.set_access_token(stored_token)
86 | # Verify token is still valid with a simple API call
87 | kite.margins()
88 | print("Successfully restored previous session")
89 | except Exception:
90 | print("Stored token is invalid, will wait for new login...")
91 | if os.path.exists(TOKEN_STORE_PATH):
92 | os.remove(TOKEN_STORE_PATH)
93 |
94 | # Create context
95 | ctx = ZerodhaContext(
96 | kite=kite,
97 | api_key=KITE_API_KEY,
98 | api_secret=KITE_API_SECRET,
99 | app=app,
100 | )
101 |
102 | try:
103 | # Setup FastAPI endpoint for auth callback
104 | @app.get("/zerodha/auth/redirect")
105 | async def callback(request_token: str = None, status: str = None):
106 | """Handle the redirect from Zerodha login"""
107 | global _request_token
108 |
109 | if status != "success":
110 | print(f"Login failed with status: {status}")
111 | raise HTTPException(
112 | status_code=400, detail=f"Login failed with status: {status}"
113 | )
114 | if not request_token:
115 | print("No request token received")
116 | raise HTTPException(status_code=400, detail="No request token received")
117 |
118 | try:
119 | # Generate session
120 | print("Generating session with request token")
121 | data = ctx.kite.generate_session(
122 | request_token, api_secret=ctx.api_secret
123 | )
124 | access_token = data["access_token"]
125 |
126 | # Save and set the access token
127 | print("Saving and setting access token")
128 | save_access_token(access_token)
129 | ctx.kite.set_access_token(access_token)
130 | _request_token = request_token
131 | print("Login successful")
132 |
133 | return HTMLResponse(
134 | content="""
135 | <html>
136 | <body style="font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f5f5f5;">
137 | <div style="text-align: center; padding: 2rem; background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
138 | <h1 style="color: #2ecc71;">Login Successful!</h1>
139 | <p>You can close this window now.</p>
140 | </div>
141 | </body>
142 | </html>
143 | """
144 | )
145 | except Exception as e:
146 | error_msg = f"Failed to generate session: {str(e)}"
147 | print(error_msg)
148 | raise HTTPException(status_code=500, detail=error_msg)
149 |
150 | # Yield the context to the tools
151 | yield ctx
152 | finally:
153 | # Cleanup on shutdown
154 | print("Shutting down Zerodha context...")
155 | # Additional cleanup could go here if needed
156 |
157 |
158 | # Initialize FastMCP server with lifespan and dependencies
159 | mcp = FastMCP(
160 | "zerodha",
161 | lifespan=zerodha_lifespan,
162 | dependencies=["kiteconnect", "fastapi", "uvicorn", "python-dotenv", "httpx"],
163 | )
164 |
165 |
166 | @mcp.tool()
167 | def initiate_login(ctx: Context) -> Dict[str, Any]:
168 | """
169 | Start the Zerodha login flow by opening the login URL in a browser
170 | and starting a local server to handle the redirect
171 | """
172 | try:
173 | # Reset the request token
174 | global _request_token
175 | _request_token = None
176 | print("Initiating Zerodha login flow")
177 |
178 | # Get strongly typed context
179 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
180 |
181 | # Start the local server in a separate thread if not already running
182 | if not zerodha_ctx.server_thread or not zerodha_ctx.server_thread.is_alive():
183 | server_thread = Thread(target=start_server)
184 | server_thread.daemon = True
185 | server_thread.start()
186 | zerodha_ctx.server_thread = server_thread
187 |
188 | # Get the login URL
189 | login_url = zerodha_ctx.kite.login_url()
190 | print(f"Generated login URL: {login_url}")
191 |
192 | # Open the login URL in browser
193 | webbrowser.open(login_url)
194 | print("Opened login URL in browser")
195 |
196 | return {
197 | "message": "Login page opened in browser. Please complete the login process."
198 | }
199 | except Exception as e:
200 | error_msg = f"Error initiating login: {str(e)}"
201 | print(error_msg)
202 | return {"error": error_msg}
203 |
204 |
205 | @mcp.tool()
206 | def get_request_token(ctx: Context) -> Dict[str, Any]:
207 | """Get the current request token after login redirect"""
208 | if _request_token:
209 | return {"request_token": _request_token}
210 | return {
211 | "error": "No request token available. Please complete the login process first."
212 | }
213 |
214 |
215 | @mcp.tool()
216 | def get_holdings(ctx: Context) -> List[Dict[str, Any]]:
217 | """Get user's holdings/portfolio"""
218 | try:
219 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
220 | return zerodha_ctx.kite.holdings()
221 | except Exception as e:
222 | return {"error": str(e)}
223 |
224 |
225 | @mcp.tool()
226 | def get_positions(ctx: Context) -> Dict[str, Any]:
227 | """Get user's positions"""
228 | try:
229 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
230 | return zerodha_ctx.kite.positions()
231 | except Exception as e:
232 | return {"error": str(e)}
233 |
234 |
235 | @mcp.tool()
236 | def get_margins(ctx: Context) -> Dict[str, Any]:
237 | """Get account margins"""
238 | try:
239 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
240 | return zerodha_ctx.kite.margins()
241 | except Exception as e:
242 | return {"error": str(e)}
243 |
244 |
245 | @mcp.tool()
246 | def place_order(
247 | ctx: Context,
248 | tradingsymbol: str,
249 | exchange: str,
250 | transaction_type: str,
251 | quantity: int,
252 | product: str,
253 | order_type: str,
254 | price: Optional[float] = None,
255 | trigger_price: Optional[float] = None,
256 | ) -> Dict[str, Any]:
257 | """
258 | Place an order on Zerodha
259 |
260 | Args:
261 | tradingsymbol: Trading symbol (e.g., 'INFY')
262 | exchange: Exchange (NSE, BSE, NFO, etc.)
263 | transaction_type: BUY or SELL
264 | quantity: Number of shares/units
265 | product: Product code (CNC, MIS, NRML)
266 | order_type: Order type (MARKET, LIMIT, SL, SL-M)
267 | price: Price for LIMIT orders
268 | trigger_price: Trigger price for SL orders
269 | """
270 | try:
271 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
272 | return zerodha_ctx.kite.place_order(
273 | variety="regular",
274 | exchange=exchange,
275 | tradingsymbol=tradingsymbol,
276 | transaction_type=transaction_type,
277 | quantity=quantity,
278 | product=product,
279 | order_type=order_type,
280 | price=price,
281 | trigger_price=trigger_price,
282 | )
283 | except Exception as e:
284 | return {"error": str(e)}
285 |
286 |
287 | @mcp.tool()
288 | def get_quote(ctx: Context, symbols: List[str]) -> Dict[str, Any]:
289 | """
290 | Get quote for symbols
291 |
292 | Args:
293 | symbols: List of symbols (e.g., ['NSE:INFY', 'BSE:RELIANCE'])
294 | """
295 | try:
296 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
297 | return zerodha_ctx.kite.quote(symbols)
298 | except Exception as e:
299 | return {"error": str(e)}
300 |
301 |
302 | @mcp.tool()
303 | def get_historical_data(
304 | ctx: Context, instrument_token: int, from_date: str, to_date: str, interval: str
305 | ) -> List[Dict[str, Any]]:
306 | """
307 | Get historical data for an instrument
308 |
309 | Args:
310 | instrument_token: Instrument token
311 | from_date: From date (format: 2024-01-01)
312 | to_date: To date (format: 2024-03-13)
313 | interval: Candle interval (minute, day, 3minute, etc.)
314 | """
315 | try:
316 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
317 | return zerodha_ctx.kite.historical_data(
318 | instrument_token=instrument_token,
319 | from_date=from_date,
320 | to_date=to_date,
321 | interval=interval,
322 | )
323 | except Exception as e:
324 | return {"error": str(e)}
325 |
326 |
327 | @mcp.tool()
328 | def check_and_authenticate(ctx: Context) -> Dict[str, Any]:
329 | """
330 | Check if Kite is authenticated and initiate authentication if needed.
331 | Returns the authentication status and any relevant messages.
332 | """
333 | try:
334 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
335 |
336 | # First try to load existing token
337 | stored_token = load_stored_token()
338 | if stored_token:
339 | try:
340 | zerodha_ctx.kite.set_access_token(stored_token)
341 | # Verify token is still valid with a simple API call
342 | zerodha_ctx.kite.margins()
343 | return {
344 | "status": "authenticated",
345 | "message": "Already authenticated with valid token",
346 | }
347 | except Exception:
348 | print("Stored token is invalid, will initiate new login...")
349 | if os.path.exists(TOKEN_STORE_PATH):
350 | os.remove(TOKEN_STORE_PATH)
351 |
352 | # If we reach here, we need to authenticate
353 | # Call the existing initiate_login function
354 | login_result = initiate_login(ctx)
355 |
356 | if "error" in login_result:
357 | return {"status": "error", "message": login_result["error"]}
358 |
359 | return {"status": "login_initiated", "message": login_result["message"]}
360 |
361 | except Exception as e:
362 | error_msg = f"Error checking/initiating authentication: {str(e)}"
363 | print(error_msg)
364 | return {"status": "error", "message": error_msg}
365 |
366 |
367 | # Mutual Fund Tools
368 |
369 |
370 | @mcp.tool()
371 | def get_mf_orders(ctx: Context) -> List[Dict[str, Any]]:
372 | """Get all mutual fund orders"""
373 | try:
374 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
375 | return zerodha_ctx.kite.mf_orders()
376 | except Exception as e:
377 | return {"error": str(e)}
378 |
379 |
380 | @mcp.tool()
381 | def place_mf_order(
382 | ctx: Context,
383 | tradingsymbol: str,
384 | transaction_type: str,
385 | amount: float,
386 | tag: Optional[str] = None,
387 | ) -> Dict[str, Any]:
388 | """
389 | Place a mutual fund order
390 |
391 | Args:
392 | tradingsymbol: Trading symbol (e.g., 'INF090I01239')
393 | transaction_type: BUY or SELL
394 | amount: Amount to invest or redeem
395 | tag: Optional tag for the order
396 | """
397 | try:
398 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
399 | return zerodha_ctx.kite.place_mf_order(
400 | tradingsymbol=tradingsymbol,
401 | transaction_type=transaction_type,
402 | amount=amount,
403 | tag=tag,
404 | )
405 | except Exception as e:
406 | return {"error": str(e)}
407 |
408 |
409 | @mcp.tool()
410 | def cancel_mf_order(ctx: Context, order_id: str) -> Dict[str, Any]:
411 | """
412 | Cancel a mutual fund order
413 |
414 | Args:
415 | order_id: Order ID to cancel
416 | """
417 | try:
418 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
419 | return zerodha_ctx.kite.cancel_mf_order(order_id=order_id)
420 | except Exception as e:
421 | return {"error": str(e)}
422 |
423 |
424 | @mcp.tool()
425 | def get_mf_instruments(ctx: Context) -> List[Dict[str, Any]]:
426 | """Get all available mutual fund instruments"""
427 | try:
428 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
429 | return zerodha_ctx.kite.mf_instruments()
430 | except Exception as e:
431 | return {"error": str(e)}
432 |
433 |
434 | @mcp.tool()
435 | def get_mf_holdings(ctx: Context) -> List[Dict[str, Any]]:
436 | """Get user's mutual fund holdings"""
437 | try:
438 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
439 | return zerodha_ctx.kite.mf_holdings()
440 | except Exception as e:
441 | return {"error": str(e)}
442 |
443 |
444 | @mcp.tool()
445 | def get_mf_sips(ctx: Context) -> List[Dict[str, Any]]:
446 | """Get all mutual fund SIPs"""
447 | try:
448 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
449 | return zerodha_ctx.kite.mf_sips()
450 | except Exception as e:
451 | return {"error": str(e)}
452 |
453 |
454 | @mcp.tool()
455 | def place_mf_sip(
456 | ctx: Context,
457 | tradingsymbol: str,
458 | amount: float,
459 | instalments: int,
460 | frequency: str,
461 | initial_amount: Optional[float] = None,
462 | instalment_day: Optional[int] = None,
463 | tag: Optional[str] = None,
464 | ) -> Dict[str, Any]:
465 | """
466 | Place a mutual fund SIP (Systematic Investment Plan)
467 |
468 | Args:
469 | tradingsymbol: Trading symbol (e.g., 'INF090I01239')
470 | amount: Amount per instalment
471 | instalments: Number of instalments (minimum 6)
472 | frequency: weekly, monthly, or quarterly
473 | initial_amount: Optional initial amount
474 | instalment_day: Optional day of month/week for instalment (1-31 for monthly, 1-7 for weekly)
475 | tag: Optional tag for the SIP
476 | """
477 | try:
478 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
479 | return zerodha_ctx.kite.place_mf_sip(
480 | tradingsymbol=tradingsymbol,
481 | amount=amount,
482 | instalments=instalments,
483 | frequency=frequency,
484 | initial_amount=initial_amount,
485 | instalment_day=instalment_day,
486 | tag=tag,
487 | )
488 | except Exception as e:
489 | return {"error": str(e)}
490 |
491 |
492 | @mcp.tool()
493 | def modify_mf_sip(
494 | ctx: Context,
495 | sip_id: str,
496 | amount: Optional[float] = None,
497 | frequency: Optional[str] = None,
498 | instalments: Optional[int] = None,
499 | instalment_day: Optional[int] = None,
500 | status: Optional[str] = None,
501 | ) -> Dict[str, Any]:
502 | """
503 | Modify a mutual fund SIP
504 |
505 | Args:
506 | sip_id: SIP ID to modify
507 | amount: New amount per instalment
508 | frequency: New frequency (weekly, monthly, or quarterly)
509 | instalments: New number of instalments
510 | instalment_day: New day of month/week for instalment
511 | status: SIP status (active or paused)
512 | """
513 | try:
514 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
515 | return zerodha_ctx.kite.modify_mf_sip(
516 | sip_id=sip_id,
517 | amount=amount,
518 | frequency=frequency,
519 | instalments=instalments,
520 | instalment_day=instalment_day,
521 | status=status,
522 | )
523 | except Exception as e:
524 | return {"error": str(e)}
525 |
526 |
527 | @mcp.tool()
528 | def cancel_mf_sip(ctx: Context, sip_id: str) -> Dict[str, Any]:
529 | """
530 | Cancel a mutual fund SIP
531 |
532 | Args:
533 | sip_id: SIP ID to cancel
534 | """
535 | try:
536 | zerodha_ctx: ZerodhaContext = ctx.request_context.lifespan_context
537 | return zerodha_ctx.kite.cancel_mf_sip(sip_id=sip_id)
538 | except Exception as e:
539 | return {"error": str(e)}
540 |
541 |
542 | if __name__ == "__main__":
543 | # We don't need the main function anymore since MCP handles the lifecycle
544 | print("Starting Zerodha MCP server...")
545 | mcp.run()
546 |
```