Adding MCP Tools
MCP tools are defined in skopaq/mcp_server.py using the @mcp.tool() decorator from FastMCP. Each tool is an async function that returns a JSON string.
Anatomy of a Tool
@mcp.tool()
async def my_tool(symbol: str, days: int = 5) -> str:
"""Short description shown to the AI.
Longer description with details about what the tool does,
when to use it, and what it returns.
Args:
symbol: Stock symbol (e.g. RELIANCE, TCS).
days: Number of days of history (default 5).
"""
config = _get_config()
# ... implementation ...
return json.dumps({
"symbol": symbol,
"result": "...",
})
Key rules:
- Async function -- All tools must be
async def - Returns
str-- Always returnjson.dumps(...), never raw dicts - Docstring is the description -- The AI reads this to decide when to use the tool
- Type hints on all args -- FastMCP generates the tool schema from type hints
- Default values -- Provide sensible defaults for optional parameters
Step-by-Step: Adding a New Tool
Step 1: Define the Function
Add your tool in the appropriate section of skopaq/mcp_server.py:
# ── My New Section ──────────────────────────────────────────────────────────
@mcp.tool()
async def get_option_greeks(
symbol: str = "NIFTY",
strike: float = 0,
option_type: str = "CE",
) -> str:
"""Calculate option Greeks (delta, gamma, theta, vega) for a specific contract.
Args:
symbol: Underlying symbol (NIFTY, BANKNIFTY, or stock).
strike: Strike price.
option_type: CE (call) or PE (put).
"""
kite = _get_kite()
if not kite:
return json.dumps({"error": "Kite not connected"})
try:
# Your implementation here
greeks = calculate_greeks(symbol, strike, option_type)
return json.dumps({
"symbol": symbol,
"strike": strike,
"type": option_type,
"delta": greeks.delta,
"gamma": greeks.gamma,
"theta": greeks.theta,
"vega": greeks.vega,
})
except Exception as exc:
logger.exception("Greeks calculation failed")
return json.dumps({"error": str(exc)})
Step 2: Handle Errors Gracefully
Always catch exceptions and return structured error JSON:
try:
result = await some_operation()
return json.dumps({"success": True, "data": result})
except Exception as exc:
logger.exception("Operation failed")
return json.dumps({"error": str(exc)})
This ensures the AI always gets a parseable response.
Step 3: Use Lazy Infrastructure
Access shared infrastructure via the lazy helpers:
config = _get_config() # SkopaqConfig (cached)
router = _get_router() # OrderRouter + PaperEngine (cached)
kite = _get_kite() # KiteClient or None
Do not import and instantiate these at module level -- it would slow down server startup.
Step 4: Write Tests
Add a test in tests/unit/ that mocks external dependencies:
# tests/unit/test_mcp_greeks.py
import pytest
from unittest.mock import AsyncMock, patch
@pytest.mark.asyncio
@patch("skopaq.mcp_server._get_kite")
async def test_get_option_greeks(mock_kite):
from skopaq.mcp_server import get_option_greeks
mock_kite.return_value = AsyncMock()
# ... mock the calculation
result = await get_option_greeks("NIFTY", 24000, "CE")
data = json.loads(result)
assert "delta" in data
Step 5: Update Tests and Permissions
After adding your tool:
- Update
tests/unit/chat/test_mcp_server.py-- add tool name to the expected set - Add to
.claude/settings.jsonpermissions if it should be auto-allowed - Run tests:
python3 -m pytest tests/unit/ -x -q
Step 6: Add to Skills (Optional)
If your tool should be available via a slash command, add it to an existing skill's allowed-tools or create a new skill:
# .claude/skills/greeks/SKILL.md
---
name: greeks
description: Calculate option Greeks for a contract
allowed-tools: mcp__skopaq__get_option_greeks mcp__skopaq__get_option_chain
---
Design Guidelines
Docstring Quality
The docstring is critical -- it is the only thing the AI sees when deciding which tool to use:
# Good: specific, mentions when to use it
"""Calculate implied volatility for an option contract.
Use this to compare IV across strikes and identify overpriced/underpriced options.
Returns IV as a percentage along with the historical IV rank.
Args:
symbol: Underlying symbol.
"""
# Bad: vague, no context
"""Get some option data."""
Return Structure
Always return a flat JSON object with clear field names:
# Good
return json.dumps({
"symbol": "NIFTY",
"iv": 15.3,
"iv_rank": 45,
"iv_percentile": 62,
})
# Bad: nested, unclear
return json.dumps({
"data": {"s": "NIFTY", "vals": [15.3, 45, 62]}
})
Kite-Dependent Tools
If your tool requires Kite Connect, check for it and return a clear error:
Size Limits
Truncate large responses to avoid overwhelming the AI's context window:
# Limit text fields
return json.dumps({
"news": news_text[:3000], # Cap at 3000 chars
"candles": candles[-20:], # Last 20 only
})
Tool Naming Conventions
| Pattern | Example | When |
|---|---|---|
get_* |
get_quote, get_funds |
Read-only data retrieval |
place_* |
place_order, place_gtt_order |
Actions that create something |
list_* |
list_gtt_orders |
List collections |
gather_* |
gather_market_data |
Fetch raw data for analysis |
check_* |
check_safety |
Validation tools |
setup_* |
setup_swing_trade |
Multi-step workflows |
suggest_* |
suggest_option_trade |
AI recommendations |
File Reference
| File | Purpose |
|---|---|
skopaq/mcp_server.py |
All tool definitions (add your tool here) |
skopaq/config.py |
Configuration (if your tool needs new config) |
.claude/.mcp.json |
MCP server registration for Claude Code |
.claude/skills/*/SKILL.md |
Skill files that reference tools |