getsonar-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.
- getsonar_mcp-0.1.0/.gitignore +33 -0
- getsonar_mcp-0.1.0/LICENSE +21 -0
- getsonar_mcp-0.1.0/PKG-INFO +186 -0
- getsonar_mcp-0.1.0/README.md +160 -0
- getsonar_mcp-0.1.0/pyproject.toml +65 -0
- getsonar_mcp-0.1.0/src/getsonar_mcp/__init__.py +20 -0
- getsonar_mcp-0.1.0/src/getsonar_mcp/__main__.py +53 -0
- getsonar_mcp-0.1.0/src/getsonar_mcp/client.py +213 -0
- getsonar_mcp-0.1.0/src/getsonar_mcp/http_server.py +61 -0
- getsonar_mcp-0.1.0/src/getsonar_mcp/tools.py +261 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
.Python
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
env/
|
|
11
|
+
|
|
12
|
+
# Environment — NEVER commit these
|
|
13
|
+
.env
|
|
14
|
+
backend/.env
|
|
15
|
+
|
|
16
|
+
# Frontend
|
|
17
|
+
frontend/node_modules/
|
|
18
|
+
frontend/dist/
|
|
19
|
+
frontend/.env
|
|
20
|
+
|
|
21
|
+
# macOS
|
|
22
|
+
.DS_Store
|
|
23
|
+
|
|
24
|
+
# Logs
|
|
25
|
+
*.log
|
|
26
|
+
Logs/
|
|
27
|
+
BG.mp4
|
|
28
|
+
BG.png
|
|
29
|
+
BG1 .mp4
|
|
30
|
+
*.gif
|
|
31
|
+
|
|
32
|
+
# Wave 2 validation venv — local only
|
|
33
|
+
backend/.venv-test/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SONAR
|
|
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,186 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: getsonar-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for SONAR — competitive intelligence, KYB due diligence, and adverse media screening for AI agents.
|
|
5
|
+
Project-URL: Homepage, https://getsonar.report
|
|
6
|
+
Project-URL: Documentation, https://getsonar.report/developers
|
|
7
|
+
Project-URL: API Reference, https://api.getsonar.report/docs
|
|
8
|
+
Project-URL: Support, https://getsonar.report/api-keys
|
|
9
|
+
Author-email: SONAR <ivan.vakulenko@wildix.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: adverse-media,anthropic,claude,competitive-intelligence,compliance,kyb,mcp,model-context-protocol,osint
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
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: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: mcp>=1.0.0
|
|
23
|
+
Provides-Extra: http
|
|
24
|
+
Requires-Dist: uvicorn>=0.30.0; extra == 'http'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# getsonar-mcp
|
|
28
|
+
|
|
29
|
+
MCP server for [SONAR](https://getsonar.report) — competitive intelligence, KYB due diligence, and adverse media screening, exposed as tools any MCP-compatible AI client can call.
|
|
30
|
+
|
|
31
|
+
Works with **Claude Desktop**, **Claude Code**, **Cursor**, **Windsurf**, **Continue.dev**, and any other [Model Context Protocol](https://modelcontextprotocol.io) client.
|
|
32
|
+
|
|
33
|
+
## What it does
|
|
34
|
+
|
|
35
|
+
Adds 7 tools to your AI assistant:
|
|
36
|
+
|
|
37
|
+
| Tool | What it does | Cost (after free tier) |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `kyb_findings` | Quick KYB lookup — corporate registration, sanctions, FCA, directors, UBOs | $0.40 |
|
|
40
|
+
| `kyb_run_submit` | Full KYB report (async, with Claude analysis) | $2.00 |
|
|
41
|
+
| `competitive_findings` | Quick competitor intel — pricing, hiring, product, sentiment | $0.40 |
|
|
42
|
+
| `competitive_run_submit` | Full CI report (async, with Claude analysis) | $2.00 |
|
|
43
|
+
| `adverse_media` | Negative news + complaints + bad-review screening | $0.40 |
|
|
44
|
+
| `check_report_status` | Poll the result of an async `*_run_submit` call | free |
|
|
45
|
+
| `account_balance` | Check remaining free-tier calls + usage | free |
|
|
46
|
+
|
|
47
|
+
**Free tier: 100 calls/month** per API key. Beyond that, balance-deducted at the prices above.
|
|
48
|
+
|
|
49
|
+
## Get an API key
|
|
50
|
+
|
|
51
|
+
1. Sign up at [getsonar.report](https://getsonar.report)
|
|
52
|
+
2. Go to **API Keys** in the sidebar
|
|
53
|
+
3. Click **Create API Key**, copy the `sk-sonar-...` key
|
|
54
|
+
4. (Optional) Top up your account on the same page if you'll exceed 100 calls/month
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
### Recommended: `uvx` (no install needed)
|
|
59
|
+
|
|
60
|
+
If you have [`uv`](https://docs.astral.sh/uv/) installed (most modern Python setups), nothing to install — `uvx getsonar-mcp` runs the latest version on demand.
|
|
61
|
+
|
|
62
|
+
### Alternative: `pipx`
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pipx install getsonar-mcp
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configure your MCP client
|
|
69
|
+
|
|
70
|
+
### Claude Desktop
|
|
71
|
+
|
|
72
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"sonar": {
|
|
78
|
+
"command": "uvx",
|
|
79
|
+
"args": ["getsonar-mcp"],
|
|
80
|
+
"env": {
|
|
81
|
+
"SONAR_API_KEY": "sk-sonar-YOUR_KEY_HERE"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Restart Claude Desktop. The 7 tools should appear in the 🔌 menu.
|
|
89
|
+
|
|
90
|
+
### Claude Code
|
|
91
|
+
|
|
92
|
+
Add to your project's `.mcp.json` or run:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
claude mcp add sonar -- uvx getsonar-mcp
|
|
96
|
+
# Then in your shell: export SONAR_API_KEY=sk-sonar-...
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Or with the env var inline:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
claude mcp add sonar --env SONAR_API_KEY=sk-sonar-... -- uvx getsonar-mcp
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Cursor
|
|
106
|
+
|
|
107
|
+
`~/.cursor/mcp.json`:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"mcpServers": {
|
|
112
|
+
"sonar": {
|
|
113
|
+
"command": "uvx",
|
|
114
|
+
"args": ["getsonar-mcp"],
|
|
115
|
+
"env": {
|
|
116
|
+
"SONAR_API_KEY": "sk-sonar-YOUR_KEY_HERE"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Windsurf / Continue.dev
|
|
124
|
+
|
|
125
|
+
Same config shape as Claude Desktop. See your client's MCP docs for the config file path.
|
|
126
|
+
|
|
127
|
+
## Configuration
|
|
128
|
+
|
|
129
|
+
Environment variables (all optional except `SONAR_API_KEY`):
|
|
130
|
+
|
|
131
|
+
| Variable | Default | Purpose |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| `SONAR_API_KEY` | (required) | Your `sk-sonar-...` API key |
|
|
134
|
+
| `SONAR_API_BASE` | `https://api.getsonar.report` | Override for staging / self-hosted SONAR |
|
|
135
|
+
| `SONAR_API_TIMEOUT` | `120` | Per-request timeout in seconds |
|
|
136
|
+
| `SONAR_MCP_LOG_LEVEL` | `INFO` | `DEBUG` for verbose tool dispatch logs |
|
|
137
|
+
|
|
138
|
+
## Example prompts
|
|
139
|
+
|
|
140
|
+
After configuring, try these in Claude:
|
|
141
|
+
|
|
142
|
+
> "Run a KYB check on Tesco PLC and tell me if anything looks risky."
|
|
143
|
+
>
|
|
144
|
+
> Claude calls `kyb_findings("Tesco PLC")` → ~30s → reads back the structured findings → highlights the FCA Register hit, any sanctions matches, director-level red flags.
|
|
145
|
+
|
|
146
|
+
> "Submit a full competitive analysis for Notion and tell me when it's done."
|
|
147
|
+
>
|
|
148
|
+
> Claude calls `competitive_run_submit("Notion")` → gets `run_id` → polls `check_report_status` every 30s → returns the narrative report.
|
|
149
|
+
|
|
150
|
+
> "Am I close to my free-tier limit?"
|
|
151
|
+
>
|
|
152
|
+
> Claude calls `account_balance()` → tells you how many of your 100 free monthly calls you've used.
|
|
153
|
+
|
|
154
|
+
## How async reports work
|
|
155
|
+
|
|
156
|
+
The `*_run_submit` tools return a `run_id` immediately (in <1s). Claude then needs to call `check_report_status(run_id, agent_type)` to fetch the result, which is normally ready in 60-180s. Most MCP clients handle this seamlessly — Claude knows from the tool descriptions to poll until status="completed".
|
|
157
|
+
|
|
158
|
+
The cheaper `*_findings` tools are synchronous and complete in 30-90s on a single call. Use those when the LLM just needs raw structured data and will do its own analysis.
|
|
159
|
+
|
|
160
|
+
## Verify your install
|
|
161
|
+
|
|
162
|
+
Use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to confirm the server starts and tool dispatch works:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
SONAR_API_KEY=sk-sonar-... npx @modelcontextprotocol/inspector uvx getsonar-mcp
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
You should see all 7 tools listed and be able to invoke them with test inputs.
|
|
169
|
+
|
|
170
|
+
## Source
|
|
171
|
+
|
|
172
|
+
`getsonar-mcp` is a thin client for the SONAR public API. The source is currently maintained privately; for issues, feature requests, or transport changes, email **ivan.vakulenko@wildix.com**.
|
|
173
|
+
|
|
174
|
+
## Troubleshooting
|
|
175
|
+
|
|
176
|
+
**Claude says "tool not available"** — check `claude_desktop_config.json` is valid JSON, restart Claude Desktop, then verify `uvx --version` runs in your terminal.
|
|
177
|
+
|
|
178
|
+
**`401 Unauthorized`** — `SONAR_API_KEY` is missing or wrong. Verify in the SONAR dashboard at [getsonar.report/api-keys](https://getsonar.report/api-keys).
|
|
179
|
+
|
|
180
|
+
**`402 Payment Required`** — your free tier is exhausted and your balance is $0. Top up at [getsonar.report/api-keys](https://getsonar.report/api-keys).
|
|
181
|
+
|
|
182
|
+
**Timeouts** — bump `SONAR_API_TIMEOUT` in your env config. The `/run` tools can take 3+ minutes for complex KYB subjects.
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# getsonar-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [SONAR](https://getsonar.report) — competitive intelligence, KYB due diligence, and adverse media screening, exposed as tools any MCP-compatible AI client can call.
|
|
4
|
+
|
|
5
|
+
Works with **Claude Desktop**, **Claude Code**, **Cursor**, **Windsurf**, **Continue.dev**, and any other [Model Context Protocol](https://modelcontextprotocol.io) client.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
Adds 7 tools to your AI assistant:
|
|
10
|
+
|
|
11
|
+
| Tool | What it does | Cost (after free tier) |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `kyb_findings` | Quick KYB lookup — corporate registration, sanctions, FCA, directors, UBOs | $0.40 |
|
|
14
|
+
| `kyb_run_submit` | Full KYB report (async, with Claude analysis) | $2.00 |
|
|
15
|
+
| `competitive_findings` | Quick competitor intel — pricing, hiring, product, sentiment | $0.40 |
|
|
16
|
+
| `competitive_run_submit` | Full CI report (async, with Claude analysis) | $2.00 |
|
|
17
|
+
| `adverse_media` | Negative news + complaints + bad-review screening | $0.40 |
|
|
18
|
+
| `check_report_status` | Poll the result of an async `*_run_submit` call | free |
|
|
19
|
+
| `account_balance` | Check remaining free-tier calls + usage | free |
|
|
20
|
+
|
|
21
|
+
**Free tier: 100 calls/month** per API key. Beyond that, balance-deducted at the prices above.
|
|
22
|
+
|
|
23
|
+
## Get an API key
|
|
24
|
+
|
|
25
|
+
1. Sign up at [getsonar.report](https://getsonar.report)
|
|
26
|
+
2. Go to **API Keys** in the sidebar
|
|
27
|
+
3. Click **Create API Key**, copy the `sk-sonar-...` key
|
|
28
|
+
4. (Optional) Top up your account on the same page if you'll exceed 100 calls/month
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
### Recommended: `uvx` (no install needed)
|
|
33
|
+
|
|
34
|
+
If you have [`uv`](https://docs.astral.sh/uv/) installed (most modern Python setups), nothing to install — `uvx getsonar-mcp` runs the latest version on demand.
|
|
35
|
+
|
|
36
|
+
### Alternative: `pipx`
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pipx install getsonar-mcp
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Configure your MCP client
|
|
43
|
+
|
|
44
|
+
### Claude Desktop
|
|
45
|
+
|
|
46
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"sonar": {
|
|
52
|
+
"command": "uvx",
|
|
53
|
+
"args": ["getsonar-mcp"],
|
|
54
|
+
"env": {
|
|
55
|
+
"SONAR_API_KEY": "sk-sonar-YOUR_KEY_HERE"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Restart Claude Desktop. The 7 tools should appear in the 🔌 menu.
|
|
63
|
+
|
|
64
|
+
### Claude Code
|
|
65
|
+
|
|
66
|
+
Add to your project's `.mcp.json` or run:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
claude mcp add sonar -- uvx getsonar-mcp
|
|
70
|
+
# Then in your shell: export SONAR_API_KEY=sk-sonar-...
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or with the env var inline:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
claude mcp add sonar --env SONAR_API_KEY=sk-sonar-... -- uvx getsonar-mcp
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Cursor
|
|
80
|
+
|
|
81
|
+
`~/.cursor/mcp.json`:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"mcpServers": {
|
|
86
|
+
"sonar": {
|
|
87
|
+
"command": "uvx",
|
|
88
|
+
"args": ["getsonar-mcp"],
|
|
89
|
+
"env": {
|
|
90
|
+
"SONAR_API_KEY": "sk-sonar-YOUR_KEY_HERE"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Windsurf / Continue.dev
|
|
98
|
+
|
|
99
|
+
Same config shape as Claude Desktop. See your client's MCP docs for the config file path.
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
Environment variables (all optional except `SONAR_API_KEY`):
|
|
104
|
+
|
|
105
|
+
| Variable | Default | Purpose |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `SONAR_API_KEY` | (required) | Your `sk-sonar-...` API key |
|
|
108
|
+
| `SONAR_API_BASE` | `https://api.getsonar.report` | Override for staging / self-hosted SONAR |
|
|
109
|
+
| `SONAR_API_TIMEOUT` | `120` | Per-request timeout in seconds |
|
|
110
|
+
| `SONAR_MCP_LOG_LEVEL` | `INFO` | `DEBUG` for verbose tool dispatch logs |
|
|
111
|
+
|
|
112
|
+
## Example prompts
|
|
113
|
+
|
|
114
|
+
After configuring, try these in Claude:
|
|
115
|
+
|
|
116
|
+
> "Run a KYB check on Tesco PLC and tell me if anything looks risky."
|
|
117
|
+
>
|
|
118
|
+
> Claude calls `kyb_findings("Tesco PLC")` → ~30s → reads back the structured findings → highlights the FCA Register hit, any sanctions matches, director-level red flags.
|
|
119
|
+
|
|
120
|
+
> "Submit a full competitive analysis for Notion and tell me when it's done."
|
|
121
|
+
>
|
|
122
|
+
> Claude calls `competitive_run_submit("Notion")` → gets `run_id` → polls `check_report_status` every 30s → returns the narrative report.
|
|
123
|
+
|
|
124
|
+
> "Am I close to my free-tier limit?"
|
|
125
|
+
>
|
|
126
|
+
> Claude calls `account_balance()` → tells you how many of your 100 free monthly calls you've used.
|
|
127
|
+
|
|
128
|
+
## How async reports work
|
|
129
|
+
|
|
130
|
+
The `*_run_submit` tools return a `run_id` immediately (in <1s). Claude then needs to call `check_report_status(run_id, agent_type)` to fetch the result, which is normally ready in 60-180s. Most MCP clients handle this seamlessly — Claude knows from the tool descriptions to poll until status="completed".
|
|
131
|
+
|
|
132
|
+
The cheaper `*_findings` tools are synchronous and complete in 30-90s on a single call. Use those when the LLM just needs raw structured data and will do its own analysis.
|
|
133
|
+
|
|
134
|
+
## Verify your install
|
|
135
|
+
|
|
136
|
+
Use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to confirm the server starts and tool dispatch works:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
SONAR_API_KEY=sk-sonar-... npx @modelcontextprotocol/inspector uvx getsonar-mcp
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
You should see all 7 tools listed and be able to invoke them with test inputs.
|
|
143
|
+
|
|
144
|
+
## Source
|
|
145
|
+
|
|
146
|
+
`getsonar-mcp` is a thin client for the SONAR public API. The source is currently maintained privately; for issues, feature requests, or transport changes, email **ivan.vakulenko@wildix.com**.
|
|
147
|
+
|
|
148
|
+
## Troubleshooting
|
|
149
|
+
|
|
150
|
+
**Claude says "tool not available"** — check `claude_desktop_config.json` is valid JSON, restart Claude Desktop, then verify `uvx --version` runs in your terminal.
|
|
151
|
+
|
|
152
|
+
**`401 Unauthorized`** — `SONAR_API_KEY` is missing or wrong. Verify in the SONAR dashboard at [getsonar.report/api-keys](https://getsonar.report/api-keys).
|
|
153
|
+
|
|
154
|
+
**`402 Payment Required`** — your free tier is exhausted and your balance is $0. Top up at [getsonar.report/api-keys](https://getsonar.report/api-keys).
|
|
155
|
+
|
|
156
|
+
**Timeouts** — bump `SONAR_API_TIMEOUT` in your env config. The `/run` tools can take 3+ minutes for complex KYB subjects.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "getsonar-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for SONAR — competitive intelligence, KYB due diligence, and adverse media screening for AI agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "SONAR", email = "ivan.vakulenko@wildix.com" },
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"mcp",
|
|
18
|
+
"model-context-protocol",
|
|
19
|
+
"claude",
|
|
20
|
+
"anthropic",
|
|
21
|
+
"kyb",
|
|
22
|
+
"compliance",
|
|
23
|
+
"competitive-intelligence",
|
|
24
|
+
"osint",
|
|
25
|
+
"adverse-media",
|
|
26
|
+
]
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 4 - Beta",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Topic :: Software Development :: Libraries",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
dependencies = [
|
|
38
|
+
"mcp>=1.0.0",
|
|
39
|
+
"httpx>=0.27.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
http = [
|
|
44
|
+
"uvicorn>=0.30.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://getsonar.report"
|
|
49
|
+
Documentation = "https://getsonar.report/developers"
|
|
50
|
+
"API Reference" = "https://api.getsonar.report/docs"
|
|
51
|
+
Support = "https://getsonar.report/api-keys"
|
|
52
|
+
|
|
53
|
+
[project.scripts]
|
|
54
|
+
getsonar-mcp = "getsonar_mcp.__main__:main"
|
|
55
|
+
getsonar-mcp-http = "getsonar_mcp.http_server:main"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/getsonar_mcp"]
|
|
59
|
+
|
|
60
|
+
[tool.hatch.build.targets.sdist]
|
|
61
|
+
include = [
|
|
62
|
+
"/src",
|
|
63
|
+
"/README.md",
|
|
64
|
+
"/pyproject.toml",
|
|
65
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""SONAR Intelligence MCP Server.
|
|
2
|
+
|
|
3
|
+
Exposes SONAR's competitive intelligence, KYB due diligence, and adverse
|
|
4
|
+
media screening tools to MCP-compatible AI clients (Claude Desktop, Claude
|
|
5
|
+
Code, Cursor, etc.).
|
|
6
|
+
|
|
7
|
+
Two transports from one codebase:
|
|
8
|
+
|
|
9
|
+
- **stdio** (default, for local install) — ``python -m getsonar_mcp`` or
|
|
10
|
+
``uvx getsonar-mcp``. Each customer runs the server locally, configures
|
|
11
|
+
their API key via the ``SONAR_API_KEY`` env var, and adds the command
|
|
12
|
+
to their MCP client config. No infrastructure required.
|
|
13
|
+
|
|
14
|
+
- **streamable-http** (remote/hosted) — ``python -m getsonar_mcp.http_server``
|
|
15
|
+
starts an HTTP MCP endpoint suitable for Render / Cloud Run. Customers
|
|
16
|
+
paste a single URL into their MCP client config; OAuth handles auth.
|
|
17
|
+
See ``http_server.py`` and ``oauth.py`` for the deployment story.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Stdio MCP entry point — what ``uvx getsonar-mcp`` and
|
|
2
|
+
``python -m getsonar_mcp`` invoke.
|
|
3
|
+
|
|
4
|
+
The default transport for local install. Each customer runs the server
|
|
5
|
+
on their own machine; the MCP client (Claude Desktop / Code / Cursor)
|
|
6
|
+
spawns this process and talks to it over stdin/stdout. The customer's
|
|
7
|
+
``SONAR_API_KEY`` env var holds their API key — see the README for
|
|
8
|
+
client-specific config snippets.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from mcp.server.fastmcp import FastMCP
|
|
18
|
+
|
|
19
|
+
from . import __version__
|
|
20
|
+
from .tools import register_tools
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _setup_logging() -> None:
|
|
24
|
+
"""Log to stderr so MCP-protocol stdout stays clean."""
|
|
25
|
+
level = os.environ.get("SONAR_MCP_LOG_LEVEL", "INFO").upper()
|
|
26
|
+
logging.basicConfig(
|
|
27
|
+
level=getattr(logging, level, logging.INFO),
|
|
28
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
29
|
+
stream=sys.stderr,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main() -> None:
|
|
34
|
+
_setup_logging()
|
|
35
|
+
log = logging.getLogger("getsonar-mcp")
|
|
36
|
+
log.info("Starting SONAR MCP server v%s (stdio transport)", __version__)
|
|
37
|
+
|
|
38
|
+
mcp = FastMCP(
|
|
39
|
+
"SONAR Intelligence",
|
|
40
|
+
description=(
|
|
41
|
+
"AI-powered competitive intelligence, KYB due diligence, and "
|
|
42
|
+
"adverse media screening. Backed by 40+ OSINT sources. "
|
|
43
|
+
"Get an API key at https://getsonar.report/api-keys."
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
register_tools(mcp)
|
|
47
|
+
|
|
48
|
+
# FastMCP.run() defaults to stdio transport when called with no args
|
|
49
|
+
mcp.run()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""HTTP client + response formatters for the SONAR public API.
|
|
2
|
+
|
|
3
|
+
Shared by both stdio and streamable-http MCP transports. The transport
|
|
4
|
+
layer just wires up tool dispatch; this module owns the actual API calls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("getsonar-mcp.client")
|
|
17
|
+
|
|
18
|
+
# Public-API base URL. Custom-domain default — overridable for staging /
|
|
19
|
+
# self-hosted deployments via env.
|
|
20
|
+
DEFAULT_API_BASE = "https://api.getsonar.report"
|
|
21
|
+
|
|
22
|
+
DEFAULT_TIMEOUT_S = 120
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_api_base() -> str:
|
|
26
|
+
return os.environ.get("SONAR_API_BASE", DEFAULT_API_BASE).rstrip("/")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_default_timeout() -> int:
|
|
30
|
+
raw = os.environ.get("SONAR_API_TIMEOUT", "").strip()
|
|
31
|
+
if not raw:
|
|
32
|
+
return DEFAULT_TIMEOUT_S
|
|
33
|
+
try:
|
|
34
|
+
return max(5, int(raw))
|
|
35
|
+
except ValueError:
|
|
36
|
+
return DEFAULT_TIMEOUT_S
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_api_key_from_env() -> str:
|
|
40
|
+
"""Resolve the API key from the environment. Raises a friendly error
|
|
41
|
+
when the env var is missing — the most common config mistake."""
|
|
42
|
+
key = os.environ.get("SONAR_API_KEY", "").strip()
|
|
43
|
+
if not key:
|
|
44
|
+
raise RuntimeError(
|
|
45
|
+
"SONAR_API_KEY environment variable is not set. "
|
|
46
|
+
"Get a key at https://getsonar.report/api-keys, then add "
|
|
47
|
+
"SONAR_API_KEY to your MCP client config."
|
|
48
|
+
)
|
|
49
|
+
return key
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def call_sonar(
|
|
53
|
+
method: str,
|
|
54
|
+
path: str,
|
|
55
|
+
api_key: str,
|
|
56
|
+
*,
|
|
57
|
+
body: Optional[dict] = None,
|
|
58
|
+
timeout: Optional[int] = None,
|
|
59
|
+
) -> dict:
|
|
60
|
+
"""Call the SONAR REST API and return the parsed JSON response.
|
|
61
|
+
|
|
62
|
+
Returns ``{"error": True, "status": int, "message": str}`` on non-200
|
|
63
|
+
so callers can format a friendly message without try/except.
|
|
64
|
+
"""
|
|
65
|
+
url = f"{get_api_base()}{path}"
|
|
66
|
+
headers = {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
"X-API-Key": api_key,
|
|
69
|
+
"User-Agent": "getsonar-mcp/0.1.0",
|
|
70
|
+
}
|
|
71
|
+
actual_timeout = timeout or get_default_timeout()
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
async with httpx.AsyncClient(timeout=actual_timeout) as c:
|
|
75
|
+
if method == "POST":
|
|
76
|
+
resp = await c.post(url, json=body or {}, headers=headers)
|
|
77
|
+
else:
|
|
78
|
+
resp = await c.get(url, headers=headers)
|
|
79
|
+
except httpx.TimeoutException:
|
|
80
|
+
return {
|
|
81
|
+
"error": True,
|
|
82
|
+
"status": 0,
|
|
83
|
+
"message": (
|
|
84
|
+
f"Request to {path} timed out after {actual_timeout}s. "
|
|
85
|
+
"The /findings endpoint usually responds in 30-90s; if "
|
|
86
|
+
"you're consistently timing out, raise SONAR_API_TIMEOUT."
|
|
87
|
+
),
|
|
88
|
+
}
|
|
89
|
+
except httpx.RequestError as e:
|
|
90
|
+
return {
|
|
91
|
+
"error": True,
|
|
92
|
+
"status": 0,
|
|
93
|
+
"message": f"Network error calling {path}: {type(e).__name__}",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if resp.status_code != 200 and resp.status_code != 202:
|
|
97
|
+
try:
|
|
98
|
+
err = resp.json()
|
|
99
|
+
# The SONAR public API wraps errors as
|
|
100
|
+
# {"detail": {"error": {"code": str, "message": str, ...}}}
|
|
101
|
+
msg = (
|
|
102
|
+
err.get("detail", {}).get("error", {}).get("message")
|
|
103
|
+
or err.get("detail")
|
|
104
|
+
or resp.text
|
|
105
|
+
)
|
|
106
|
+
except Exception:
|
|
107
|
+
msg = resp.text
|
|
108
|
+
return {
|
|
109
|
+
"error": True,
|
|
110
|
+
"status": resp.status_code,
|
|
111
|
+
"message": str(msg)[:400],
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
return resp.json()
|
|
116
|
+
except ValueError:
|
|
117
|
+
return {"error": True, "status": resp.status_code, "message": "non-JSON response"}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ── Response formatters ──────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def format_findings(result: dict, title: str) -> str:
|
|
124
|
+
"""Format a /findings API response as readable Markdown for the LLM."""
|
|
125
|
+
if result.get("error"):
|
|
126
|
+
# Surface a friendly 402 message verbatim — it includes top-up URL
|
|
127
|
+
# and the customer needs to see it
|
|
128
|
+
if result.get("status") == 402:
|
|
129
|
+
return f"⚠️ Insufficient balance.\n\n{result.get('message', '')}"
|
|
130
|
+
return (
|
|
131
|
+
f"⚠️ SONAR API error (HTTP {result.get('status', '?')}).\n\n"
|
|
132
|
+
f"{result.get('message', 'unknown error')}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
fields = result.get("fields", {})
|
|
136
|
+
found = result.get("fields_found", []) or []
|
|
137
|
+
not_found = result.get("fields_not_found", []) or []
|
|
138
|
+
sources = result.get("sources_checked", 0)
|
|
139
|
+
time_ms = result.get("processing_time_ms", 0)
|
|
140
|
+
|
|
141
|
+
parts = [
|
|
142
|
+
f"# {title}",
|
|
143
|
+
f"Sources checked: {sources} · Processing time: {time_ms}ms",
|
|
144
|
+
f"Fields with data: {', '.join(found) if found else 'none'}",
|
|
145
|
+
]
|
|
146
|
+
if not_found:
|
|
147
|
+
parts.append(f"No data for: {', '.join(not_found)}")
|
|
148
|
+
parts.append("")
|
|
149
|
+
|
|
150
|
+
for field_name, field_data in fields.items():
|
|
151
|
+
findings = (field_data or {}).get("findings", []) or []
|
|
152
|
+
confidence = (field_data or {}).get("confidence") or "N/A"
|
|
153
|
+
if findings:
|
|
154
|
+
label = field_name.replace("_", " ").title()
|
|
155
|
+
parts.append(f"## {label} (confidence: {confidence})")
|
|
156
|
+
for f in findings:
|
|
157
|
+
value = f.get("value", "") or ""
|
|
158
|
+
detail = f.get("detail", "") or ""
|
|
159
|
+
sources_list = f.get("sources", []) or []
|
|
160
|
+
url = sources_list[0].get("url", "") if sources_list else ""
|
|
161
|
+
parts.append(f"- **{value}**")
|
|
162
|
+
if detail:
|
|
163
|
+
parts.append(f" {detail}")
|
|
164
|
+
if url:
|
|
165
|
+
parts.append(f" Source: {url}")
|
|
166
|
+
parts.append("")
|
|
167
|
+
|
|
168
|
+
if not found:
|
|
169
|
+
parts.append("No findings detected across all sources.")
|
|
170
|
+
|
|
171
|
+
return "\n".join(parts)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def format_run_started(result: dict, agent_type: str) -> str:
|
|
175
|
+
"""Format the /run async-submit response."""
|
|
176
|
+
if result.get("error"):
|
|
177
|
+
if result.get("status") == 402:
|
|
178
|
+
return f"⚠️ Insufficient balance.\n\n{result.get('message', '')}"
|
|
179
|
+
return (
|
|
180
|
+
f"⚠️ Failed to start report (HTTP {result.get('status', '?')}).\n\n"
|
|
181
|
+
f"{result.get('message', '')}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
run_id = result.get("run_id", "")
|
|
185
|
+
status = result.get("status", "")
|
|
186
|
+
est = result.get("estimated_seconds", 90)
|
|
187
|
+
return (
|
|
188
|
+
f"Report submitted.\n"
|
|
189
|
+
f"- Run ID: `{run_id}`\n"
|
|
190
|
+
f"- Status: {status}\n"
|
|
191
|
+
f"- Estimated time: {est} seconds\n\n"
|
|
192
|
+
f"Call `check_report_status` with run_id='{run_id}' and "
|
|
193
|
+
f"agent_type='{agent_type}' in 30-60 seconds to get the result."
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def format_run_status(result: dict) -> str:
|
|
198
|
+
"""Format the /run/{id} status-poll response."""
|
|
199
|
+
if result.get("error"):
|
|
200
|
+
return (
|
|
201
|
+
f"⚠️ Failed to check report status (HTTP {result.get('status', '?')}).\n\n"
|
|
202
|
+
f"{result.get('message', '')}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
status = result.get("status", "unknown")
|
|
206
|
+
if status == "completed":
|
|
207
|
+
report = result.get("report", {}) or result.get("report_json", {}) or {}
|
|
208
|
+
# Compress: skip empty fields, return the narrative + key findings
|
|
209
|
+
return f"Report completed.\n\n```json\n{json.dumps(report, indent=2)[:8000]}\n```"
|
|
210
|
+
if status == "failed":
|
|
211
|
+
err = result.get("error_message") or result.get("error") or "Unknown error"
|
|
212
|
+
return f"Report failed: {err}"
|
|
213
|
+
return f"Report status: {status}. Still processing — check again in 15-30 seconds."
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Streamable-HTTP MCP server entry point — for hosted/remote deployment.
|
|
2
|
+
|
|
3
|
+
Same tools as the stdio variant, just exposed via Streamable HTTP so a
|
|
4
|
+
single hosted instance can serve many customers. Combine with ``oauth.py``
|
|
5
|
+
(in the parent directory) once OAuth is fully wired into the request
|
|
6
|
+
pipeline so per-user API keys are resolved from the bearer token.
|
|
7
|
+
|
|
8
|
+
For now this still uses the global ``SONAR_API_KEY`` env var as the
|
|
9
|
+
authentication gate. That's correct for self-hosted single-tenant; full
|
|
10
|
+
multi-tenant deployment needs the OAuth ``validate_token`` integration.
|
|
11
|
+
|
|
12
|
+
Run via:
|
|
13
|
+
python -m getsonar_mcp.http_server
|
|
14
|
+
or:
|
|
15
|
+
PORT=8080 python -m getsonar_mcp.http_server
|
|
16
|
+
|
|
17
|
+
The Dockerfile in the parent directory uses the legacy ``server.py``
|
|
18
|
+
entry which delegates here.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
from mcp.server.fastmcp import FastMCP
|
|
28
|
+
|
|
29
|
+
from . import __version__
|
|
30
|
+
from .tools import register_tools
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _setup_logging() -> None:
|
|
34
|
+
level = os.environ.get("SONAR_MCP_LOG_LEVEL", "INFO").upper()
|
|
35
|
+
logging.basicConfig(
|
|
36
|
+
level=getattr(logging, level, logging.INFO),
|
|
37
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
38
|
+
stream=sys.stderr,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main() -> None:
|
|
43
|
+
_setup_logging()
|
|
44
|
+
log = logging.getLogger("getsonar-mcp.http")
|
|
45
|
+
port = int(os.environ.get("PORT", "8080"))
|
|
46
|
+
log.info("Starting SONAR MCP server v%s (streamable-http on :%d)",
|
|
47
|
+
__version__, port)
|
|
48
|
+
|
|
49
|
+
mcp = FastMCP(
|
|
50
|
+
"SONAR Intelligence",
|
|
51
|
+
description=(
|
|
52
|
+
"AI-powered competitive intelligence, KYB due diligence, and "
|
|
53
|
+
"adverse media screening. Backed by 40+ OSINT sources."
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
register_tools(mcp)
|
|
57
|
+
mcp.run(transport="streamable-http", host="0.0.0.0", port=port)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
main()
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""MCP tool definitions — shared across stdio and HTTP transports.
|
|
2
|
+
|
|
3
|
+
Each ``@mcp.tool`` calls the SONAR public API via ``client.call_sonar()``
|
|
4
|
+
and formats the response as Markdown the LLM can ingest. Tool annotations
|
|
5
|
+
follow the MCP spec (``readOnlyHint``, ``destructiveHint``, ``openWorldHint``)
|
|
6
|
+
so MCP clients can render them with appropriate UX warnings.
|
|
7
|
+
|
|
8
|
+
Tool catalogue:
|
|
9
|
+
|
|
10
|
+
- kyb_findings — quick KYB lookup (raw signals, no AI analysis)
|
|
11
|
+
- kyb_run_submit — full KYB report (Claude analysis, async)
|
|
12
|
+
- competitive_findings— quick CI lookup
|
|
13
|
+
- competitive_run_submit — full CI report (async)
|
|
14
|
+
- adverse_media — focused negative-media + complaints check
|
|
15
|
+
- check_report_status — poll an async submission
|
|
16
|
+
- account_balance — read current account balance + free-tier remaining
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from .client import (
|
|
25
|
+
call_sonar,
|
|
26
|
+
format_findings,
|
|
27
|
+
format_run_started,
|
|
28
|
+
format_run_status,
|
|
29
|
+
get_api_key_from_env,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register_tools(mcp: Any) -> None:
|
|
34
|
+
"""Register all SONAR tools onto a FastMCP instance.
|
|
35
|
+
|
|
36
|
+
Caller passes their FastMCP instance (different transports build the
|
|
37
|
+
instance differently — see stdio.py and http_server.py).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# ── KYB ────────────────────────────────────────────────────────────────
|
|
41
|
+
@mcp.tool(
|
|
42
|
+
annotations={
|
|
43
|
+
"title": "KYB Quick Lookup",
|
|
44
|
+
"readOnlyHint": True,
|
|
45
|
+
"destructiveHint": False,
|
|
46
|
+
"openWorldHint": True,
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
async def kyb_findings(company_name: str, website: str = "") -> str:
|
|
50
|
+
"""Run a fast KYB (Know Your Business) screening on a company.
|
|
51
|
+
|
|
52
|
+
Returns raw structured findings — corporate registration, sanctions,
|
|
53
|
+
adverse media, regulatory licensure (FCA for UK), directors, UBOs.
|
|
54
|
+
Cheap, fast (30-90s), no Claude analysis. Cost: $0.40/call beyond
|
|
55
|
+
free tier.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
company_name: Legal name of the company (e.g. "Tesco PLC")
|
|
59
|
+
website: Company website URL for better disambiguation
|
|
60
|
+
"""
|
|
61
|
+
api_key = get_api_key_from_env()
|
|
62
|
+
body = {"company_name": company_name}
|
|
63
|
+
if website:
|
|
64
|
+
body["website"] = website
|
|
65
|
+
result = await call_sonar(
|
|
66
|
+
"POST", "/v1/agents/kyb-diligence/findings", api_key, body=body,
|
|
67
|
+
)
|
|
68
|
+
return format_findings(result, f"KYB Quick Lookup: {company_name}")
|
|
69
|
+
|
|
70
|
+
@mcp.tool(
|
|
71
|
+
annotations={
|
|
72
|
+
"title": "KYB Full Report (async)",
|
|
73
|
+
"readOnlyHint": False,
|
|
74
|
+
"destructiveHint": False,
|
|
75
|
+
"openWorldHint": True,
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
async def kyb_run_submit(company_name: str, website: str = "") -> str:
|
|
79
|
+
"""Submit a full KYB compliance report for AI-narrated analysis.
|
|
80
|
+
|
|
81
|
+
Returns a run_id immediately (202). Poll with check_report_status
|
|
82
|
+
in 1-3 minutes. Includes everything kyb_findings returns plus
|
|
83
|
+
Claude-generated risk flags, narrative summary, and recommendations
|
|
84
|
+
formatted for compliance-officer review. Cost: $2.00/call beyond
|
|
85
|
+
free tier.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
company_name: Legal name of the company
|
|
89
|
+
website: Company website URL
|
|
90
|
+
"""
|
|
91
|
+
api_key = get_api_key_from_env()
|
|
92
|
+
body = {"company_name": company_name}
|
|
93
|
+
if website:
|
|
94
|
+
body["website"] = website
|
|
95
|
+
result = await call_sonar(
|
|
96
|
+
"POST", "/v1/agents/kyb-diligence/run", api_key, body=body,
|
|
97
|
+
)
|
|
98
|
+
return format_run_started(result, "kyb-diligence")
|
|
99
|
+
|
|
100
|
+
# ── Competitive Intelligence ───────────────────────────────────────────
|
|
101
|
+
@mcp.tool(
|
|
102
|
+
annotations={
|
|
103
|
+
"title": "Competitive Intelligence Quick Lookup",
|
|
104
|
+
"readOnlyHint": True,
|
|
105
|
+
"destructiveHint": False,
|
|
106
|
+
"openWorldHint": True,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
async def competitive_findings(company_name: str, website: str = "") -> str:
|
|
110
|
+
"""Research a company across 40+ OSINT sources.
|
|
111
|
+
|
|
112
|
+
Returns structured signals on pricing changes, product updates,
|
|
113
|
+
hiring, corporate events, customer sentiment, tech stack, and
|
|
114
|
+
market presence. Cost: $0.40/call beyond free tier.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
company_name: The company to research (e.g. "Notion")
|
|
118
|
+
website: Company website URL for better accuracy
|
|
119
|
+
"""
|
|
120
|
+
api_key = get_api_key_from_env()
|
|
121
|
+
body = {"company_name": company_name}
|
|
122
|
+
if website:
|
|
123
|
+
body["website"] = website
|
|
124
|
+
result = await call_sonar(
|
|
125
|
+
"POST", "/v1/agents/company-intelligence/findings", api_key, body=body,
|
|
126
|
+
)
|
|
127
|
+
return format_findings(result, f"Competitive Intel: {company_name}")
|
|
128
|
+
|
|
129
|
+
@mcp.tool(
|
|
130
|
+
annotations={
|
|
131
|
+
"title": "Competitive Intelligence Full Report (async)",
|
|
132
|
+
"readOnlyHint": False,
|
|
133
|
+
"destructiveHint": False,
|
|
134
|
+
"openWorldHint": True,
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
async def competitive_run_submit(company_name: str, website: str = "") -> str:
|
|
138
|
+
"""Submit a full competitive-intelligence report for AI analysis.
|
|
139
|
+
|
|
140
|
+
Async — returns run_id, poll with check_report_status. Includes
|
|
141
|
+
narrative analysis, top alerts, competitor positioning, strategic
|
|
142
|
+
recommendations. Cost: $2.00/call beyond free tier.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
company_name: The company to analyze
|
|
146
|
+
website: Company website URL
|
|
147
|
+
"""
|
|
148
|
+
api_key = get_api_key_from_env()
|
|
149
|
+
body = {"company_name": company_name}
|
|
150
|
+
if website:
|
|
151
|
+
body["website"] = website
|
|
152
|
+
result = await call_sonar(
|
|
153
|
+
"POST", "/v1/agents/company-intelligence/run", api_key, body=body,
|
|
154
|
+
)
|
|
155
|
+
return format_run_started(result, "company-intelligence")
|
|
156
|
+
|
|
157
|
+
# ── Adverse Media ──────────────────────────────────────────────────────
|
|
158
|
+
@mcp.tool(
|
|
159
|
+
annotations={
|
|
160
|
+
"title": "Adverse Media Check",
|
|
161
|
+
"readOnlyHint": True,
|
|
162
|
+
"destructiveHint": False,
|
|
163
|
+
"openWorldHint": True,
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
async def adverse_media(company_name: str, website: str = "") -> str:
|
|
167
|
+
"""Screen a company for negative news, complaints, and bad reviews.
|
|
168
|
+
|
|
169
|
+
Checks news sources, review platforms (G2, Capterra, Trustpilot,
|
|
170
|
+
BBB), Reddit, and complaint databases. Cost: $0.40/call beyond
|
|
171
|
+
free tier.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
company_name: Company to screen
|
|
175
|
+
website: Company website URL for accuracy
|
|
176
|
+
"""
|
|
177
|
+
api_key = get_api_key_from_env()
|
|
178
|
+
body = {"company_name": company_name}
|
|
179
|
+
if website:
|
|
180
|
+
body["website"] = website
|
|
181
|
+
result = await call_sonar(
|
|
182
|
+
"POST", "/v1/agents/adverse-media/findings", api_key, body=body,
|
|
183
|
+
)
|
|
184
|
+
return format_findings(result, f"Adverse Media: {company_name}")
|
|
185
|
+
|
|
186
|
+
# ── Async polling ──────────────────────────────────────────────────────
|
|
187
|
+
@mcp.tool(
|
|
188
|
+
annotations={
|
|
189
|
+
"title": "Check Report Status",
|
|
190
|
+
"readOnlyHint": True,
|
|
191
|
+
"destructiveHint": False,
|
|
192
|
+
"openWorldHint": False,
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
async def check_report_status(
|
|
196
|
+
run_id: str,
|
|
197
|
+
agent_type: str = "kyb-diligence",
|
|
198
|
+
) -> str:
|
|
199
|
+
"""Poll the status of an async report submitted via *_run_submit.
|
|
200
|
+
|
|
201
|
+
Returns the full report JSON when status='completed'. Reports
|
|
202
|
+
typically complete in 60-180s.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
run_id: The run_id returned by kyb_run_submit / competitive_run_submit
|
|
206
|
+
agent_type: One of "kyb-diligence", "company-intelligence", "adverse-media"
|
|
207
|
+
"""
|
|
208
|
+
valid = {"kyb-diligence", "company-intelligence", "adverse-media"}
|
|
209
|
+
if agent_type not in valid:
|
|
210
|
+
return json.dumps({
|
|
211
|
+
"error": f"Invalid agent_type. Must be one of: {sorted(valid)}"
|
|
212
|
+
})
|
|
213
|
+
api_key = get_api_key_from_env()
|
|
214
|
+
result = await call_sonar(
|
|
215
|
+
"GET", f"/v1/agents/{agent_type}/run/{run_id}", api_key,
|
|
216
|
+
)
|
|
217
|
+
return format_run_status(result)
|
|
218
|
+
|
|
219
|
+
# ── Account / billing ──────────────────────────────────────────────────
|
|
220
|
+
@mcp.tool(
|
|
221
|
+
annotations={
|
|
222
|
+
"title": "Account Balance & Usage",
|
|
223
|
+
"readOnlyHint": True,
|
|
224
|
+
"destructiveHint": False,
|
|
225
|
+
"openWorldHint": False,
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
async def account_balance() -> str:
|
|
229
|
+
"""Check current account usage and remaining free-tier quota.
|
|
230
|
+
|
|
231
|
+
Useful for AI agents to decide whether the next call will be free
|
|
232
|
+
or paid. Returns remaining free-tier calls this month plus the
|
|
233
|
+
per-key rate limit. (Pre-paid balance lookup requires a separate
|
|
234
|
+
JWT-authenticated endpoint and is not exposed via API key auth.)
|
|
235
|
+
"""
|
|
236
|
+
api_key = get_api_key_from_env()
|
|
237
|
+
result = await call_sonar("GET", "/v1/usage", api_key)
|
|
238
|
+
if result.get("error"):
|
|
239
|
+
return f"⚠️ Couldn't read account: {result.get('message', '')}"
|
|
240
|
+
period = result.get("period", "")
|
|
241
|
+
total = result.get("total_requests", 0)
|
|
242
|
+
quota = result.get("monthly_quota", 100)
|
|
243
|
+
remaining = result.get("remaining", quota - total)
|
|
244
|
+
by_endpoint = result.get("by_endpoint", {}) or {}
|
|
245
|
+
lines = [
|
|
246
|
+
f"# Account usage — {period}",
|
|
247
|
+
f"- Calls this month: **{total} / {quota}**",
|
|
248
|
+
f"- Free tier remaining: **{remaining}** calls",
|
|
249
|
+
]
|
|
250
|
+
if by_endpoint:
|
|
251
|
+
lines.append("")
|
|
252
|
+
lines.append("By endpoint:")
|
|
253
|
+
for k, v in sorted(by_endpoint.items()):
|
|
254
|
+
lines.append(f" - {k}: {v}")
|
|
255
|
+
if remaining <= 5:
|
|
256
|
+
lines.append("")
|
|
257
|
+
lines.append(
|
|
258
|
+
"⚠️ Free tier nearly exhausted. Top up your balance at "
|
|
259
|
+
"https://getsonar.report/api-keys to keep calling."
|
|
260
|
+
)
|
|
261
|
+
return "\n".join(lines)
|