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.
- naragate_mcp/__init__.py +6 -0
- naragate_mcp/client.py +134 -0
- naragate_mcp/server.py +269 -0
- naragate_mcp-0.3.0.dist-info/METADATA +228 -0
- naragate_mcp-0.3.0.dist-info/RECORD +9 -0
- naragate_mcp-0.3.0.dist-info/WHEEL +5 -0
- naragate_mcp-0.3.0.dist-info/entry_points.txt +2 -0
- naragate_mcp-0.3.0.dist-info/licenses/LICENSE +21 -0
- naragate_mcp-0.3.0.dist-info/top_level.txt +1 -0
naragate_mcp/__init__.py
ADDED
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,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
|