naragate-mcp 0.3.0__py3-none-any.whl

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,6 @@
1
+ """Naragate MCP server package."""
2
+
3
+ from naragate_mcp.client import NaragateClient, NaragateError
4
+
5
+ __all__ = ["NaragateClient", "NaragateError"]
6
+ __version__ = "0.3.0"
naragate_mcp/client.py ADDED
@@ -0,0 +1,134 @@
1
+ """Thin REST client for the Naragate backend.
2
+
3
+ Every non-web surface (MCP tools, scripts) goes through the backend so that
4
+ evidence caching and credit accounting live in exactly one place. Configure the
5
+ backend with `NARAGATE_BACKEND_URL` (default `http://127.0.0.1:5678`).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from typing import Any
12
+
13
+ import httpx
14
+
15
+ DEFAULT_BASE_URL = "http://127.0.0.1:5678"
16
+
17
+
18
+ class NaragateError(RuntimeError):
19
+ """Raised when the Naragate backend is unreachable or returns an error."""
20
+
21
+ def __init__(self, message: str, status: int | None = None, detail: Any = None):
22
+ super().__init__(message)
23
+ self.status = status
24
+ self.detail = detail
25
+
26
+
27
+ class NaragateClient:
28
+ def __init__(self, base_url: str | None = None, timeout: float = 600.0, transport: httpx.BaseTransport | None = None):
29
+ self.base_url = (
30
+ base_url or os.environ.get("NARAGATE_BACKEND_URL") or DEFAULT_BASE_URL
31
+ ).rstrip("/")
32
+ self.timeout = timeout
33
+ self._transport = transport
34
+
35
+ def _request(self, method: str, path: str, **kwargs: Any) -> Any:
36
+ url = f"{self.base_url}{path}"
37
+ try:
38
+ with httpx.Client(timeout=self.timeout, transport=self._transport) as client:
39
+ resp = client.request(method, url, **kwargs)
40
+ except httpx.HTTPError as exc: # network / timeout
41
+ raise NaragateError(
42
+ f"cannot reach Naragate backend at {self.base_url}: {exc}"
43
+ ) from exc
44
+
45
+ if resp.status_code >= 400:
46
+ detail: Any
47
+ try:
48
+ detail = resp.json().get("detail")
49
+ except Exception: # noqa: BLE001 — non-JSON error body
50
+ detail = resp.text
51
+ raise NaragateError(
52
+ f"{method} {path} -> HTTP {resp.status_code}: {detail}",
53
+ status=resp.status_code,
54
+ detail=detail,
55
+ )
56
+ return resp.json()
57
+
58
+ # ---- high-level, credit-safe operations (all reuse the backend pipeline) ----
59
+
60
+ def analyze(self, narrative: str) -> dict[str, Any]:
61
+ return self._request("POST", "/api/v1/analyze", json={"narrative": narrative})
62
+
63
+ def get_claim(self, claim_id: str) -> dict[str, Any]:
64
+ return self._request("GET", f"/api/v1/claims/{claim_id}")
65
+
66
+ def list_history(self, limit: int = 20) -> list[dict[str, Any]]:
67
+ return self._request("GET", "/api/v1/claims/", params={"limit": limit})
68
+
69
+ def get_summary(self) -> dict[str, Any]:
70
+ return self._request("GET", "/api/v1/claims/summary")
71
+
72
+ def get_precheck(self, sector: str | None = None) -> dict[str, Any]:
73
+ params = {"sector": sector} if sector else None
74
+ return self._request("GET", "/api/v1/precheck/", params=params)
75
+
76
+ def list_templates(self) -> dict[str, Any]:
77
+ return self._request("GET", "/api/v1/templates")
78
+
79
+ def get_usage(self) -> dict[str, Any]:
80
+ return self._request("GET", "/api/v1/usage")
81
+
82
+ # ---- low-level agent tools (parity with skills/*/tools.yaml) ----
83
+
84
+ def sectors_company_report(self, ticker: str, sections: list[str] | None = None) -> dict[str, Any]:
85
+ params = {"ticker": ticker, "sections": ",".join(sections)} if sections else {"ticker": ticker}
86
+ return self._request("GET", "/api/v1/tools/sectors/company-report", params=params)
87
+
88
+ def sectors_subsector_report(self, sub_sector: str, sections: list[str] | None = None) -> dict[str, Any]:
89
+ params = {"sub_sector": sub_sector}
90
+ if sections:
91
+ params["sections"] = ",".join(sections)
92
+ return self._request("GET", "/api/v1/tools/sectors/subsector-report", params=params)
93
+
94
+ def sectors_quarterly_financials(self, ticker: str, n_quarters: int = 8) -> list[dict[str, Any]]:
95
+ return self._request("GET", "/api/v1/tools/sectors/quarterly-financials",
96
+ params={"ticker": ticker, "n_quarters": n_quarters})
97
+
98
+ def sectors_daily_transaction(self, ticker: str, start: str | None = None, end: str | None = None) -> list[dict[str, Any]]:
99
+ params: dict[str, Any] = {"ticker": ticker}
100
+ if start:
101
+ params["start"] = start
102
+ if end:
103
+ params["end"] = end
104
+ return self._request("GET", "/api/v1/tools/sectors/daily-transaction", params=params)
105
+
106
+ def sectors_news(self, ticker: str, limit: int = 20) -> dict[str, Any]:
107
+ return self._request("GET", "/api/v1/tools/sectors/news", params={"ticker": ticker, "limit": limit})
108
+
109
+ def sectors_corporate_actions(self, ticker: str) -> dict[str, Any]:
110
+ return self._request("GET", "/api/v1/tools/sectors/corporate-actions", params={"ticker": ticker})
111
+
112
+ def sectors_filings(self, ticker: str, filing_type: str | None = None) -> dict[str, Any]:
113
+ params: dict[str, Any] = {"ticker": ticker}
114
+ if filing_type:
115
+ params["filing_type"] = filing_type
116
+ return self._request("GET", "/api/v1/tools/sectors/filings", params=params)
117
+
118
+ def evidence_cache_get(self, ticker: str) -> dict[str, Any]:
119
+ return self._request("GET", "/api/v1/tools/evidence-cache", params={"ticker": ticker})
120
+
121
+ def evidence_cache_merge(self, ticker: str, key: str, value: Any, ttl: int | None = None) -> dict[str, Any]:
122
+ body: dict[str, Any] = {"ticker": ticker, "key": key, "value": value}
123
+ if ttl:
124
+ body["ttl"] = ttl
125
+ return self._request("POST", "/api/v1/tools/evidence-cache", json=body)
126
+
127
+ def llm_complete(self, prompt: str, system: str | None = None,
128
+ response_format: dict | None = None, role: str = "default") -> dict[str, Any]:
129
+ body: dict[str, Any] = {"prompt": prompt, "role": role}
130
+ if system:
131
+ body["system"] = system
132
+ if response_format:
133
+ body["response_format"] = response_format
134
+ return self._request("POST", "/api/v1/tools/llm-complete", json=body)
naragate_mcp/server.py ADDED
@@ -0,0 +1,269 @@
1
+ """Naragate MCP server.
2
+
3
+ Exposes the Naragate reality-gap engine over the Model Context Protocol so the
4
+ same skills package works on any MCP-capable harness (Claude Code, Claude
5
+ Desktop, Cursor, Windsurf, Zed, VS Code, opencode, Codex, ...).
6
+
7
+ Design: this server is a thin, credit-safe client over the Naragate backend's
8
+ REST API. It never talks to Sectors directly — all evidence gathering, caching
9
+ and credit accounting happen in the backend pipeline, so a non-web run costs
10
+ exactly the same as a web-UI run (0 additional Sectors calls on a warm cache).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from datetime import datetime, timezone
17
+ from typing import Any
18
+
19
+ from mcp.server.fastmcp import FastMCP
20
+
21
+ from naragate_mcp.client import NaragateClient, NaragateError
22
+
23
+ mcp = FastMCP("naragate")
24
+
25
+ _EVIDENCE_KEYS = ("valuation", "fundamental", "market", "news", "corporate_actions", "filings")
26
+
27
+
28
+ def _now() -> str:
29
+ return datetime.now(timezone.utc).isoformat()
30
+
31
+
32
+ def _compact(state: dict[str, Any]) -> dict[str, Any]:
33
+ """Shape a finished claim into a compact, agent-friendly report."""
34
+ claim = state.get("claim") or {}
35
+ score = state.get("score") or {}
36
+ skeptic = state.get("skeptic") or {}
37
+ evidence = state.get("evidence") or {}
38
+ policy = evidence.get("policy") or {}
39
+
40
+ return {
41
+ "claim_id": state.get("claim_id"),
42
+ "narrative": state.get("narrative"),
43
+ "status": state.get("status"),
44
+ "clarification": state.get("clarification"),
45
+ "ticker": claim.get("ticker"),
46
+ "category": claim.get("category"),
47
+ "direction": claim.get("direction"),
48
+ "assertion": claim.get("assertion"),
49
+ "is_policy": claim.get("is_policy"),
50
+ "sector": claim.get("sector"),
51
+ "sector_members": claim.get("sector_members"),
52
+ "reality_gap_score": score.get("reality_gap_score"),
53
+ "verdict": score.get("verdict"),
54
+ "explanation": score.get("explanation"),
55
+ "dimensions": score.get("dimensions"),
56
+ "evidence_sections": [k for k in _EVIDENCE_KEYS if evidence.get(k)],
57
+ "policy_events": (policy.get("policy_events") or []) if claim.get("is_policy") else [],
58
+ "skeptic": {
59
+ "score": skeptic.get("skepticism_score"),
60
+ "counter_arguments": len(skeptic.get("counter_arguments") or []),
61
+ "ambiguity_points": len(skeptic.get("ambiguity_points") or []),
62
+ "missing_evidence": len(skeptic.get("missing_evidence") or []),
63
+ },
64
+ "url": f"https://naragate.ilkomers.com/results/{state.get('claim_id')}",
65
+ }
66
+
67
+
68
+ # --------------------------------------------------------------------- tools --
69
+
70
+ @mcp.tool()
71
+ def analyze_narrative(narrative: str) -> dict[str, Any]:
72
+ """Verify an Indonesian market narrative end-to-end and return the Reality Gap report.
73
+
74
+ Runs the full Naragate pipeline (claim parsing, evidence, skeptic, judge,
75
+ score) against real Sectors v2 data. Returns the score (0-100), verdict,
76
+ direction, evidence sections, policy signal and skeptic summary.
77
+
78
+ Args:
79
+ narrative: the Indonesian market claim to verify, e.g.
80
+ "PE BBCA mahal di 25x, jauh di atas rata-rata sektor 18x."
81
+ """
82
+ state = NaragateClient().analyze(narrative)
83
+ if state.get("status") == "needs_clarification":
84
+ return {
85
+ "claim_id": state.get("claim_id"),
86
+ "narrative": state.get("narrative"),
87
+ "status": "needs_clarification",
88
+ "clarification": state.get("message"),
89
+ "hint": "Name a specific 4-letter IDX ticker so the claim can be verified.",
90
+ }
91
+ return _compact(state)
92
+
93
+
94
+ @mcp.tool()
95
+ def analyze_template(template_id: str) -> dict[str, Any]:
96
+ """Verify one of the 12 curated demo templates by id (same tiles as the web dashboard).
97
+
98
+ Use `list_templates` to discover ids. Example ids: `valuation`, `market`,
99
+ `policy_bbm`, `policy_hba`, `policy_nickel`, `contradiction`, `no_ticker`.
100
+ """
101
+ templates = NaragateClient().list_templates().get("templates", [])
102
+ match = next((t for t in templates if t.get("id") == template_id), None)
103
+ if match is None:
104
+ return {"error": f"unknown template_id '{template_id}'", "available": [t["id"] for t in templates]}
105
+ return analyze_narrative(match["narrative"])
106
+
107
+
108
+ @mcp.tool()
109
+ def list_templates() -> dict[str, Any]:
110
+ """List the 12 curated demo narratives (the dashboard tiles) so non-web users get the same entry points."""
111
+ return NaragateClient().list_templates()
112
+
113
+
114
+ @mcp.tool()
115
+ def get_claim(claim_id: str) -> dict[str, Any]:
116
+ """Fetch the full stored record for a claim (claim, evidence, skeptic, score, policy)."""
117
+ return NaragateClient().get_claim(claim_id)
118
+
119
+
120
+ @mcp.tool()
121
+ def get_reality_gap(claim_id: str) -> dict[str, Any]:
122
+ """Fetch a compact Reality Gap report for a stored claim by id."""
123
+ return _compact(NaragateClient().get_claim(claim_id))
124
+
125
+
126
+ @mcp.tool()
127
+ def list_history(limit: int = 20) -> dict[str, Any]:
128
+ """List recent analyses, most recent first (the web UI's History page)."""
129
+ claims = NaragateClient().list_history(limit=limit)
130
+ rows = [
131
+ {
132
+ "claim_id": c.get("claim_id"),
133
+ "narrative": c.get("narrative"),
134
+ "status": c.get("status"),
135
+ "ticker": (c.get("claim") or {}).get("ticker"),
136
+ "created_at": c.get("created_at"),
137
+ "reality_gap_score": (c.get("score") or {}).get("reality_gap_score"),
138
+ "verdict": (c.get("score") or {}).get("verdict"),
139
+ }
140
+ for c in claims
141
+ ]
142
+ return {"count": len(rows), "claims": rows}
143
+
144
+
145
+ @mcp.tool()
146
+ def get_trend_summary() -> dict[str, Any]:
147
+ """Aggregate trends across completed analyses: totals, average score, verdict distribution, per-ticker history."""
148
+ return NaragateClient().get_summary()
149
+
150
+
151
+ @mcp.tool()
152
+ def get_policy_precheck(sector: str | None = None) -> dict[str, Any]:
153
+ """Policy-to-price pre-check (T1). Scoped to the claim's sector to avoid cross-sector leakage.
154
+
155
+ Args:
156
+ sector: optional sector slug, e.g. `oil-gas`, `coal`, `nickel`.
157
+ """
158
+ return NaragateClient().get_precheck(sector=sector)
159
+
160
+
161
+ @mcp.tool()
162
+ def get_usage() -> dict[str, Any]:
163
+ """Sectors API and LLM credit usage: totals, cache hits, remaining budget, daily breakdown."""
164
+ return NaragateClient().get_usage()
165
+
166
+
167
+ # ---------------------------------------------- low-level tools (tools.yaml parity) --
168
+ # These mirror skills/*/tools.yaml exactly, for harnesses that want to compose
169
+ # per-agent. Credit discipline: check `evidence_cache_get` first, then call the
170
+ # `sectors_*` tool on a miss and `evidence_cache_merge` the result.
171
+
172
+
173
+ @mcp.tool()
174
+ def sectors_company_report(ticker: str, sections: list[str] | None = None) -> dict[str, Any]:
175
+ """Sectors v2 company report (sections: valuation, overview, financials)."""
176
+ return NaragateClient().sectors_company_report(ticker, sections)
177
+
178
+
179
+ @mcp.tool()
180
+ def sectors_subsector_report(sub_sector: str, sections: list[str] | None = None) -> dict[str, Any]:
181
+ """Sectors v2 subsector report (sections: statistics, valuation, ...)."""
182
+ return NaragateClient().sectors_subsector_report(sub_sector, sections)
183
+
184
+
185
+ @mcp.tool()
186
+ def sectors_quarterly_financials(ticker: str, n_quarters: int = 8) -> list[dict[str, Any]]:
187
+ """Sectors v2 quarterly financials (revenue, earnings, margins) for a ticker."""
188
+ return NaragateClient().sectors_quarterly_financials(ticker, n_quarters)
189
+
190
+
191
+ @mcp.tool()
192
+ def sectors_daily_transaction(ticker: str, start: str | None = None, end: str | None = None) -> list[dict[str, Any]]:
193
+ """Sectors v2 daily transaction data (price, volume, close). Sectors caps a call at 90 days."""
194
+ return NaragateClient().sectors_daily_transaction(ticker, start, end)
195
+
196
+
197
+ @mcp.tool()
198
+ def sectors_news(ticker: str, limit: int = 20) -> dict[str, Any]:
199
+ """Sectors v2 recent news headlines for a ticker."""
200
+ return NaragateClient().sectors_news(ticker, limit)
201
+
202
+
203
+ @mcp.tool()
204
+ def sectors_corporate_actions(ticker: str) -> dict[str, Any]:
205
+ """Sectors v2 corporate actions (dividends, splits, warrants) for a ticker."""
206
+ return NaragateClient().sectors_corporate_actions(ticker)
207
+
208
+
209
+ @mcp.tool()
210
+ def sectors_filings(ticker: str, filing_type: str | None = None) -> dict[str, Any]:
211
+ """Sectors v2 insider-trade filings for a ticker (filing_type: buy, sell, others)."""
212
+ return NaragateClient().sectors_filings(ticker, filing_type)
213
+
214
+
215
+ @mcp.tool()
216
+ def evidence_cache_get(ticker: str) -> dict[str, Any]:
217
+ """Read the full Evidence Graph for a ticker from the cache (null on miss)."""
218
+ return NaragateClient().evidence_cache_get(ticker)
219
+
220
+
221
+ @mcp.tool()
222
+ def evidence_cache_merge(ticker: str, key: str, value: Any, ttl: int | None = None) -> dict[str, Any]:
223
+ """Atomically merge one section into the cached Evidence Graph for a ticker."""
224
+ return NaragateClient().evidence_cache_merge(ticker, key, value, ttl)
225
+
226
+
227
+ @mcp.tool()
228
+ def llm_complete(prompt: str, system: str | None = None,
229
+ response_format: dict | None = None, role: str = "default") -> dict[str, Any]:
230
+ """Run a one-shot completion on Naragate's configured LLM and return the raw text."""
231
+ return NaragateClient().llm_complete(prompt, system, response_format, role)
232
+
233
+
234
+ # ----------------------------------------------------------------- resources --
235
+
236
+ @mcp.resource("naragate://templates")
237
+ def templates_resource() -> str:
238
+ """The 12 curated demo templates as JSON."""
239
+ return json.dumps(NaragateClient().list_templates(), ensure_ascii=False, indent=2)
240
+
241
+
242
+ @mcp.resource("naragate://usage")
243
+ def usage_resource() -> str:
244
+ """Current Sectors/LLM credit usage as JSON."""
245
+ return json.dumps(NaragateClient().get_usage(), ensure_ascii=False, indent=2)
246
+
247
+
248
+ @mcp.resource("naragate://history")
249
+ def history_resource() -> str:
250
+ """Recent analyses as JSON."""
251
+ return json.dumps(NaragateClient().list_history(limit=20), ensure_ascii=False, indent=2)
252
+
253
+
254
+ @mcp.resource("naragate://claim/{claim_id}")
255
+ def claim_resource(claim_id: str) -> str:
256
+ """A stored claim's compact Reality Gap report as JSON."""
257
+ try:
258
+ return json.dumps(_compact(NaragateClient().get_claim(claim_id)), ensure_ascii=False, indent=2)
259
+ except NaragateError as exc:
260
+ return json.dumps({"error": str(exc)}, ensure_ascii=False)
261
+
262
+
263
+ def main() -> None:
264
+ """Entry point for the `naragate-mcp` console script (stdio transport)."""
265
+ mcp.run()
266
+
267
+
268
+ if __name__ == "__main__":
269
+ main()
@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.4
2
+ Name: naragate-mcp
3
+ Version: 0.3.0
4
+ Summary: MCP server exposing the Naragate reality-gap evidence engine to any MCP-capable agent (desktop, CLI, IDE).
5
+ Author-email: Devid HW <devid.wahid@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/masdevid/naragate
8
+ Project-URL: Repository, https://github.com/masdevid/naragate
9
+ Project-URL: Issues, https://github.com/masdevid/naragate/issues
10
+ Project-URL: Changelog, https://github.com/masdevid/naragate/releases
11
+ Project-URL: Documentation, https://github.com/masdevid/naragate/blob/master/mcp/README.md
12
+ Keywords: mcp,model-context-protocol,ai-agents,claude,cursor,opencode,codex,stock,indonesia,idx,sectors,fact-check
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Financial and Insurance Industry
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Office/Business :: Financial :: Investment
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: mcp<2,>=1.2
28
+ Requires-Dist: httpx>=0.27
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest>=8.0.0; extra == "test"
31
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "test"
32
+ Dynamic: license-file
33
+
34
+ # Naragate MCP Server
35
+
36
+ One portable tool layer so the **same Naragate experience** works on the custom web UI *and* on any
37
+ MCP-capable agent surface — Claude Code, Claude Desktop, Cursor, Windsurf, Zed, VS Code, opencode,
38
+ Codex, or your own harness.
39
+
40
+ ```
41
+ ┌─ custom web UI (Angular)
42
+ Naragate backend ├─ MCP server ──► any MCP harness (desktop / CLI / IDE)
43
+ (FastAPI+cache) └─ Pi agent harness
44
+ ```
45
+
46
+ ## The three layers (what a user actually needs)
47
+
48
+ | Layer | Path | Required? | Role |
49
+ |---|---|---|---|
50
+ | **MCP server** | `mcp/` (this package) | **Yes** for non-web | The tools: analyze, history, trend, precheck, usage |
51
+ | **Skills** | `skills/` | **Yes** | Domain instructions: how to parse, challenge and score a claim |
52
+ | **Agents** | `agents/` | No | Optional orchestration for harnesses with subagents |
53
+
54
+ Skills alone can't fetch data (they declare tools but don't ship them); agents+skills add
55
+ orchestration but still have no data. **The MCP bundle is the data layer** — with it, skills run
56
+ anywhere.
57
+
58
+ ## Credit safety (hard requirement)
59
+
60
+ This server **never talks to Sectors directly**. Every tool is a thin client over the Naragate
61
+ backend, so evidence gathering, the Evidence Graph cache and credit accounting live in exactly one
62
+ place. A warm re-run costs **0 additional Sectors calls**, identical to the web UI. There is a test
63
+ asserting the MCP package never references the Sectors API.
64
+
65
+ ## Install
66
+
67
+ ```bash
68
+ uvx naragate-mcp # run without installing (recommended)
69
+ pip install naragate-mcp # or install into your environment
70
+ pipx install naragate-mcp
71
+ ```
72
+
73
+ From a checkout: `pip install -e mcp`.
74
+
75
+ Point it at your backend (default `http://127.0.0.1:5678`):
76
+
77
+ ```bash
78
+ export NARAGATE_BACKEND_URL="http://127.0.0.1:5678"
79
+ naragate-mcp
80
+ ```
81
+
82
+ Transport is **stdio**.
83
+
84
+ ## Tools
85
+
86
+ | Tool | What it does |
87
+ |---|---|
88
+ | `analyze_narrative(narrative)` | Verify a narrative end-to-end → Reality Gap score, verdict, evidence sections, policy signal, skeptic summary |
89
+ | `analyze_template(template_id)` | Run one of the 12 curated dashboard templates by id |
90
+ | `list_templates()` | The 12 curated narratives (same tiles as the web dashboard) |
91
+ | `get_claim(claim_id)` | Full stored record (claim, evidence, skeptic, score, policy) |
92
+ | `get_reality_gap(claim_id)` | Compact report for a stored claim |
93
+ | `list_history(limit)` | Recent analyses, most recent first |
94
+ | `get_trend_summary()` | Totals, average score, verdict distribution, per-ticker history |
95
+ | `get_policy_precheck(sector)` | Policy→price pre-check, scoped to the claim's sector |
96
+ | `get_usage()` | Sectors/LLM credit usage, cache hits, remaining budget |
97
+
98
+ ### Low-level tools — full `tools.yaml` parity
99
+
100
+ Every primitive declared in `skills/*/tools.yaml` is also exposed, for harnesses that want to compose
101
+ per-agent exactly like the Pi pipeline (check the cache, then fetch on a miss and merge back):
102
+
103
+ | Tool | What it does |
104
+ |---|---|
105
+ | `sectors_company_report(ticker, sections)` | Sectors v2 company report (valuation/overview/financials) |
106
+ | `sectors_subsector_report(sub_sector, sections)` | Sectors v2 subsector report |
107
+ | `sectors_quarterly_financials(ticker, n_quarters)` | Sectors v2 quarterly financials |
108
+ | `sectors_daily_transaction(ticker, start, end)` | Sectors v2 daily price/volume (Sectors caps a call at 90 days) |
109
+ | `sectors_news(ticker, limit)` | Sectors v2 news headlines |
110
+ | `sectors_corporate_actions(ticker)` | Sectors v2 corporate actions |
111
+ | `sectors_filings(ticker, filing_type)` | Sectors v2 insider-trade filings |
112
+ | `evidence_cache_get(ticker)` | Read the Evidence Graph (null on miss) |
113
+ | `evidence_cache_merge(ticker, key, value, ttl?)` | Merge one section into the cache |
114
+ | `llm_complete(prompt, system?, response_format?, role?)` | One-shot completion on Naragate's configured LLM |
115
+
116
+ A test (`tests/test_parity.py`) asserts that **every** tool declared in any `skills/*/tools.yaml`
117
+ exists on the server, so this can't drift.
118
+
119
+ ## Resources
120
+
121
+ - `naragate://templates`
122
+ - `naragate://usage`
123
+ - `naragate://history`
124
+ - `naragate://claim/{claim_id}`
125
+
126
+ ## Harness configuration
127
+
128
+ ### Claude Code
129
+
130
+ ```bash
131
+ claude mcp add naragate -e NARAGATE_BACKEND_URL=http://127.0.0.1:5678 -- uvx naragate-mcp
132
+ ```
133
+
134
+ ### Claude Desktop (`claude_desktop_config.json`)
135
+
136
+ ```json
137
+ {
138
+ "mcpServers": {
139
+ "naragate": {
140
+ "command": "uvx",
141
+ "args": ["naragate-mcp"],
142
+ "env": { "NARAGATE_BACKEND_URL": "http://127.0.0.1:5678" }
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ ### opencode (`opencode.json`)
149
+
150
+ ```json
151
+ {
152
+ "mcp": {
153
+ "naragate": {
154
+ "type": "local",
155
+ "command": ["uvx", "naragate-mcp"],
156
+ "enabled": true,
157
+ "environment": { "NARAGATE_BACKEND_URL": "http://127.0.0.1:5678" }
158
+ }
159
+ }
160
+ }
161
+ ```
162
+
163
+ ### Cursor (`.cursor/mcp.json`) / Windsurf / Zed / VS Code
164
+
165
+ ```json
166
+ {
167
+ "mcpServers": {
168
+ "naragate": {
169
+ "command": "uvx",
170
+ "args": ["naragate-mcp"],
171
+ "env": { "NARAGATE_BACKEND_URL": "http://127.0.0.1:5678" }
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
177
+ ### Codex (`~/.codex/config.toml`)
178
+
179
+ ```toml
180
+ [mcp_servers.naragate]
181
+ command = "uvx"
182
+ args = ["naragate-mcp"]
183
+ env = { NARAGATE_BACKEND_URL = "http://127.0.0.1:5678" }
184
+ ```
185
+
186
+ ## Example prompts
187
+
188
+ - *"Use naragate to verify: PE BBCA mahal di 25x."*
189
+ - *"List the naragate templates and run the nickel policy one."*
190
+ - *"Show my last 10 naragate analyses and the trend summary."*
191
+ - *"What's my Sectors credit usage?"*
192
+
193
+ ## Tests
194
+
195
+ ```bash
196
+ cd mcp
197
+ python -m pytest -q
198
+ ```
199
+
200
+ Covers the REST client (mock transport), the tool surface, compact-report shaping, and the
201
+ credit-safety guarantee. No live backend or Sectors access required.
202
+
203
+ ## Publishing
204
+
205
+ Releases publish automatically via [`.github/workflows/publish-mcp.yml`](https://github.com/masdevid/naragate/blob/master/.github/workflows/publish-mcp.yml)
206
+ when a GitHub release is published (using the `PYPI_TOKEN` repository secret), or locally:
207
+
208
+ ```bash
209
+ mcp/publish.sh # PyPI
210
+ mcp/publish.sh testpypi # TestPyPI
211
+ ```
212
+
213
+ `publish.sh` reads `PYPI_TOKEN` from the environment or the repo-root `.env`, builds with
214
+ `python -m build`, validates with `twine check`, and uploads — the token is never echoed. Manual
215
+ equivalent:
216
+
217
+ ```bash
218
+ python -m build
219
+ python -m twine check dist/*
220
+ TWINE_USERNAME=__token__ TWINE_PASSWORD="$PYPI_TOKEN" python -m twine upload dist/*
221
+ ```
222
+
223
+ ## Related
224
+
225
+ - [Naragate README](https://github.com/masdevid/naragate#readme) — project overview and the "Use Naragate from any MCP agent" section
226
+ - [Skills](https://github.com/masdevid/naragate/blob/master/skills/README.md) — the 13 skills whose `tools.yaml` this server satisfies
227
+ - [Agents](https://github.com/masdevid/naragate/blob/master/agents/README.md) — optional subagent topology for harnesses that support it
228
+ - [Parity test](https://github.com/masdevid/naragate/blob/master/mcp/tests/test_parity.py) — enforces that every `skills/*/tools.yaml` tool is exposed here
@@ -0,0 +1,9 @@
1
+ naragate_mcp/__init__.py,sha256=cLukElhuV1w-zwMhXbEtYYENGW8nrNgdHANppDj1_hE,167
2
+ naragate_mcp/client.py,sha256=g5a6pV5UaD7UHNauvCxYi1mGE5VUP5Y7diHaRceGc4E,5913
3
+ naragate_mcp/server.py,sha256=4QmJKZGFTdgfyFZtr9VS8_luruf7sjMsG7EDREUZPvc,10443
4
+ naragate_mcp-0.3.0.dist-info/licenses/LICENSE,sha256=cEZ6Etdzgq1eRitg54aCE-FQXJrw-Q4jVAL8EaeJLB4,1065
5
+ naragate_mcp-0.3.0.dist-info/METADATA,sha256=j2OVJwENzTbARqn9vpUoevd-y9fh8tE7UnWP8xx-X7s,8439
6
+ naragate_mcp-0.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
7
+ naragate_mcp-0.3.0.dist-info/entry_points.txt,sha256=sB8xSAwZvFr6PgtPfza2PXlRfM_bRR7HcRJTZArMiaA,58
8
+ naragate_mcp-0.3.0.dist-info/top_level.txt,sha256=NInI4wfT2Xlohju5d-NFyABlJvxPX0edYg6Jpjh892o,13
9
+ naragate_mcp-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ naragate-mcp = naragate_mcp.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Devid HW
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 @@
1
+ naragate_mcp