# Directory Structure
```
├── .env.example
├── .gitignore
├── alpaca_mcp_server.py
├── LICENSE
├── README.md
└── requirements.txt
```
# Files
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | # Alpaca API credentials
2 | # Get these from https://app.alpaca.markets/paper/dashboard/overview
3 | API_KEY_ID=your_alpaca_api_key_here
4 | API_SECRET_KEY=your_alpaca_secret_key_here
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Environment and secrets
2 | .env
3 | .env.local
4 | *.secret
5 |
6 | # Python
7 | __pycache__/
8 | *.py[cod]
9 | *$py.class
10 | *.so
11 | .Python
12 | env/
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # Virtual environments
29 | venv/
30 | ENV/
31 | .venv/
32 |
33 | # IDEs and editors
34 | .idea/
35 | .vscode/
36 | *.swp
37 | *.swo
38 | .DS_Store
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Alpaca MCP Server
2 |
3 | This is a Model Context Protocol (MCP) server for Alpaca, allowing LLMs like Claude to interact with the Alpaca trading API. It enables trading stocks, checking positions, fetching market data, and managing your account - all through natural language.
4 |
5 | ## Features
6 |
7 | - 📊 **Market Data** - Get real-time stock quotes and historical price data
8 | - 💵 **Account Information** - Check your balances, buying power, and status
9 | - 📈 **Position Management** - View current positions and their performance
10 | - 🛒 **Order Placement** - Place market and limit orders through natural language
11 | - 📋 **Order Management** - List, track, and cancel orders
12 |
13 | ## Prerequisites
14 |
15 | - Python 3.10+
16 | - Alpaca API keys
17 | - Claude for Desktop or another MCP client
18 |
19 | ## Installation
20 |
21 | 1. Clone this repository:
22 | ```bash
23 | git clone https://github.com/YOUR_USERNAME/alpaca-mcp.git
24 | cd alpaca-mcp
25 | ```
26 |
27 | 2. Install the required packages:
28 | ```bash
29 | pip install mcp alpaca-py python-dotenv
30 | ```
31 |
32 | 3. Create a `.env` file with your Alpaca API credentials:
33 | ```
34 | API_KEY_ID=your_alpaca_api_key
35 | API_SECRET_KEY=your_alpaca_secret_key
36 | ```
37 |
38 | ## Usage
39 |
40 | ### Running the server
41 |
42 | Start the server by running:
43 |
44 | ```bash
45 | python alpaca_mcp_server.py
46 | ```
47 |
48 | ### Configuring Claude for Desktop
49 |
50 | 1. Open Claude for Desktop
51 | 2. Go to Settings
52 | 3. Click on "Developer" and then "Edit Config"
53 | 4. Add the server configuration to `claude_desktop_config.json`:
54 |
55 | ```json
56 | {
57 | "mcpServers": {
58 | "alpaca": {
59 | "command": "python",
60 | "args": [
61 | "/path/to/alpaca_mcp_server.py"
62 | ],
63 | "env": {
64 | "API_KEY_ID": "your_alpaca_api_key",
65 | "API_SECRET_KEY": "your_alpaca_secret_key"
66 | }
67 | }
68 | }
69 | }
70 | ```
71 |
72 | 5. Save and restart Claude for Desktop
73 |
74 | ### Available Tools
75 |
76 | The server exposes the following tools:
77 |
78 | - `get_account_info()` - Get account balances and status
79 | - `get_positions()` - List all current positions in the portfolio
80 | - `get_stock_quote(symbol)` - Get the latest quote for a stock
81 | - `get_stock_bars(symbol, days)` - Get historical price bars for a stock
82 | - `get_orders(status, limit)` - List orders with specified status
83 | - `place_market_order(symbol, side, quantity)` - Place a market order
84 | - `place_limit_order(symbol, side, quantity, limit_price)` - Place a limit order
85 | - `cancel_all_orders()` - Cancel all open orders
86 | - `close_all_positions(cancel_orders)` - Close all open positions
87 |
88 | ## Example Queries
89 |
90 | Once the server is connected to Claude, you can ask questions like:
91 |
92 | - "What's my current account balance and buying power?"
93 | - "Show me my current positions"
94 | - "Get the latest quote for AAPL"
95 | - "Show me the price history for TSLA over the last 10 days"
96 | - "Buy 5 shares of MSFT at market price"
97 | - "Sell 10 shares of AMZN with a limit price of $130"
98 | - "Cancel all my open orders"
99 |
100 | ## Note
101 |
102 | This server uses Alpaca's paper trading by default. To use real money trading, change `paper=True` to `paper=False` in the `TradingClient` initialization.
103 |
104 | ## Security Notice
105 |
106 | This MCP server will have access to your Alpaca account and can place real trades. Always review what Claude is suggesting before approving any trades.
107 |
108 | ## License
109 |
110 | MIT
```
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
```
1 | mcp>=1.2.0
2 | alpaca-py
3 | python-dotenv
```
--------------------------------------------------------------------------------
/alpaca_mcp_server.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 | from typing import Dict, Any, List, Optional
3 | from datetime import datetime, timedelta
4 | from mcp.server.fastmcp import FastMCP
5 | from alpaca.trading.client import TradingClient
6 | from alpaca.trading.requests import GetOrdersRequest, MarketOrderRequest, LimitOrderRequest
7 | from alpaca.trading.enums import OrderSide, TimeInForce, QueryOrderStatus
8 | from alpaca.data.historical import StockHistoricalDataClient
9 | from alpaca.data.requests import StockBarsRequest, StockLatestQuoteRequest
10 | from alpaca.data.timeframe import TimeFrame
11 |
12 | # Initialize FastMCP server
13 | mcp = FastMCP("alpaca-trading")
14 |
15 | # Initialize Alpaca clients using environment variables
16 | API_KEY = os.getenv("API_KEY_ID")
17 | API_SECRET = os.getenv("API_SECRET_KEY")
18 |
19 | # Check if keys are available
20 | if not API_KEY or not API_SECRET:
21 | raise ValueError("Alpaca API credentials not found in environment variables.")
22 |
23 | # Initialize trading and data clients
24 | trading_client = TradingClient(API_KEY, API_SECRET, paper=True)
25 | stock_client = StockHistoricalDataClient(API_KEY, API_SECRET)
26 |
27 | # Account information tools
28 | @mcp.tool()
29 | async def get_account_info() -> str:
30 | """Get the current account information including balances and status."""
31 | account = trading_client.get_account()
32 |
33 | info = f"""
34 | Account Information:
35 | -------------------
36 | Account ID: {account.id}
37 | Status: {account.status}
38 | Currency: {account.currency}
39 | Buying Power: ${float(account.buying_power):.2f}
40 | Cash: ${float(account.cash):.2f}
41 | Portfolio Value: ${float(account.portfolio_value):.2f}
42 | Equity: ${float(account.equity):.2f}
43 | Long Market Value: ${float(account.long_market_value):.2f}
44 | Short Market Value: ${float(account.short_market_value):.2f}
45 | Pattern Day Trader: {'Yes' if account.pattern_day_trader else 'No'}
46 | Day Trades Remaining: {account.daytrade_count if hasattr(account, 'daytrade_count') else 'Unknown'}
47 | """
48 | return info
49 |
50 | @mcp.tool()
51 | async def get_positions() -> str:
52 | """Get all current positions in the portfolio."""
53 | positions = trading_client.get_all_positions()
54 |
55 | if not positions:
56 | return "No open positions found."
57 |
58 | result = "Current Positions:\n-------------------\n"
59 | for position in positions:
60 | result += f"""
61 | Symbol: {position.symbol}
62 | Quantity: {position.qty} shares
63 | Market Value: ${float(position.market_value):.2f}
64 | Average Entry Price: ${float(position.avg_entry_price):.2f}
65 | Current Price: ${float(position.current_price):.2f}
66 | Unrealized P/L: ${float(position.unrealized_pl):.2f} ({float(position.unrealized_plpc) * 100:.2f}%)
67 | -------------------
68 | """
69 | return result
70 |
71 | # Market data tools
72 | @mcp.tool()
73 | async def get_stock_quote(symbol: str) -> str:
74 | """
75 | Get the latest quote for a stock.
76 |
77 | Args:
78 | symbol: Stock ticker symbol (e.g., AAPL, MSFT)
79 | """
80 | try:
81 | request_params = StockLatestQuoteRequest(symbol_or_symbols=symbol)
82 | quotes = stock_client.get_stock_latest_quote(request_params)
83 |
84 | if symbol in quotes:
85 | quote = quotes[symbol]
86 | return f"""
87 | Latest Quote for {symbol}:
88 | ------------------------
89 | Ask Price: ${quote.ask_price:.2f}
90 | Bid Price: ${quote.bid_price:.2f}
91 | Ask Size: {quote.ask_size}
92 | Bid Size: {quote.bid_size}
93 | Timestamp: {quote.timestamp}
94 | """
95 | else:
96 | return f"No quote data found for {symbol}."
97 | except Exception as e:
98 | return f"Error fetching quote for {symbol}: {str(e)}"
99 |
100 | @mcp.tool()
101 | async def get_stock_bars(symbol: str, days: int = 5) -> str:
102 | """
103 | Get historical price bars for a stock.
104 |
105 | Args:
106 | symbol: Stock ticker symbol (e.g., AAPL, MSFT)
107 | days: Number of trading days to look back (default: 5)
108 | """
109 | try:
110 | # Calculate start time based on days
111 | start_time = datetime.now() - timedelta(days=days)
112 |
113 | request_params = StockBarsRequest(
114 | symbol_or_symbols=symbol,
115 | timeframe=TimeFrame.Day,
116 | start=start_time
117 | )
118 |
119 | bars = stock_client.get_stock_bars(request_params)
120 |
121 | if symbol in bars and bars[symbol]:
122 | result = f"Historical Data for {symbol} (Last {days} trading days):\n"
123 | result += "---------------------------------------------------\n"
124 |
125 | for bar in bars[symbol]:
126 | result += f"Date: {bar.timestamp.date()}, Open: ${bar.open:.2f}, High: ${bar.high:.2f}, Low: ${bar.low:.2f}, Close: ${bar.close:.2f}, Volume: {bar.volume}\n"
127 |
128 | return result
129 | else:
130 | return f"No historical data found for {symbol} in the last {days} days."
131 | except Exception as e:
132 | return f"Error fetching historical data for {symbol}: {str(e)}"
133 |
134 | # Order management tools
135 | @mcp.tool()
136 | async def get_orders(status: str = "all", limit: int = 10) -> str:
137 | """
138 | Get orders with the specified status.
139 |
140 | Args:
141 | status: Order status to filter by (open, closed, all)
142 | limit: Maximum number of orders to return (default: 10)
143 | """
144 | try:
145 | # Convert status string to enum
146 | if status.lower() == "open":
147 | query_status = QueryOrderStatus.OPEN
148 | elif status.lower() == "closed":
149 | query_status = QueryOrderStatus.CLOSED
150 | else:
151 | query_status = QueryOrderStatus.ALL
152 |
153 | request_params = GetOrdersRequest(
154 | status=query_status,
155 | limit=limit
156 | )
157 |
158 | orders = trading_client.get_orders(request_params)
159 |
160 | if not orders:
161 | return f"No {status} orders found."
162 |
163 | result = f"{status.capitalize()} Orders (Last {len(orders)}):\n"
164 | result += "-----------------------------------\n"
165 |
166 | for order in orders:
167 | result += f"""
168 | Symbol: {order.symbol}
169 | ID: {order.id}
170 | Type: {order.type}
171 | Side: {order.side}
172 | Quantity: {order.qty}
173 | Status: {order.status}
174 | Submitted At: {order.submitted_at}
175 | """
176 | if hasattr(order, 'filled_at') and order.filled_at:
177 | result += f"Filled At: {order.filled_at}\n"
178 |
179 | if hasattr(order, 'filled_avg_price') and order.filled_avg_price:
180 | result += f"Filled Price: ${float(order.filled_avg_price):.2f}\n"
181 |
182 | result += "-----------------------------------\n"
183 |
184 | return result
185 | except Exception as e:
186 | return f"Error fetching orders: {str(e)}"
187 |
188 | @mcp.tool()
189 | async def place_market_order(symbol: str, side: str, quantity: float) -> str:
190 | """
191 | Place a market order.
192 |
193 | Args:
194 | symbol: Stock ticker symbol (e.g., AAPL, MSFT)
195 | side: Order side (buy or sell)
196 | quantity: Number of shares to buy or sell
197 | """
198 | try:
199 | # Convert side string to enum
200 | if side.lower() == "buy":
201 | order_side = OrderSide.BUY
202 | elif side.lower() == "sell":
203 | order_side = OrderSide.SELL
204 | else:
205 | return f"Invalid order side: {side}. Must be 'buy' or 'sell'."
206 |
207 | # Create market order request
208 | order_data = MarketOrderRequest(
209 | symbol=symbol,
210 | qty=quantity,
211 | side=order_side,
212 | time_in_force=TimeInForce.DAY
213 | )
214 |
215 | # Submit order
216 | order = trading_client.submit_order(order_data)
217 |
218 | return f"""
219 | Market Order Placed Successfully:
220 | --------------------------------
221 | Order ID: {order.id}
222 | Symbol: {order.symbol}
223 | Side: {order.side}
224 | Quantity: {order.qty}
225 | Type: {order.type}
226 | Time In Force: {order.time_in_force}
227 | Status: {order.status}
228 | """
229 | except Exception as e:
230 | return f"Error placing market order: {str(e)}"
231 |
232 | @mcp.tool()
233 | async def place_limit_order(symbol: str, side: str, quantity: float, limit_price: float) -> str:
234 | """
235 | Place a limit order.
236 |
237 | Args:
238 | symbol: Stock ticker symbol (e.g., AAPL, MSFT)
239 | side: Order side (buy or sell)
240 | quantity: Number of shares to buy or sell
241 | limit_price: Limit price for the order
242 | """
243 | try:
244 | # Convert side string to enum
245 | if side.lower() == "buy":
246 | order_side = OrderSide.BUY
247 | elif side.lower() == "sell":
248 | order_side = OrderSide.SELL
249 | else:
250 | return f"Invalid order side: {side}. Must be 'buy' or 'sell'."
251 |
252 | # Create limit order request
253 | order_data = LimitOrderRequest(
254 | symbol=symbol,
255 | qty=quantity,
256 | side=order_side,
257 | time_in_force=TimeInForce.DAY,
258 | limit_price=limit_price
259 | )
260 |
261 | # Submit order
262 | order = trading_client.submit_order(order_data)
263 |
264 | return f"""
265 | Limit Order Placed Successfully:
266 | -------------------------------
267 | Order ID: {order.id}
268 | Symbol: {order.symbol}
269 | Side: {order.side}
270 | Quantity: {order.qty}
271 | Type: {order.type}
272 | Limit Price: ${float(order.limit_price):.2f}
273 | Time In Force: {order.time_in_force}
274 | Status: {order.status}
275 | """
276 | except Exception as e:
277 | return f"Error placing limit order: {str(e)}"
278 |
279 | @mcp.tool()
280 | async def cancel_all_orders() -> str:
281 | """Cancel all open orders."""
282 | try:
283 | cancel_statuses = trading_client.cancel_orders()
284 | return f"Successfully canceled all open orders. Status: {cancel_statuses}"
285 | except Exception as e:
286 | return f"Error canceling orders: {str(e)}"
287 |
288 | # Account management tools
289 | @mcp.tool()
290 | async def close_all_positions(cancel_orders: bool = True) -> str:
291 | """
292 | Close all open positions.
293 |
294 | Args:
295 | cancel_orders: Whether to cancel all open orders before closing positions (default: True)
296 | """
297 | try:
298 | trading_client.close_all_positions(cancel_orders=cancel_orders)
299 | return "Successfully closed all positions."
300 | except Exception as e:
301 | return f"Error closing positions: {str(e)}"
302 |
303 | # Run the server
304 | if __name__ == "__main__":
305 | mcp.run(transport='stdio')
```