# Directory Structure
```
├── .env.example
├── .gitignore
├── .python-version
├── LICENSE
├── pyproject.toml
├── README.md
├── server.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
1 | 3.11
2 |
```
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | # Get your Financial Datasets API key from https://financialdatasets.ai/
2 | FINANCIAL_DATASETS_API_KEY=your-financial-datasets-api-key
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # UV
98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | #uv.lock
102 |
103 | # poetry
104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | # This is especially recommended for binary packages to ensure reproducibility, and is more
106 | # commonly ignored for libraries.
107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 |
110 | # pdm
111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | # in version control.
115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 |
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 |
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 |
127 | # SageMath parsed files
128 | *.sage.py
129 |
130 | # Environments
131 | .env
132 | .venv
133 | env/
134 | venv/
135 | ENV/
136 | env.bak/
137 | venv.bak/
138 |
139 | # Spyder project settings
140 | .spyderproject
141 | .spyproject
142 |
143 | # Rope project settings
144 | .ropeproject
145 |
146 | # mkdocs documentation
147 | /site
148 |
149 | # mypy
150 | .mypy_cache/
151 | .dmypy.json
152 | dmypy.json
153 |
154 | # Pyre type checker
155 | .pyre/
156 |
157 | # pytype static type analyzer
158 | .pytype/
159 |
160 | # Cython debug symbols
161 | cython_debug/
162 |
163 | # PyCharm
164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166 | # and can be added to the global gitignore or merged into this file. For a more nuclear
167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168 | #.idea/
169 |
170 | # PyPI configuration file
171 | .pypirc
172 |
173 | .DS_Store
174 |
175 | # Ignore vscode
176 | .vscode/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Financial Datasets MCP Server
2 |
3 | ## Introduction
4 |
5 | This is a Model Context Protocol (MCP) server that provides access to stock market data from [Financial Datasets](https://www.financialdatasets.ai/).
6 |
7 | It allows Claude and other AI assistants to retrieve income statements, balance sheets, cash flow statements, stock prices, and market news directly through the MCP interface.
8 |
9 | ## Available Tools
10 |
11 | This MCP server provides the following tools:
12 | - **get_income_statements**: Get income statements for a company.
13 | - **get_balance_sheets**: Get balance sheets for a company.
14 | - **get_cash_flow_statements**: Get cash flow statements for a company.
15 | - **get_current_stock_price**: Get the current / latest price of a company.
16 | - **get_historical_stock_prices**: Gets historical stock prices for a company.
17 | - **get_company_news**: Get news for a company.
18 | - **get_available_crypto_tickers**: Gets all available crypto tickers.
19 | - **get_crypto_prices**: Gets historical prices for a crypto currency.
20 | - **get_historical_crypto_prices**: Gets historical prices for a crypto currency.
21 | - **get_current_crypto_price**: Get the current / latest price of a crypto currency.
22 |
23 | ## Setup
24 |
25 | ### Prerequisites
26 |
27 | - Python 3.10 or higher
28 | - [uv](https://github.com/astral-sh/uv) package manager
29 |
30 | ### Installation
31 |
32 | 1. Clone this repository:
33 | ```bash
34 | git clone https://github.com/financial-datasets/mcp-server
35 | cd mcp-server
36 | ```
37 |
38 | 2. If you don't have uv installed, install it:
39 | ```bash
40 | # macOS/Linux
41 | curl -LsSf https://astral.sh/uv/install.sh | sh
42 |
43 | # Windows
44 | curl -LsSf https://astral.sh/uv/install.ps1 | powershell
45 | ```
46 |
47 | 3. Install dependencies:
48 | ```bash
49 | # Create virtual env and activate it
50 | uv venv
51 | source .venv/bin/activate # On Windows: .venv\Scripts\activate
52 |
53 | # Install dependencies
54 | uv add "mcp[cli]" httpx # On Windows: uv add mcp[cli] httpx
55 |
56 | ```
57 |
58 | 4. Set up environment variables:
59 | ```bash
60 | # Create .env file for your API keys
61 | cp .env.example .env
62 |
63 | # Set API key in .env
64 | FINANCIAL_DATASETS_API_KEY=your-financial-datasets-api-key
65 | ```
66 |
67 | 5. Run the server:
68 | ```bash
69 | uv run server.py
70 | ```
71 |
72 | ## Connecting to Claude Desktop
73 |
74 | 1. Install [Claude Desktop](https://claude.ai/desktop) if you haven't already
75 |
76 | 2. Create or edit the Claude Desktop configuration file:
77 | ```bash
78 | # macOS
79 | mkdir -p ~/Library/Application\ Support/Claude/
80 | nano ~/Library/Application\ Support/Claude/claude_desktop_config.json
81 | ```
82 |
83 | 3. Add the following configuration:
84 | ```json
85 | {
86 | "mcpServers": {
87 | "financial-datasets": {
88 | "command": "/path/to/uv",
89 | "args": [
90 | "--directory",
91 | "/absolute/path/to/financial-datasets-mcp",
92 | "run",
93 | "server.py"
94 | ]
95 | }
96 | }
97 | }
98 | ```
99 |
100 | Replace `/path/to/uv` with the result of `which uv` and `/absolute/path/to/financial-datasets-mcp` with the absolute path to this project.
101 |
102 | 4. Restart Claude Desktop
103 |
104 | 5. You should now see the financial tools available in Claude Desktop's tools menu (hammer icon)
105 |
106 | 6. Try asking Claude questions like:
107 | - "What are Apple's recent income statements?"
108 | - "Show me the current price of Tesla stock"
109 | - "Get historical prices for MSFT from 2024-01-01 to 2024-12-31"
110 |
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "mcp-server"
3 | version = "0.1.0"
4 | description = "Add your description here"
5 | readme = "README.md"
6 | requires-python = ">=3.11"
7 | dependencies = [
8 | "httpx>=0.28.1",
9 | "mcp[cli]>=1.3.0",
10 | "python-dotenv>=1.0.0",
11 | ]
12 |
```
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
```python
1 | import json
2 | import os
3 | import httpx
4 | import logging
5 | import sys
6 | from mcp.server.fastmcp import FastMCP
7 | from dotenv import load_dotenv
8 |
9 | # Configure logging to write to stderr
10 | logging.basicConfig(
11 | level=logging.INFO,
12 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
13 | stream=sys.stderr,
14 | )
15 | logger = logging.getLogger("financial-datasets-mcp")
16 |
17 | # Initialize FastMCP server
18 | mcp = FastMCP("financial-datasets")
19 |
20 | # Constants
21 | FINANCIAL_DATASETS_API_BASE = "https://api.financialdatasets.ai"
22 |
23 |
24 | # Helper function to make API requests
25 | async def make_request(url: str) -> dict[str, any] | None:
26 | """Make a request to the Financial Datasets API with proper error handling."""
27 | # Load environment variables from .env file
28 | load_dotenv()
29 |
30 | headers = {}
31 | if api_key := os.environ.get("FINANCIAL_DATASETS_API_KEY"):
32 | headers["X-API-KEY"] = api_key
33 |
34 | async with httpx.AsyncClient() as client:
35 | try:
36 | response = await client.get(url, headers=headers, timeout=30.0)
37 | response.raise_for_status()
38 | return response.json()
39 | except Exception as e:
40 | return {"Error": str(e)}
41 |
42 |
43 | @mcp.tool()
44 | async def get_income_statements(
45 | ticker: str,
46 | period: str = "annual",
47 | limit: int = 4,
48 | ) -> str:
49 | """Get income statements for a company.
50 |
51 | Args:
52 | ticker: Ticker symbol of the company (e.g. AAPL, GOOGL)
53 | period: Period of the income statement (e.g. annual, quarterly, ttm)
54 | limit: Number of income statements to return (default: 4)
55 | """
56 | # Fetch data from the API
57 | url = f"{FINANCIAL_DATASETS_API_BASE}/financials/income-statements/?ticker={ticker}&period={period}&limit={limit}"
58 | data = await make_request(url)
59 |
60 | # Check if data is found
61 | if not data:
62 | return "Unable to fetch income statements or no income statements found."
63 |
64 | # Extract the income statements
65 | income_statements = data.get("income_statements", [])
66 |
67 | # Check if income statements are found
68 | if not income_statements:
69 | return "Unable to fetch income statements or no income statements found."
70 |
71 | # Stringify the income statements
72 | return json.dumps(income_statements, indent=2)
73 |
74 |
75 | @mcp.tool()
76 | async def get_balance_sheets(
77 | ticker: str,
78 | period: str = "annual",
79 | limit: int = 4,
80 | ) -> str:
81 | """Get balance sheets for a company.
82 |
83 | Args:
84 | ticker: Ticker symbol of the company (e.g. AAPL, GOOGL)
85 | period: Period of the balance sheet (e.g. annual, quarterly, ttm)
86 | limit: Number of balance sheets to return (default: 4)
87 | """
88 | # Fetch data from the API
89 | url = f"{FINANCIAL_DATASETS_API_BASE}/financials/balance-sheets/?ticker={ticker}&period={period}&limit={limit}"
90 | data = await make_request(url)
91 |
92 | # Check if data is found
93 | if not data:
94 | return "Unable to fetch balance sheets or no balance sheets found."
95 |
96 | # Extract the balance sheets
97 | balance_sheets = data.get("balance_sheets", [])
98 |
99 | # Check if balance sheets are found
100 | if not balance_sheets:
101 | return "Unable to fetch balance sheets or no balance sheets found."
102 |
103 | # Stringify the balance sheets
104 | return json.dumps(balance_sheets, indent=2)
105 |
106 |
107 | @mcp.tool()
108 | async def get_cash_flow_statements(
109 | ticker: str,
110 | period: str = "annual",
111 | limit: int = 4,
112 | ) -> str:
113 | """Get cash flow statements for a company.
114 |
115 | Args:
116 | ticker: Ticker symbol of the company (e.g. AAPL, GOOGL)
117 | period: Period of the cash flow statement (e.g. annual, quarterly, ttm)
118 | limit: Number of cash flow statements to return (default: 4)
119 | """
120 | # Fetch data from the API
121 | url = f"{FINANCIAL_DATASETS_API_BASE}/financials/cash-flow-statements/?ticker={ticker}&period={period}&limit={limit}"
122 | data = await make_request(url)
123 |
124 | # Check if data is found
125 | if not data:
126 | return "Unable to fetch cash flow statements or no cash flow statements found."
127 |
128 | # Extract the cash flow statements
129 | cash_flow_statements = data.get("cash_flow_statements", [])
130 |
131 | # Check if cash flow statements are found
132 | if not cash_flow_statements:
133 | return "Unable to fetch cash flow statements or no cash flow statements found."
134 |
135 | # Stringify the cash flow statements
136 | return json.dumps(cash_flow_statements, indent=2)
137 |
138 |
139 | @mcp.tool()
140 | async def get_current_stock_price(ticker: str) -> str:
141 | """Get the current / latest price of a company.
142 |
143 | Args:
144 | ticker: Ticker symbol of the company (e.g. AAPL, GOOGL)
145 | """
146 | # Fetch data from the API
147 | url = f"{FINANCIAL_DATASETS_API_BASE}/prices/snapshot/?ticker={ticker}"
148 | data = await make_request(url)
149 |
150 | # Check if data is found
151 | if not data:
152 | return "Unable to fetch current price or no current price found."
153 |
154 | # Extract the current price
155 | snapshot = data.get("snapshot", {})
156 |
157 | # Check if current price is found
158 | if not snapshot:
159 | return "Unable to fetch current price or no current price found."
160 |
161 | # Stringify the current price
162 | return json.dumps(snapshot, indent=2)
163 |
164 |
165 | @mcp.tool()
166 | async def get_historical_stock_prices(
167 | ticker: str,
168 | start_date: str,
169 | end_date: str,
170 | interval: str = "day",
171 | interval_multiplier: int = 1,
172 | ) -> str:
173 | """Gets historical stock prices for a company.
174 |
175 | Args:
176 | ticker: Ticker symbol of the company (e.g. AAPL, GOOGL)
177 | start_date: Start date of the price data (e.g. 2020-01-01)
178 | end_date: End date of the price data (e.g. 2020-12-31)
179 | interval: Interval of the price data (e.g. minute, hour, day, week, month)
180 | interval_multiplier: Multiplier of the interval (e.g. 1, 2, 3)
181 | """
182 | # Fetch data from the API
183 | url = f"{FINANCIAL_DATASETS_API_BASE}/prices/?ticker={ticker}&interval={interval}&interval_multiplier={interval_multiplier}&start_date={start_date}&end_date={end_date}"
184 | data = await make_request(url)
185 |
186 | # Check if data is found
187 | if not data:
188 | return "Unable to fetch prices or no prices found."
189 |
190 | # Extract the prices
191 | prices = data.get("prices", [])
192 |
193 | # Check if prices are found
194 | if not prices:
195 | return "Unable to fetch prices or no prices found."
196 |
197 | # Stringify the prices
198 | return json.dumps(prices, indent=2)
199 |
200 |
201 | @mcp.tool()
202 | async def get_company_news(ticker: str) -> str:
203 | """Get news for a company.
204 |
205 | Args:
206 | ticker: Ticker symbol of the company (e.g. AAPL, GOOGL)
207 | """
208 | # Fetch data from the API
209 | url = f"{FINANCIAL_DATASETS_API_BASE}/news/?ticker={ticker}"
210 | data = await make_request(url)
211 |
212 | # Check if data is found
213 | if not data:
214 | return "Unable to fetch news or no news found."
215 |
216 | # Extract the news
217 | news = data.get("news", [])
218 |
219 | # Check if news are found
220 | if not news:
221 | return "Unable to fetch news or no news found."
222 | return json.dumps(news, indent=2)
223 |
224 |
225 | @mcp.tool()
226 | async def get_available_crypto_tickers() -> str:
227 | """
228 | Gets all available crypto tickers.
229 | """
230 | # Fetch data from the API
231 | url = f"{FINANCIAL_DATASETS_API_BASE}/crypto/prices/tickers"
232 | data = await make_request(url)
233 |
234 | # Check if data is found
235 | if not data:
236 | return "Unable to fetch available crypto tickers or no available crypto tickers found."
237 |
238 | # Extract the available crypto tickers
239 | tickers = data.get("tickers", [])
240 |
241 | # Stringify the available crypto tickers
242 | return json.dumps(tickers, indent=2)
243 |
244 |
245 | @mcp.tool()
246 | async def get_crypto_prices(
247 | ticker: str,
248 | start_date: str,
249 | end_date: str,
250 | interval: str = "day",
251 | interval_multiplier: int = 1,
252 | ) -> str:
253 | """
254 | Gets historical prices for a crypto currency.
255 | """
256 | # Fetch data from the API
257 | url = f"{FINANCIAL_DATASETS_API_BASE}/crypto/prices/?ticker={ticker}&interval={interval}&interval_multiplier={interval_multiplier}&start_date={start_date}&end_date={end_date}"
258 | data = await make_request(url)
259 |
260 | # Check if data is found
261 | if not data:
262 | return "Unable to fetch prices or no prices found."
263 |
264 | # Extract the prices
265 | prices = data.get("prices", [])
266 |
267 | # Check if prices are found
268 | if not prices:
269 | return "Unable to fetch prices or no prices found."
270 |
271 | # Stringify the prices
272 | return json.dumps(prices, indent=2)
273 |
274 |
275 | @mcp.tool()
276 | async def get_historical_crypto_prices(
277 | ticker: str,
278 | start_date: str,
279 | end_date: str,
280 | interval: str = "day",
281 | interval_multiplier: int = 1,
282 | ) -> str:
283 | """Gets historical prices for a crypto currency.
284 |
285 | Args:
286 | ticker: Ticker symbol of the crypto currency (e.g. BTC-USD). The list of available crypto tickers can be retrieved via the get_available_crypto_tickers tool.
287 | start_date: Start date of the price data (e.g. 2020-01-01)
288 | end_date: End date of the price data (e.g. 2020-12-31)
289 | interval: Interval of the price data (e.g. minute, hour, day, week, month)
290 | interval_multiplier: Multiplier of the interval (e.g. 1, 2, 3)
291 | """
292 | # Fetch data from the API
293 | url = f"{FINANCIAL_DATASETS_API_BASE}/crypto/prices/?ticker={ticker}&interval={interval}&interval_multiplier={interval_multiplier}&start_date={start_date}&end_date={end_date}"
294 | data = await make_request(url)
295 |
296 | # Check if data is found
297 | if not data:
298 | return "Unable to fetch prices or no prices found."
299 |
300 | # Extract the prices
301 | prices = data.get("prices", [])
302 |
303 | # Check if prices are found
304 | if not prices:
305 | return "Unable to fetch prices or no prices found."
306 |
307 | # Stringify the prices
308 | return json.dumps(prices, indent=2)
309 |
310 |
311 | @mcp.tool()
312 | async def get_current_crypto_price(ticker: str) -> str:
313 | """Get the current / latest price of a crypto currency.
314 |
315 | Args:
316 | ticker: Ticker symbol of the crypto currency (e.g. BTC-USD). The list of available crypto tickers can be retrieved via the get_available_crypto_tickers tool.
317 | """
318 | # Fetch data from the API
319 | url = f"{FINANCIAL_DATASETS_API_BASE}/crypto/prices/snapshot/?ticker={ticker}"
320 | data = await make_request(url)
321 |
322 | # Check if data is found
323 | if not data:
324 | return "Unable to fetch current price or no current price found."
325 |
326 | # Extract the current price
327 | snapshot = data.get("snapshot", {})
328 |
329 | # Check if current price is found
330 | if not snapshot:
331 | return "Unable to fetch current price or no current price found."
332 |
333 | # Stringify the current price
334 | return json.dumps(snapshot, indent=2)
335 |
336 |
337 | @mcp.tool()
338 | async def get_sec_filings(
339 | ticker: str,
340 | limit: int = 10,
341 | filing_type: str | None = None,
342 | ) -> str:
343 | """Get all SEC filings for a company.
344 |
345 | Args:
346 | ticker: Ticker symbol of the company (e.g. AAPL, GOOGL)
347 | limit: Number of SEC filings to return (default: 10)
348 | filing_type: Type of SEC filing (e.g. 10-K, 10-Q, 8-K)
349 | """
350 | # Fetch data from the API
351 | url = f"{FINANCIAL_DATASETS_API_BASE}/filings/?ticker={ticker}&limit={limit}"
352 | if filing_type:
353 | url += f"&filing_type={filing_type}"
354 |
355 | # Call the API
356 | data = await make_request(url)
357 |
358 | # Extract the SEC filings
359 | filings = data.get("filings", [])
360 |
361 | # Check if SEC filings are found
362 | if not filings:
363 | return f"Unable to fetch SEC filings or no SEC filings found."
364 |
365 | # Stringify the SEC filings
366 | return json.dumps(filings, indent=2)
367 |
368 | if __name__ == "__main__":
369 | # Log server startup
370 | logger.info("Starting Financial Datasets MCP Server...")
371 |
372 | # Initialize and run the server
373 | mcp.run(transport="stdio")
374 |
375 | # This line won't be reached during normal operation
376 | logger.info("Server stopped")
377 |
```