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.
@@ -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)