vanoe-intelligence-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VanoeAI Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: vanoe-intelligence-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for the Vanoe Market Intelligence API: composite verdicts, trend-stage analysis, Point & Figure, sector breadth, macro regime and SEC filings as tools for Claude and other MCP clients.
5
+ Author-email: VanoeAI Ltd <support@vanoe.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, https://api.vanoe.ai
8
+ Project-URL: Documentation, https://api.vanoe.ai/docs
9
+ Project-URL: Get an API key, https://api.vanoe.ai/signup
10
+ Keywords: mcp,model-context-protocol,finance,stocks,trading,signals,claude
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Financial and Insurance Industry
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Office/Business :: Financial :: Investment
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: mcp<3.0,>=2.0
25
+ Requires-Dist: httpx>=0.27
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=8.0; extra == "test"
28
+ Requires-Dist: pytest-asyncio>=0.24; extra == "test"
29
+ Dynamic: license-file
30
+
31
+ # Vanoe Market Intelligence MCP server
32
+
33
+ Exposes the [Vanoe Market Intelligence API](https://api.vanoe.ai) as tools any
34
+ MCP-capable AI assistant (Claude Desktop, Claude Code, Cursor, etc.) can call
35
+ directly: a composite 0–100 verdict per ticker, trend-stage analysis (current
36
+ and a backtest-safe weekly history), Point & Figure signals, sector breadth,
37
+ the macro risk regime, SEC insider and 8-K filings, the macro calendar, and
38
+ your usage.
39
+
40
+ This is a thin authenticated client, not a free tier: every tool call is a
41
+ real, metered call against your plan using your own API key, exactly like
42
+ calling the REST API yourself. Get a free key (1,000 credits a month, no card)
43
+ at https://api.vanoe.ai/signup.
44
+
45
+ > Informational data only. Not financial advice. See https://api.vanoe.ai/terms.
46
+
47
+ ## Install
48
+
49
+ No install step is needed with [uv](https://docs.astral.sh/uv/) — `uvx` fetches
50
+ and runs the package on demand:
51
+
52
+ ```
53
+ uvx vanoe-intelligence-mcp
54
+ ```
55
+
56
+ Or install it permanently with `pipx install vanoe-intelligence-mcp` or
57
+ `pip install vanoe-intelligence-mcp`, which puts a `vanoe-intelligence-mcp`
58
+ command on your PATH.
59
+
60
+ ## Configure your MCP client
61
+
62
+ Set one environment variable, `VANOE_API_KEY`. `VANOE_API_BASE_URL` is optional
63
+ and defaults to `https://api.vanoe.ai`.
64
+
65
+ **Claude Desktop** (`claude_desktop_config.json`) and most other clients:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "vanoe": {
71
+ "command": "uvx",
72
+ "args": ["vanoe-intelligence-mcp"],
73
+ "env": { "VANOE_API_KEY": "sk_live_your_key_here" }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ **Claude Code**:
80
+
81
+ ```
82
+ claude mcp add vanoe -e VANOE_API_KEY=sk_live_your_key_here -- uvx vanoe-intelligence-mcp
83
+ ```
84
+
85
+ Then ask your assistant things like *"What's the verdict on NVDA and why?"*,
86
+ *"Which stage are AAPL, MSFT and AMD in?"*, or *"Is the macro regime risk-on
87
+ right now?"* — it will call the tools below.
88
+
89
+ ## Tools
90
+
91
+ | Tool | API call | Credits |
92
+ |---|---|---|
93
+ | `get_verdict(ticker)` | `GET /v1/verdict/{ticker}` | 5 |
94
+ | `get_stage(tickers)` | `GET /v1/signals/stage` | 1 per ticker |
95
+ | `get_stage_history(ticker, weeks)` | `GET /v1/signals/stage/history` | 5 |
96
+ | `get_pnf(tickers)` | `GET /v1/signals/pnf` | 1 per ticker |
97
+ | `get_sector_breadth()` | `GET /v1/sector-breadth` | 1 |
98
+ | `get_macro()` | `GET /v1/macro` | 1 |
99
+ | `get_filings(ticker)` | `GET /v1/filings/{ticker}` | 1 |
100
+ | `get_calendar()` | `GET /v1/calendar` | 1 |
101
+ | `get_usage()` | `GET /v1/usage` | 0 |
102
+
103
+ Tools never raise. Any failure — missing key, network error, quota exceeded,
104
+ an unknown ticker — comes back as `{"error": {"code", "message",
105
+ "http_status"}}` carrying the API's own message, so the assistant can explain
106
+ what happened instead of crashing the call.
107
+
108
+ Webhook management is deliberately not exposed: it is account administration,
109
+ not something to do mid-conversation. Use the REST API for that.
110
+
111
+ ## Development
112
+
113
+ ```
114
+ cd mcp_server
115
+ python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
116
+ pip install -e '.[test]'
117
+ pytest tests
118
+ VANOE_API_BASE_URL=http://localhost:8000 VANOE_API_KEY=sk_live_… vanoe-intelligence-mcp
119
+ ```
120
+
121
+ Install it in its own virtualenv, separate from the API server's own
122
+ `requirements.txt`: the `mcp` SDK pulls in a newer `starlette` than the API's
123
+ FastAPI pin tolerates, and sharing one environment breaks the API server with
124
+ `Router.__init__() got an unexpected keyword argument 'on_startup'`. End users
125
+ never hit this (the MCP server runs on their machine, next to their assistant).
126
+
127
+ `tests/manual_e2e_smoke.py` runs the real server as a subprocess over stdio
128
+ against a reachable API with a real key; it is not part of `pytest`.
129
+
130
+ The MCP Python SDK is on v2: the server class is
131
+ `mcp.server.mcpserver.MCPServer` (older tutorials show `FastMCP` at a
132
+ different import path).
133
+
134
+ ## Releasing
135
+
136
+ ```
137
+ cd mcp_server
138
+ python -m build
139
+ twine upload dist/*
140
+ ```
141
+
142
+ Bump `version` in `pyproject.toml` first. The older `INTEL_API_KEY` /
143
+ `INTEL_API_BASE_URL` variable names are still accepted for compatibility.
@@ -0,0 +1,113 @@
1
+ # Vanoe Market Intelligence MCP server
2
+
3
+ Exposes the [Vanoe Market Intelligence API](https://api.vanoe.ai) as tools any
4
+ MCP-capable AI assistant (Claude Desktop, Claude Code, Cursor, etc.) can call
5
+ directly: a composite 0–100 verdict per ticker, trend-stage analysis (current
6
+ and a backtest-safe weekly history), Point & Figure signals, sector breadth,
7
+ the macro risk regime, SEC insider and 8-K filings, the macro calendar, and
8
+ your usage.
9
+
10
+ This is a thin authenticated client, not a free tier: every tool call is a
11
+ real, metered call against your plan using your own API key, exactly like
12
+ calling the REST API yourself. Get a free key (1,000 credits a month, no card)
13
+ at https://api.vanoe.ai/signup.
14
+
15
+ > Informational data only. Not financial advice. See https://api.vanoe.ai/terms.
16
+
17
+ ## Install
18
+
19
+ No install step is needed with [uv](https://docs.astral.sh/uv/) — `uvx` fetches
20
+ and runs the package on demand:
21
+
22
+ ```
23
+ uvx vanoe-intelligence-mcp
24
+ ```
25
+
26
+ Or install it permanently with `pipx install vanoe-intelligence-mcp` or
27
+ `pip install vanoe-intelligence-mcp`, which puts a `vanoe-intelligence-mcp`
28
+ command on your PATH.
29
+
30
+ ## Configure your MCP client
31
+
32
+ Set one environment variable, `VANOE_API_KEY`. `VANOE_API_BASE_URL` is optional
33
+ and defaults to `https://api.vanoe.ai`.
34
+
35
+ **Claude Desktop** (`claude_desktop_config.json`) and most other clients:
36
+
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "vanoe": {
41
+ "command": "uvx",
42
+ "args": ["vanoe-intelligence-mcp"],
43
+ "env": { "VANOE_API_KEY": "sk_live_your_key_here" }
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ **Claude Code**:
50
+
51
+ ```
52
+ claude mcp add vanoe -e VANOE_API_KEY=sk_live_your_key_here -- uvx vanoe-intelligence-mcp
53
+ ```
54
+
55
+ Then ask your assistant things like *"What's the verdict on NVDA and why?"*,
56
+ *"Which stage are AAPL, MSFT and AMD in?"*, or *"Is the macro regime risk-on
57
+ right now?"* — it will call the tools below.
58
+
59
+ ## Tools
60
+
61
+ | Tool | API call | Credits |
62
+ |---|---|---|
63
+ | `get_verdict(ticker)` | `GET /v1/verdict/{ticker}` | 5 |
64
+ | `get_stage(tickers)` | `GET /v1/signals/stage` | 1 per ticker |
65
+ | `get_stage_history(ticker, weeks)` | `GET /v1/signals/stage/history` | 5 |
66
+ | `get_pnf(tickers)` | `GET /v1/signals/pnf` | 1 per ticker |
67
+ | `get_sector_breadth()` | `GET /v1/sector-breadth` | 1 |
68
+ | `get_macro()` | `GET /v1/macro` | 1 |
69
+ | `get_filings(ticker)` | `GET /v1/filings/{ticker}` | 1 |
70
+ | `get_calendar()` | `GET /v1/calendar` | 1 |
71
+ | `get_usage()` | `GET /v1/usage` | 0 |
72
+
73
+ Tools never raise. Any failure — missing key, network error, quota exceeded,
74
+ an unknown ticker — comes back as `{"error": {"code", "message",
75
+ "http_status"}}` carrying the API's own message, so the assistant can explain
76
+ what happened instead of crashing the call.
77
+
78
+ Webhook management is deliberately not exposed: it is account administration,
79
+ not something to do mid-conversation. Use the REST API for that.
80
+
81
+ ## Development
82
+
83
+ ```
84
+ cd mcp_server
85
+ python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
86
+ pip install -e '.[test]'
87
+ pytest tests
88
+ VANOE_API_BASE_URL=http://localhost:8000 VANOE_API_KEY=sk_live_… vanoe-intelligence-mcp
89
+ ```
90
+
91
+ Install it in its own virtualenv, separate from the API server's own
92
+ `requirements.txt`: the `mcp` SDK pulls in a newer `starlette` than the API's
93
+ FastAPI pin tolerates, and sharing one environment breaks the API server with
94
+ `Router.__init__() got an unexpected keyword argument 'on_startup'`. End users
95
+ never hit this (the MCP server runs on their machine, next to their assistant).
96
+
97
+ `tests/manual_e2e_smoke.py` runs the real server as a subprocess over stdio
98
+ against a reachable API with a real key; it is not part of `pytest`.
99
+
100
+ The MCP Python SDK is on v2: the server class is
101
+ `mcp.server.mcpserver.MCPServer` (older tutorials show `FastMCP` at a
102
+ different import path).
103
+
104
+ ## Releasing
105
+
106
+ ```
107
+ cd mcp_server
108
+ python -m build
109
+ twine upload dist/*
110
+ ```
111
+
112
+ Bump `version` in `pyproject.toml` first. The older `INTEL_API_KEY` /
113
+ `INTEL_API_BASE_URL` variable names are still accepted for compatibility.
File without changes
@@ -0,0 +1,4 @@
1
+ from mcp_server.server import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,72 @@
1
+ """Thin HTTP client for the Vanoe Market Intelligence API — one function per
2
+ /v1 GET endpoint.
3
+
4
+ Auth: Authorization: Bearer <VANOE_API_KEY>. Base URL: VANOE_API_BASE_URL
5
+ (defaults to https://api.vanoe.ai; point it at a local instance for dev).
6
+ The older INTEL_API_KEY / INTEL_API_BASE_URL names are still honoured.
7
+
8
+ On any non-2xx response, raises IntelAPIError carrying the API's own
9
+ structured {"error": {code, message, request_id}} body when present, so a
10
+ tool caller sees the SAME message a developer would get calling the API
11
+ directly (e.g. "Monthly quota exceeded..."), not a raw HTTP status.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import os
16
+
17
+ import httpx
18
+
19
+ DEFAULT_BASE_URL = "https://api.vanoe.ai"
20
+ SIGNUP_URL = "https://api.vanoe.ai/signup"
21
+ _TIMEOUT_SECONDS = 20
22
+
23
+
24
+ class IntelAPIError(Exception):
25
+ def __init__(self, status_code: int, code: str, message: str):
26
+ self.status_code = status_code
27
+ self.code = code
28
+ self.message = message
29
+ super().__init__(f"{message} (code={code}, http_status={status_code})")
30
+
31
+
32
+ class MissingApiKeyError(Exception):
33
+ def __init__(self):
34
+ super().__init__(
35
+ f"VANOE_API_KEY is not set. Get a free key at {SIGNUP_URL} and set the "
36
+ "VANOE_API_KEY environment variable before starting this MCP server."
37
+ )
38
+
39
+
40
+ def _env(new: str, old: str) -> str | None:
41
+ return os.environ.get(new) or os.environ.get(old)
42
+
43
+
44
+ def _base_url() -> str:
45
+ return (_env("VANOE_API_BASE_URL", "INTEL_API_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
46
+
47
+
48
+ def _headers() -> dict:
49
+ key = _env("VANOE_API_KEY", "INTEL_API_KEY")
50
+ if not key:
51
+ raise MissingApiKeyError()
52
+ return {"Authorization": f"Bearer {key}"}
53
+
54
+
55
+ async def get(path: str, params: dict | None = None) -> dict:
56
+ url = f"{_base_url()}{path}"
57
+ async with httpx.AsyncClient(timeout=_TIMEOUT_SECONDS) as http:
58
+ try:
59
+ r = await http.get(url, params=params, headers=_headers())
60
+ except httpx.RequestError as e:
61
+ raise IntelAPIError(0, "connection_error",
62
+ f"Could not reach the API at {_base_url()}: {e}") from e
63
+
64
+ if r.status_code >= 400:
65
+ try:
66
+ body = r.json()
67
+ err = body.get("error", {})
68
+ raise IntelAPIError(r.status_code, err.get("code", "unknown_error"),
69
+ err.get("message", r.text))
70
+ except ValueError:
71
+ raise IntelAPIError(r.status_code, "unknown_error", r.text)
72
+ return r.json()
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "vanoe-intelligence-mcp"
3
+ version = "0.1.0"
4
+ description = "MCP server for the Vanoe Market Intelligence API: composite verdicts, trend-stage analysis, Point & Figure, sector breadth, macro regime and SEC filings as tools for Claude and other MCP clients."
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ authors = [{ name = "VanoeAI Ltd", email = "support@vanoe.ai" }]
8
+ requires-python = ">=3.10"
9
+ keywords = ["mcp", "model-context-protocol", "finance", "stocks", "trading", "signals", "claude"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "Intended Audience :: Financial and Insurance Industry",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Office/Business :: Financial :: Investment",
21
+ ]
22
+ dependencies = ["mcp>=2.0,<3.0", "httpx>=0.27"]
23
+
24
+ [project.urls]
25
+ Homepage = "https://api.vanoe.ai"
26
+ Documentation = "https://api.vanoe.ai/docs"
27
+ "Get an API key" = "https://api.vanoe.ai/signup"
28
+
29
+ [project.scripts]
30
+ vanoe-intelligence-mcp = "mcp_server.server:main"
31
+
32
+ [build-system]
33
+ requires = ["setuptools>=68"]
34
+ build-backend = "setuptools.build_meta"
35
+
36
+ [tool.setuptools]
37
+ packages = ["mcp_server"]
38
+ license-files = ["LICENSE"]
39
+
40
+ [tool.setuptools.package-dir]
41
+ mcp_server = "."
42
+
43
+ [project.optional-dependencies]
44
+ test = ["pytest>=8.0", "pytest-asyncio>=0.24"]
@@ -0,0 +1,129 @@
1
+ """Vanoe Market Intelligence MCP server — exposes the API's signals as MCP
2
+ tools so any MCP-capable AI assistant (Claude Desktop, Claude Code, etc.) can
3
+ call them directly.
4
+
5
+ This is a thin authenticated client, not a free bypass: every tool call
6
+ makes a real, metered call against your plan using your own VANOE_API_KEY,
7
+ exactly like calling the REST API yourself.
8
+
9
+ Setup:
10
+ VANOE_API_KEY your sk_live_... key (required) — free at https://api.vanoe.ai/signup
11
+ VANOE_API_BASE_URL defaults to https://api.vanoe.ai; point at a local
12
+ instance for development
13
+
14
+ Run: vanoe-intelligence-mcp (or: python -m mcp_server) — stdio transport,
15
+ for Claude Desktop / Claude Code style MCP client configs.
16
+
17
+ Tools never raise — every failure (missing key, network error, an API
18
+ 4xx/5xx) is caught and returned as a {"error": {...}} JSON payload in the
19
+ tool result, not an exception. This is deliberate: a raised exception from a
20
+ tool crashes with UnexpectedToolError rather than being cleanly surfaced to
21
+ the caller (confirmed against the installed SDK version), so tools return a
22
+ predictable JSON shape either way instead of depending on that behavior.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+
28
+ from mcp.server.mcpserver import MCPServer
29
+
30
+ from mcp_server import client
31
+
32
+ mcp = MCPServer(
33
+ name="vanoe-market-intelligence",
34
+ version="0.1.0",
35
+ instructions=(
36
+ "Vanoe Market Intelligence signals: four-stage trend analysis, Point & "
37
+ "Figure patterns, sector breadth, a composite verdict score, macro "
38
+ "regime, insider filings, and the economic calendar. All output is "
39
+ "informational data only — not financial advice, and not a "
40
+ "recommendation, solicitation, or offer to buy or sell any security."
41
+ ),
42
+ )
43
+
44
+
45
+ async def _call(path: str, params: dict | None = None) -> str:
46
+ """GET the API and return the JSON result as a string, or a
47
+ {"error": {...}} JSON string on any failure — tools never raise."""
48
+ try:
49
+ data = await client.get(path, params)
50
+ return json.dumps(data, indent=2)
51
+ except client.MissingApiKeyError as e:
52
+ return json.dumps({"error": {"code": "missing_api_key", "message": str(e)}}, indent=2)
53
+ except client.IntelAPIError as e:
54
+ return json.dumps(
55
+ {"error": {"code": e.code, "message": e.message, "http_status": e.status_code}},
56
+ indent=2,
57
+ )
58
+ except Exception as e: # last-resort guard — tools must never raise
59
+ return json.dumps({"error": {"code": "mcp_server_error", "message": str(e)}}, indent=2)
60
+
61
+
62
+ @mcp.tool(description="Macro regime (risk_on/neutral/risk_off), rates, inflation, yield curve, positioning.")
63
+ async def get_macro() -> str:
64
+ return await _call("/v1/macro")
65
+
66
+
67
+ @mcp.tool(description=(
68
+ "Stage analysis (1=Basing, 2=Advancing, 3=Topping, 4=Declining) "
69
+ "for up to 50 tickers. Pass tickers as a comma-separated string, e.g. "
70
+ "'AAPL,MSFT,NVDA'."
71
+ ))
72
+ async def get_stage(tickers: str) -> str:
73
+ return await _call("/v1/signals/stage", {"tickers": tickers})
74
+
75
+
76
+ @mcp.tool(description=(
77
+ "Historical weekly trend-stage series for one ticker, walk-forward "
78
+ "computed (no lookahead) — the only commercial source of trend "
79
+ "stages as a backtest-safe data series. weeks defaults to 104, max 520."
80
+ ))
81
+ async def get_stage_history(ticker: str, weeks: int = 104) -> str:
82
+ return await _call("/v1/signals/stage/history", {"ticker": ticker, "weeks": weeks})
83
+
84
+
85
+ @mcp.tool(description=(
86
+ "Point & Figure signal, breakout pattern, and relative strength vs SPY, "
87
+ "for up to 50 tickers. Pass tickers as a comma-separated string."
88
+ ))
89
+ async def get_pnf(tickers: str) -> str:
90
+ return await _call("/v1/signals/pnf", {"tickers": tickers})
91
+
92
+
93
+ @mcp.tool(description="Bullish-percent breadth by sector — the % of names in each sector on a Point & Figure buy signal.")
94
+ async def get_sector_breadth() -> str:
95
+ return await _call("/v1/sector-breadth")
96
+
97
+
98
+ @mcp.tool(description=(
99
+ "Composite verdict for one ticker: fuses trend stage, Point & "
100
+ "Figure, insider-transaction clusters, and macro regime into a 0-100 "
101
+ "score, an accumulate/hold/distribute/avoid enum, and a plain-English "
102
+ "rationale. One call replaces separately calling stage, pnf, filings, "
103
+ "and macro."
104
+ ))
105
+ async def get_verdict(ticker: str) -> str:
106
+ return await _call(f"/v1/verdict/{ticker}")
107
+
108
+
109
+ @mcp.tool(description="Recent SEC insider transactions (Form 4) and 8-K filings for one ticker.")
110
+ async def get_filings(ticker: str) -> str:
111
+ return await _call(f"/v1/filings/{ticker}")
112
+
113
+
114
+ @mcp.tool(description="Upcoming macro events (FOMC, CPI, payrolls) within a look-ahead window in days (default 10, max 60).")
115
+ async def get_calendar(within_days: int = 10) -> str:
116
+ return await _call("/v1/calendar", {"within_days": within_days})
117
+
118
+
119
+ @mcp.tool(description="Your current billing-period usage and remaining credit quota on your plan. Free — never counts against quota.")
120
+ async def get_usage() -> str:
121
+ return await _call("/v1/usage")
122
+
123
+
124
+ def main() -> None:
125
+ mcp.run()
126
+
127
+
128
+ if __name__ == "__main__":
129
+ main()
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,97 @@
1
+ """client.get() tests — httpx is monkeypatched, no network touched."""
2
+ import httpx
3
+ import pytest
4
+
5
+ from mcp_server import client
6
+
7
+
8
+ @pytest.fixture(autouse=True)
9
+ def _api_key(monkeypatch):
10
+ monkeypatch.setenv("VANOE_API_KEY", "sk_live_test")
11
+
12
+
13
+ def test_missing_api_key_raises(monkeypatch):
14
+ monkeypatch.delenv("VANOE_API_KEY", raising=False)
15
+ monkeypatch.delenv("INTEL_API_KEY", raising=False)
16
+ with pytest.raises(client.MissingApiKeyError):
17
+ import asyncio
18
+ asyncio.run(client.get("/v1/macro"))
19
+
20
+
21
+ def test_get_success_returns_json(monkeypatch):
22
+ async def fake_get(self, url, params=None, headers=None):
23
+ assert headers["Authorization"] == "Bearer sk_live_test"
24
+ assert url == "https://api.vanoe.ai/v1/macro"
25
+ return httpx.Response(200, json={"data": {"regime": "risk_on"}}, request=httpx.Request("GET", url))
26
+
27
+ monkeypatch.setattr(httpx.AsyncClient, "get", fake_get)
28
+ import asyncio
29
+ result = asyncio.run(client.get("/v1/macro"))
30
+ assert result == {"data": {"regime": "risk_on"}}
31
+
32
+
33
+ def test_get_uses_custom_base_url(monkeypatch):
34
+ monkeypatch.setenv("VANOE_API_BASE_URL", "https://api.example.com/")
35
+
36
+ async def fake_get(self, url, params=None, headers=None):
37
+ assert url == "https://api.example.com/v1/usage"
38
+ return httpx.Response(200, json={}, request=httpx.Request("GET", url))
39
+
40
+ monkeypatch.setattr(httpx.AsyncClient, "get", fake_get)
41
+ import asyncio
42
+ asyncio.run(client.get("/v1/usage"))
43
+
44
+
45
+ def test_error_response_raises_intel_api_error_with_structured_body(monkeypatch):
46
+ async def fake_get(self, url, params=None, headers=None):
47
+ return httpx.Response(
48
+ 402,
49
+ json={"error": {"code": "quota_exceeded", "message": "Monthly quota exceeded.", "request_id": "abc"}},
50
+ request=httpx.Request("GET", url),
51
+ )
52
+
53
+ monkeypatch.setattr(httpx.AsyncClient, "get", fake_get)
54
+ import asyncio
55
+ with pytest.raises(client.IntelAPIError) as exc_info:
56
+ asyncio.run(client.get("/v1/macro"))
57
+ assert exc_info.value.status_code == 402
58
+ assert exc_info.value.code == "quota_exceeded"
59
+ assert exc_info.value.message == "Monthly quota exceeded."
60
+
61
+
62
+ def test_error_response_with_unparseable_body_still_raises(monkeypatch):
63
+ async def fake_get(self, url, params=None, headers=None):
64
+ return httpx.Response(500, content=b"not json", request=httpx.Request("GET", url))
65
+
66
+ monkeypatch.setattr(httpx.AsyncClient, "get", fake_get)
67
+ import asyncio
68
+ with pytest.raises(client.IntelAPIError) as exc_info:
69
+ asyncio.run(client.get("/v1/macro"))
70
+ assert exc_info.value.status_code == 500
71
+ assert exc_info.value.code == "unknown_error"
72
+
73
+
74
+ def test_connection_error_raises_intel_api_error(monkeypatch):
75
+ async def fake_get(self, url, params=None, headers=None):
76
+ raise httpx.ConnectError("connection refused")
77
+
78
+ monkeypatch.setattr(httpx.AsyncClient, "get", fake_get)
79
+ import asyncio
80
+ with pytest.raises(client.IntelAPIError) as exc_info:
81
+ asyncio.run(client.get("/v1/macro"))
82
+ assert exc_info.value.code == "connection_error"
83
+
84
+
85
+ def test_legacy_env_names_still_work(monkeypatch):
86
+ monkeypatch.delenv("VANOE_API_KEY", raising=False)
87
+ monkeypatch.setenv("INTEL_API_KEY", "sk_live_legacy")
88
+ monkeypatch.setenv("INTEL_API_BASE_URL", "http://localhost:8000")
89
+
90
+ async def fake_get(self, url, params=None, headers=None):
91
+ assert headers["Authorization"] == "Bearer sk_live_legacy"
92
+ assert url == "http://localhost:8000/v1/usage"
93
+ return httpx.Response(200, json={"ok": True}, request=httpx.Request("GET", url))
94
+
95
+ monkeypatch.setattr(httpx.AsyncClient, "get", fake_get)
96
+ import asyncio
97
+ assert asyncio.run(client.get("/v1/usage")) == {"ok": True}
@@ -0,0 +1,108 @@
1
+ """MCP tool tests — call tools in-process via mcp.call_tool(), with
2
+ mcp_server.client.get monkeypatched (no network, no real MCP transport).
3
+ Tools must never raise (see server.py's docstring) — every failure path
4
+ asserts a clean {"error": {...}} JSON text result instead of an exception.
5
+ """
6
+ import asyncio
7
+ import json
8
+
9
+ import pytest
10
+
11
+ from mcp_server import client, server
12
+
13
+
14
+ def _call(name: str, args: dict) -> dict:
15
+ result = asyncio.run(server.mcp.call_tool(name, args))
16
+ assert result.is_error is False # tools never raise -> is_error is always False
17
+ return json.loads(result.content[0].text)
18
+
19
+
20
+ def test_get_macro_passes_through_path(monkeypatch):
21
+ async def fake_get(path, params=None):
22
+ assert path == "/v1/macro"
23
+ assert params is None
24
+ return {"data": {"regime": "risk_on"}}
25
+
26
+ monkeypatch.setattr(client, "get", fake_get)
27
+ assert _call("get_macro", {}) == {"data": {"regime": "risk_on"}}
28
+
29
+
30
+ def test_get_stage_passes_tickers_param(monkeypatch):
31
+ async def fake_get(path, params=None):
32
+ assert path == "/v1/signals/stage"
33
+ assert params == {"tickers": "AAPL,MSFT"}
34
+ return {"data": {"AAPL": {"stage": 2}}}
35
+
36
+ monkeypatch.setattr(client, "get", fake_get)
37
+ result = _call("get_stage", {"tickers": "AAPL,MSFT"})
38
+ assert result["data"]["AAPL"]["stage"] == 2
39
+
40
+
41
+ def test_get_stage_history_default_weeks(monkeypatch):
42
+ async def fake_get(path, params=None):
43
+ assert path == "/v1/signals/stage/history"
44
+ assert params == {"ticker": "AAPL", "weeks": 104}
45
+ return {"data": {}}
46
+
47
+ monkeypatch.setattr(client, "get", fake_get)
48
+ _call("get_stage_history", {"ticker": "AAPL"})
49
+
50
+
51
+ def test_get_verdict_builds_path_with_ticker(monkeypatch):
52
+ async def fake_get(path, params=None):
53
+ assert path == "/v1/verdict/AAPL"
54
+ return {"data": {"verdict": "accumulate"}}
55
+
56
+ monkeypatch.setattr(client, "get", fake_get)
57
+ assert _call("get_verdict", {"ticker": "AAPL"})["data"]["verdict"] == "accumulate"
58
+
59
+
60
+ def test_get_calendar_default_within_days(monkeypatch):
61
+ async def fake_get(path, params=None):
62
+ assert params == {"within_days": 10}
63
+ return {"data": []}
64
+
65
+ monkeypatch.setattr(client, "get", fake_get)
66
+ _call("get_calendar", {})
67
+
68
+
69
+ def test_missing_api_key_returns_error_json_not_exception(monkeypatch):
70
+ async def fake_get(path, params=None):
71
+ raise client.MissingApiKeyError()
72
+
73
+ monkeypatch.setattr(client, "get", fake_get)
74
+ result = _call("get_usage", {})
75
+ assert result["error"]["code"] == "missing_api_key"
76
+
77
+
78
+ def test_intel_api_error_returns_structured_error_json(monkeypatch):
79
+ async def fake_get(path, params=None):
80
+ raise client.IntelAPIError(402, "quota_exceeded", "Monthly quota exceeded.")
81
+
82
+ monkeypatch.setattr(client, "get", fake_get)
83
+ result = _call("get_macro", {})
84
+ assert result["error"] == {
85
+ "code": "quota_exceeded",
86
+ "message": "Monthly quota exceeded.",
87
+ "http_status": 402,
88
+ }
89
+
90
+
91
+ def test_unexpected_exception_still_returns_error_json_not_raise(monkeypatch):
92
+ async def fake_get(path, params=None):
93
+ raise ValueError("something unrelated broke")
94
+
95
+ monkeypatch.setattr(client, "get", fake_get)
96
+ result = _call("get_macro", {})
97
+ assert result["error"]["code"] == "mcp_server_error"
98
+
99
+
100
+ def test_all_nine_tools_are_registered():
101
+ expected = {
102
+ "get_macro", "get_stage", "get_stage_history", "get_pnf",
103
+ "get_sector_breadth", "get_verdict", "get_filings", "get_calendar",
104
+ "get_usage",
105
+ }
106
+ tools = asyncio.run(server.mcp.list_tools())
107
+ names = {t.name for t in tools}
108
+ assert expected <= names
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: vanoe-intelligence-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for the Vanoe Market Intelligence API: composite verdicts, trend-stage analysis, Point & Figure, sector breadth, macro regime and SEC filings as tools for Claude and other MCP clients.
5
+ Author-email: VanoeAI Ltd <support@vanoe.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, https://api.vanoe.ai
8
+ Project-URL: Documentation, https://api.vanoe.ai/docs
9
+ Project-URL: Get an API key, https://api.vanoe.ai/signup
10
+ Keywords: mcp,model-context-protocol,finance,stocks,trading,signals,claude
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Financial and Insurance Industry
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Office/Business :: Financial :: Investment
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: mcp<3.0,>=2.0
25
+ Requires-Dist: httpx>=0.27
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=8.0; extra == "test"
28
+ Requires-Dist: pytest-asyncio>=0.24; extra == "test"
29
+ Dynamic: license-file
30
+
31
+ # Vanoe Market Intelligence MCP server
32
+
33
+ Exposes the [Vanoe Market Intelligence API](https://api.vanoe.ai) as tools any
34
+ MCP-capable AI assistant (Claude Desktop, Claude Code, Cursor, etc.) can call
35
+ directly: a composite 0–100 verdict per ticker, trend-stage analysis (current
36
+ and a backtest-safe weekly history), Point & Figure signals, sector breadth,
37
+ the macro risk regime, SEC insider and 8-K filings, the macro calendar, and
38
+ your usage.
39
+
40
+ This is a thin authenticated client, not a free tier: every tool call is a
41
+ real, metered call against your plan using your own API key, exactly like
42
+ calling the REST API yourself. Get a free key (1,000 credits a month, no card)
43
+ at https://api.vanoe.ai/signup.
44
+
45
+ > Informational data only. Not financial advice. See https://api.vanoe.ai/terms.
46
+
47
+ ## Install
48
+
49
+ No install step is needed with [uv](https://docs.astral.sh/uv/) — `uvx` fetches
50
+ and runs the package on demand:
51
+
52
+ ```
53
+ uvx vanoe-intelligence-mcp
54
+ ```
55
+
56
+ Or install it permanently with `pipx install vanoe-intelligence-mcp` or
57
+ `pip install vanoe-intelligence-mcp`, which puts a `vanoe-intelligence-mcp`
58
+ command on your PATH.
59
+
60
+ ## Configure your MCP client
61
+
62
+ Set one environment variable, `VANOE_API_KEY`. `VANOE_API_BASE_URL` is optional
63
+ and defaults to `https://api.vanoe.ai`.
64
+
65
+ **Claude Desktop** (`claude_desktop_config.json`) and most other clients:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "vanoe": {
71
+ "command": "uvx",
72
+ "args": ["vanoe-intelligence-mcp"],
73
+ "env": { "VANOE_API_KEY": "sk_live_your_key_here" }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ **Claude Code**:
80
+
81
+ ```
82
+ claude mcp add vanoe -e VANOE_API_KEY=sk_live_your_key_here -- uvx vanoe-intelligence-mcp
83
+ ```
84
+
85
+ Then ask your assistant things like *"What's the verdict on NVDA and why?"*,
86
+ *"Which stage are AAPL, MSFT and AMD in?"*, or *"Is the macro regime risk-on
87
+ right now?"* — it will call the tools below.
88
+
89
+ ## Tools
90
+
91
+ | Tool | API call | Credits |
92
+ |---|---|---|
93
+ | `get_verdict(ticker)` | `GET /v1/verdict/{ticker}` | 5 |
94
+ | `get_stage(tickers)` | `GET /v1/signals/stage` | 1 per ticker |
95
+ | `get_stage_history(ticker, weeks)` | `GET /v1/signals/stage/history` | 5 |
96
+ | `get_pnf(tickers)` | `GET /v1/signals/pnf` | 1 per ticker |
97
+ | `get_sector_breadth()` | `GET /v1/sector-breadth` | 1 |
98
+ | `get_macro()` | `GET /v1/macro` | 1 |
99
+ | `get_filings(ticker)` | `GET /v1/filings/{ticker}` | 1 |
100
+ | `get_calendar()` | `GET /v1/calendar` | 1 |
101
+ | `get_usage()` | `GET /v1/usage` | 0 |
102
+
103
+ Tools never raise. Any failure — missing key, network error, quota exceeded,
104
+ an unknown ticker — comes back as `{"error": {"code", "message",
105
+ "http_status"}}` carrying the API's own message, so the assistant can explain
106
+ what happened instead of crashing the call.
107
+
108
+ Webhook management is deliberately not exposed: it is account administration,
109
+ not something to do mid-conversation. Use the REST API for that.
110
+
111
+ ## Development
112
+
113
+ ```
114
+ cd mcp_server
115
+ python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
116
+ pip install -e '.[test]'
117
+ pytest tests
118
+ VANOE_API_BASE_URL=http://localhost:8000 VANOE_API_KEY=sk_live_… vanoe-intelligence-mcp
119
+ ```
120
+
121
+ Install it in its own virtualenv, separate from the API server's own
122
+ `requirements.txt`: the `mcp` SDK pulls in a newer `starlette` than the API's
123
+ FastAPI pin tolerates, and sharing one environment breaks the API server with
124
+ `Router.__init__() got an unexpected keyword argument 'on_startup'`. End users
125
+ never hit this (the MCP server runs on their machine, next to their assistant).
126
+
127
+ `tests/manual_e2e_smoke.py` runs the real server as a subprocess over stdio
128
+ against a reachable API with a real key; it is not part of `pytest`.
129
+
130
+ The MCP Python SDK is on v2: the server class is
131
+ `mcp.server.mcpserver.MCPServer` (older tutorials show `FastMCP` at a
132
+ different import path).
133
+
134
+ ## Releasing
135
+
136
+ ```
137
+ cd mcp_server
138
+ python -m build
139
+ twine upload dist/*
140
+ ```
141
+
142
+ Bump `version` in `pyproject.toml` first. The older `INTEL_API_KEY` /
143
+ `INTEL_API_BASE_URL` variable names are still accepted for compatibility.
@@ -0,0 +1,19 @@
1
+ LICENSE
2
+ README.md
3
+ __init__.py
4
+ __main__.py
5
+ client.py
6
+ pyproject.toml
7
+ server.py
8
+ ./__init__.py
9
+ ./__main__.py
10
+ ./client.py
11
+ ./server.py
12
+ tests/test_client.py
13
+ tests/test_server.py
14
+ vanoe_intelligence_mcp.egg-info/PKG-INFO
15
+ vanoe_intelligence_mcp.egg-info/SOURCES.txt
16
+ vanoe_intelligence_mcp.egg-info/dependency_links.txt
17
+ vanoe_intelligence_mcp.egg-info/entry_points.txt
18
+ vanoe_intelligence_mcp.egg-info/requires.txt
19
+ vanoe_intelligence_mcp.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ vanoe-intelligence-mcp = mcp_server.server:main
@@ -0,0 +1,6 @@
1
+ mcp<3.0,>=2.0
2
+ httpx>=0.27
3
+
4
+ [test]
5
+ pytest>=8.0
6
+ pytest-asyncio>=0.24