Thanks for your interest in contributing! This server is reverse-engineered against QuantData's internal REST API, so contributions that improve resilience, add new tools, or smooth out the developer experience are all welcome.
This guide covers how the project is organized, how to set up a dev environment, the recipe for adding a new MCP tool, and the conventions to follow when sending a PR.
Be kind, be useful, assume good faith. Bug reports and PRs are equally valuable.
- Bug reports — open an issue with the failing tool call, what you expected, and the error returned. If the QuantData API shape has shifted, include a sample response payload (with credentials redacted).
- New tools — wrap an additional QuantData chart/widget as an MCP tool (see Adding a new tool below).
- Output formatting — the
_fmt_*helpers inserver.pyturn raw API responses into LLM-friendly text. PRs that make output denser, clearer, or more accurate are great. - Docs — clarifications, additional ticker examples, troubleshooting entries.
- Tests — there are no tests yet. A small fixture-based test suite would be a high-leverage first contribution.
quantdata_mcp/
__init__.py package version
__main__.py CLI dispatcher: `quantdata-mcp serve` / `setup`
config.py load/save ~/.quantdata-mcp/config.json (auth, page id, tool ids)
tools.py ToolType / ToolDefinition registry — the 11 widgets created during setup
setup.py setup wizard: validates creds → creates page + tools → saves config
client.py QuantDataClient — wraps the QuantData REST API (auth, retries, fetches)
server.py FastMCP server — defines @mcp.tool() functions and output formatters
Mental model: during setup, we create a hidden page on the user's QuantData account containing one widget per data type. At runtime, each MCP tool call mutates that page's filters (date / ticker / expiration / moneyness / strike), fetches the data, then restores the filters. The tool IDs are user-specific UUIDs persisted to ~/.quantdata-mcp/config.json.
If you change anything in tools.py, existing users will need to re-run quantdata-mcp setup to provision the new tool — bear that in mind for backwards compatibility.
Requirements: Python 3.11+ and uv (recommended) or pip.
# Clone
git clone https://github.com/zzulanas/quantdata-mcp.git
cd quantdata-mcp
# Install in editable mode with uv
uv venv
source .venv/bin/activate
uv pip install -e .
# Or with pip
python3.11 -m venv .venv && source .venv/bin/activate
pip install -e .You'll need an active QuantData account to actually call the API. Pull your authorization token and x-instance-id header out of the network tab (see the README for the click-by-click) and run setup once:
quantdata-mcp setup --auth-token "eyJhbGci..." --instance-id "xxxx-xxxx-..."To keep your dev config separate from your real one, point at a different config dir:
export QUANTDATA_MCP_CONFIG_DIR=$PWD/.dev-config
quantdata-mcp setup --auth-token "..." --instance-id "..."config.py honors that env var (it falls back to ~/.quantdata-mcp otherwise).
The server speaks MCP over stdio, so the easiest way to drive it during development is to wire it into Claude Code or Claude Desktop pointed at your editable checkout:
{
"mcpServers": {
"quantdata-dev": {
"command": "/absolute/path/to/quantdata-mcp/.venv/bin/quantdata-mcp",
"args": ["serve"],
"env": {
"QUANTDATA_MCP_CONFIG_DIR": "/absolute/path/to/quantdata-mcp/.dev-config"
}
}
}
}Restart your client and you should see quantdata-dev listed alongside any other MCP servers.
For raw protocol debugging, you can drive the server by hand with the MCP Inspector:
npx @modelcontextprotocol/inspector quantdata-mcp servestdout is reserved for the MCP JSON-RPC transport — never print() to stdout from server code, or you'll corrupt the protocol stream. client.py already routes its logging handler to stderr; mirror that pattern in any new code:
import logging
import sys
logger = logging.getLogger(__name__)
if not logger.handlers:
h = logging.StreamHandler(sys.stderr)
h.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
logger.addHandler(h)To see logs from your client, raise the level (logger.setLevel(logging.DEBUG)) and check the client's MCP-server logs.
Every tool that wraps a QuantData widget follows the same shape. Here's the recipe.
Add an entry to quantdata_mcp/tools.py:
class ToolType(str, Enum):
...
MY_NEW_CHART = "OPTIONS_MY_NEW_CHART" # exact backend identifier
TOOL_DEFINITIONS = {
...
"my_new_tool": ToolDefinition(
canonical_name="my_new_tool",
tool_type=ToolType.MY_NEW_CHART,
endpoint="options/my-new-endpoint",
label="My New Tool",
),
}endpoint is the REST path stem; the client appends /{tool_id} when fetching. label shows up as the tab name on the QuantData page.
If the new endpoint behaves like the existing ones (GET options/<endpoint>/<tool_id>), the generic fetch_tool_data(tool_spec) works without changes. Otherwise add a dedicated method to QuantDataClient:
def fetch_my_new_data(self, tool_id: str) -> dict[str, Any] | None:
try:
response = self._make_request("GET", f"options/my-new-endpoint/{tool_id}", timeout=30)
return response.json()
except Exception as e:
logger.error(f"Failed to fetch my new data: {e}")
return NoneFormatters live alphabetically below the existing _fmt_* helpers in server.py. Keep them text-only and aimed at LLM consumption — wide aligned columns are fine, but avoid ANSI escapes or anything that won't render in a chat surface:
def _fmt_my_new_data(data: dict[str, Any] | None, ticker: str = "SPX") -> str:
if not data or "response" not in data:
return "No data available."
resp = data["response"]
# ...build readable output...
return "\n".join(lines)Add the @mcp.tool() function. Mirror the existing pattern: apply page filter → apply tool filter (if any) → fetch → restore filters in finally:
@mcp.tool()
def qd_get_my_new_data(
ticker: str = "SPX",
date: str | None = None,
expiration_date: str | None = None,
) -> str:
"""One-line summary the LLM will read.
Longer description: when to call this, what the output means.
Args:
ticker: Ticker symbol (default: SPX).
date: Session date YYYY-MM-DD (default: today).
expiration_date: Expiration YYYY-MM-DD (default: 0DTE).
"""
try:
c = _get_client()
changed = _apply_page_filter(date, ticker, expiration_date)
tool = _get_specs()["my_new_tool"]
try:
data = c.fetch_my_new_data(tool.tool_id)
finally:
_restore_page_filter(changed)
return _fmt_my_new_data(data, ticker=ticker)
except Exception as e:
return f"Error fetching my new data: {e}"The docstring is the tool description the LLM sees. Be explicit about defaults, units (dollars vs cents, premium vs volume), and any ticker-specific gotchas (SPX/SPY/QQQ have daily expirations; equities don't).
setup.py walks TOOL_DEFINITIONS and creates any missing tools, so:
quantdata-mcp setup --auth-token "..." --instance-id "..."picks up your new entry and provisions the widget on your page. Existing users will need to do the same after the change ships.
Drive the new tool through your MCP client and verify both the happy path (default args) and at least one filtered call (e.g. with expiration_date set, or with moneyness=["OTM"] if applicable).
- Format with ruff if you have it (
ruff format .); otherwise match the surrounding style — 4-space indent, double quotes,from __future__ import annotationsat the top of every module. - Type hints everywhere. The codebase targets Python 3.11+, so use the modern syntax:
dict[str, Any],list[float],str | None, etc. - Default to no comments. Add one only when the why is non-obvious (e.g. workarounds, hidden constraints).
- All
_fmt_*helpers return plain text. Keep tables narrow (≤ 80 chars where possible) and prefer a short header + aligned columns. - Always include the ticker and price in the output where relevant — the LLM may not retain context between tool calls.
- Prefer human-readable scaling:
$1.2Mover$1,234,567,$5.40over540cents. Convert*InCentsfields explicitly (/100).
- Each
@mcp.tool()function wraps its body intry / except Exceptionand returns af"Error ...: {e}"string. Errors are part of the LLM's context — don't raise. - Inside
client.py, log the error and returnNone/False. Let callers decide how to surface it.
If you mutate metadata or filters on a tool, restore them in a finally. Defaults are scattered through server.py; capture the original via c.get_tool(tool_id) first when in doubt.
- Use Conventional Commits-ish prefixes:
feat:,fix:,docs:,refactor:,chore:. Match the existing log style — past commits are good examples. - One logical change per PR. If you're adding a tool and refactoring shared helpers, split it.
- Update the README's "Available Tools" table whenever you add or remove an MCP tool.
- If your change requires existing users to re-run
quantdata-mcp setup, call that out in the PR description.
QuantData has no public API. Endpoints, response shapes, and filter keys can change without notice. If a tool starts returning empty data:
- Open
https://v3.quantdata.usin your browser, refresh the relevant chart, and inspect the request in DevTools → Network. - Compare the request URL, headers, and JSON body to what
client.pysends. - Compare the response shape to what the formatter expects.
- Patch the affected
fetch_*method or_fmt_*formatter.
When you discover a shape change, document it in your PR — it helps future debuggers.
Maintainers only. There is no CI — releases are built and published manually
from a clean checkout of main.
# 1. Bump version in pyproject.toml AND quantdata_mcp/__init__.py.
# (tests/test_packaging.py asserts the two stay in sync.)
# 2. Update release notes (write release-notes.md or use the GitHub UI).
# 3. Commit + tag
git commit -am "release: vX.Y.Z"
git tag vX.Y.Z
git push && git push --tags
# 4. Build distribution artifacts (sdist + wheel)
# `pipx` works in any shell without an active venv. (`pip install --user`
# is an equally portable alternative; `uv pip install` requires an active venv.)
pipx install build
pipx install twine
python -m build # produces dist/quantdata_mcp-X.Y.Z.tar.gz + .whl
# 5. Verify the artifacts before uploading
twine check dist/*
# 6. Dry-run on TestPyPI BEFORE publishing to real PyPI.
# PyPI is unrepublishable for a given version — fat-fingering 0.1.0
# means you can never re-release as 0.1.0. Always dry-run first.
# (One-time setup: register at https://test.pypi.org/account/register/
# and create an API token at https://test.pypi.org/manage/account/token/.)
twine upload --repository testpypi dist/*
pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
"quantdata-mcp==X.Y.Z"
# Verify the install actually works (e.g. `quantdata-mcp --help`),
# then proceed to real PyPI:
# 7. Publish to PyPI
twine upload dist/*
# 8. Create the GitHub release from the tag and attach the built artifacts
gh release create vX.Y.Z dist/* --notes-file release-notes.mdAfter publishing, users can install with:
pip install quantdata-mcp # PyPI
uv pip install quantdata-mcp # PyPI via uv
# or pin a specific git tag:
uv pip install git+https://github.com/zzulanas/quantdata-mcp.git@vX.Y.ZOpen a discussion or an issue. Thanks for contributing!