naragate-mcp 0.3.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.
- naragate_mcp-0.3.0/LICENSE +21 -0
- naragate_mcp-0.3.0/PKG-INFO +228 -0
- naragate_mcp-0.3.0/README.md +195 -0
- naragate_mcp-0.3.0/naragate_mcp/__init__.py +6 -0
- naragate_mcp-0.3.0/naragate_mcp/client.py +134 -0
- naragate_mcp-0.3.0/naragate_mcp/server.py +269 -0
- naragate_mcp-0.3.0/naragate_mcp.egg-info/PKG-INFO +228 -0
- naragate_mcp-0.3.0/naragate_mcp.egg-info/SOURCES.txt +15 -0
- naragate_mcp-0.3.0/naragate_mcp.egg-info/dependency_links.txt +1 -0
- naragate_mcp-0.3.0/naragate_mcp.egg-info/entry_points.txt +2 -0
- naragate_mcp-0.3.0/naragate_mcp.egg-info/requires.txt +6 -0
- naragate_mcp-0.3.0/naragate_mcp.egg-info/top_level.txt +1 -0
- naragate_mcp-0.3.0/pyproject.toml +69 -0
- naragate_mcp-0.3.0/setup.cfg +4 -0
- naragate_mcp-0.3.0/tests/test_client.py +98 -0
- naragate_mcp-0.3.0/tests/test_parity.py +41 -0
- naragate_mcp-0.3.0/tests/test_server.py +122 -0
|
@@ -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,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,195 @@
|
|
|
1
|
+
# Naragate MCP Server
|
|
2
|
+
|
|
3
|
+
One portable tool layer so the **same Naragate experience** works on the custom web UI *and* on any
|
|
4
|
+
MCP-capable agent surface — Claude Code, Claude Desktop, Cursor, Windsurf, Zed, VS Code, opencode,
|
|
5
|
+
Codex, or your own harness.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─ custom web UI (Angular)
|
|
9
|
+
Naragate backend ├─ MCP server ──► any MCP harness (desktop / CLI / IDE)
|
|
10
|
+
(FastAPI+cache) └─ Pi agent harness
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## The three layers (what a user actually needs)
|
|
14
|
+
|
|
15
|
+
| Layer | Path | Required? | Role |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| **MCP server** | `mcp/` (this package) | **Yes** for non-web | The tools: analyze, history, trend, precheck, usage |
|
|
18
|
+
| **Skills** | `skills/` | **Yes** | Domain instructions: how to parse, challenge and score a claim |
|
|
19
|
+
| **Agents** | `agents/` | No | Optional orchestration for harnesses with subagents |
|
|
20
|
+
|
|
21
|
+
Skills alone can't fetch data (they declare tools but don't ship them); agents+skills add
|
|
22
|
+
orchestration but still have no data. **The MCP bundle is the data layer** — with it, skills run
|
|
23
|
+
anywhere.
|
|
24
|
+
|
|
25
|
+
## Credit safety (hard requirement)
|
|
26
|
+
|
|
27
|
+
This server **never talks to Sectors directly**. Every tool is a thin client over the Naragate
|
|
28
|
+
backend, so evidence gathering, the Evidence Graph cache and credit accounting live in exactly one
|
|
29
|
+
place. A warm re-run costs **0 additional Sectors calls**, identical to the web UI. There is a test
|
|
30
|
+
asserting the MCP package never references the Sectors API.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uvx naragate-mcp # run without installing (recommended)
|
|
36
|
+
pip install naragate-mcp # or install into your environment
|
|
37
|
+
pipx install naragate-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
From a checkout: `pip install -e mcp`.
|
|
41
|
+
|
|
42
|
+
Point it at your backend (default `http://127.0.0.1:5678`):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
export NARAGATE_BACKEND_URL="http://127.0.0.1:5678"
|
|
46
|
+
naragate-mcp
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Transport is **stdio**.
|
|
50
|
+
|
|
51
|
+
## Tools
|
|
52
|
+
|
|
53
|
+
| Tool | What it does |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `analyze_narrative(narrative)` | Verify a narrative end-to-end → Reality Gap score, verdict, evidence sections, policy signal, skeptic summary |
|
|
56
|
+
| `analyze_template(template_id)` | Run one of the 12 curated dashboard templates by id |
|
|
57
|
+
| `list_templates()` | The 12 curated narratives (same tiles as the web dashboard) |
|
|
58
|
+
| `get_claim(claim_id)` | Full stored record (claim, evidence, skeptic, score, policy) |
|
|
59
|
+
| `get_reality_gap(claim_id)` | Compact report for a stored claim |
|
|
60
|
+
| `list_history(limit)` | Recent analyses, most recent first |
|
|
61
|
+
| `get_trend_summary()` | Totals, average score, verdict distribution, per-ticker history |
|
|
62
|
+
| `get_policy_precheck(sector)` | Policy→price pre-check, scoped to the claim's sector |
|
|
63
|
+
| `get_usage()` | Sectors/LLM credit usage, cache hits, remaining budget |
|
|
64
|
+
|
|
65
|
+
### Low-level tools — full `tools.yaml` parity
|
|
66
|
+
|
|
67
|
+
Every primitive declared in `skills/*/tools.yaml` is also exposed, for harnesses that want to compose
|
|
68
|
+
per-agent exactly like the Pi pipeline (check the cache, then fetch on a miss and merge back):
|
|
69
|
+
|
|
70
|
+
| Tool | What it does |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `sectors_company_report(ticker, sections)` | Sectors v2 company report (valuation/overview/financials) |
|
|
73
|
+
| `sectors_subsector_report(sub_sector, sections)` | Sectors v2 subsector report |
|
|
74
|
+
| `sectors_quarterly_financials(ticker, n_quarters)` | Sectors v2 quarterly financials |
|
|
75
|
+
| `sectors_daily_transaction(ticker, start, end)` | Sectors v2 daily price/volume (Sectors caps a call at 90 days) |
|
|
76
|
+
| `sectors_news(ticker, limit)` | Sectors v2 news headlines |
|
|
77
|
+
| `sectors_corporate_actions(ticker)` | Sectors v2 corporate actions |
|
|
78
|
+
| `sectors_filings(ticker, filing_type)` | Sectors v2 insider-trade filings |
|
|
79
|
+
| `evidence_cache_get(ticker)` | Read the Evidence Graph (null on miss) |
|
|
80
|
+
| `evidence_cache_merge(ticker, key, value, ttl?)` | Merge one section into the cache |
|
|
81
|
+
| `llm_complete(prompt, system?, response_format?, role?)` | One-shot completion on Naragate's configured LLM |
|
|
82
|
+
|
|
83
|
+
A test (`tests/test_parity.py`) asserts that **every** tool declared in any `skills/*/tools.yaml`
|
|
84
|
+
exists on the server, so this can't drift.
|
|
85
|
+
|
|
86
|
+
## Resources
|
|
87
|
+
|
|
88
|
+
- `naragate://templates`
|
|
89
|
+
- `naragate://usage`
|
|
90
|
+
- `naragate://history`
|
|
91
|
+
- `naragate://claim/{claim_id}`
|
|
92
|
+
|
|
93
|
+
## Harness configuration
|
|
94
|
+
|
|
95
|
+
### Claude Code
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
claude mcp add naragate -e NARAGATE_BACKEND_URL=http://127.0.0.1:5678 -- uvx naragate-mcp
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Claude Desktop (`claude_desktop_config.json`)
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"mcpServers": {
|
|
106
|
+
"naragate": {
|
|
107
|
+
"command": "uvx",
|
|
108
|
+
"args": ["naragate-mcp"],
|
|
109
|
+
"env": { "NARAGATE_BACKEND_URL": "http://127.0.0.1:5678" }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### opencode (`opencode.json`)
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mcp": {
|
|
120
|
+
"naragate": {
|
|
121
|
+
"type": "local",
|
|
122
|
+
"command": ["uvx", "naragate-mcp"],
|
|
123
|
+
"enabled": true,
|
|
124
|
+
"environment": { "NARAGATE_BACKEND_URL": "http://127.0.0.1:5678" }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Cursor (`.cursor/mcp.json`) / Windsurf / Zed / VS Code
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"naragate": {
|
|
136
|
+
"command": "uvx",
|
|
137
|
+
"args": ["naragate-mcp"],
|
|
138
|
+
"env": { "NARAGATE_BACKEND_URL": "http://127.0.0.1:5678" }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Codex (`~/.codex/config.toml`)
|
|
145
|
+
|
|
146
|
+
```toml
|
|
147
|
+
[mcp_servers.naragate]
|
|
148
|
+
command = "uvx"
|
|
149
|
+
args = ["naragate-mcp"]
|
|
150
|
+
env = { NARAGATE_BACKEND_URL = "http://127.0.0.1:5678" }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Example prompts
|
|
154
|
+
|
|
155
|
+
- *"Use naragate to verify: PE BBCA mahal di 25x."*
|
|
156
|
+
- *"List the naragate templates and run the nickel policy one."*
|
|
157
|
+
- *"Show my last 10 naragate analyses and the trend summary."*
|
|
158
|
+
- *"What's my Sectors credit usage?"*
|
|
159
|
+
|
|
160
|
+
## Tests
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
cd mcp
|
|
164
|
+
python -m pytest -q
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Covers the REST client (mock transport), the tool surface, compact-report shaping, and the
|
|
168
|
+
credit-safety guarantee. No live backend or Sectors access required.
|
|
169
|
+
|
|
170
|
+
## Publishing
|
|
171
|
+
|
|
172
|
+
Releases publish automatically via [`.github/workflows/publish-mcp.yml`](https://github.com/masdevid/naragate/blob/master/.github/workflows/publish-mcp.yml)
|
|
173
|
+
when a GitHub release is published (using the `PYPI_TOKEN` repository secret), or locally:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
mcp/publish.sh # PyPI
|
|
177
|
+
mcp/publish.sh testpypi # TestPyPI
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`publish.sh` reads `PYPI_TOKEN` from the environment or the repo-root `.env`, builds with
|
|
181
|
+
`python -m build`, validates with `twine check`, and uploads — the token is never echoed. Manual
|
|
182
|
+
equivalent:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
python -m build
|
|
186
|
+
python -m twine check dist/*
|
|
187
|
+
TWINE_USERNAME=__token__ TWINE_PASSWORD="$PYPI_TOKEN" python -m twine upload dist/*
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Related
|
|
191
|
+
|
|
192
|
+
- [Naragate README](https://github.com/masdevid/naragate#readme) — project overview and the "Use Naragate from any MCP agent" section
|
|
193
|
+
- [Skills](https://github.com/masdevid/naragate/blob/master/skills/README.md) — the 13 skills whose `tools.yaml` this server satisfies
|
|
194
|
+
- [Agents](https://github.com/masdevid/naragate/blob/master/agents/README.md) — optional subagent topology for harnesses that support it
|
|
195
|
+
- [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,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)
|