#
tokens: 8247/50000 8/8 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![smithery badge](https://smithery.ai/badge/@aptro/zerodha-mcp)](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 | 
```