mcp-botnex 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.
Files changed (38) hide show
  1. mcp_botnex-0.1.0/PKG-INFO +141 -0
  2. mcp_botnex-0.1.0/README.md +109 -0
  3. mcp_botnex-0.1.0/app/__init__.py +15 -0
  4. mcp_botnex-0.1.0/app/client/__init__.py +5 -0
  5. mcp_botnex-0.1.0/app/client/backend_client.py +270 -0
  6. mcp_botnex-0.1.0/app/core/__init__.py +1 -0
  7. mcp_botnex-0.1.0/app/core/config.py +83 -0
  8. mcp_botnex-0.1.0/app/core/exceptions.py +81 -0
  9. mcp_botnex-0.1.0/app/core/logging.py +45 -0
  10. mcp_botnex-0.1.0/app/core/registry.py +77 -0
  11. mcp_botnex-0.1.0/app/formatters/__init__.py +18 -0
  12. mcp_botnex-0.1.0/app/formatters/cve_formatter.py +106 -0
  13. mcp_botnex-0.1.0/app/formatters/finding_formatter.py +75 -0
  14. mcp_botnex-0.1.0/app/formatters/scan_formatter.py +44 -0
  15. mcp_botnex-0.1.0/app/formatters/summary_formatter.py +38 -0
  16. mcp_botnex-0.1.0/app/http_server.py +175 -0
  17. mcp_botnex-0.1.0/app/mcp_server.py +112 -0
  18. mcp_botnex-0.1.0/app/resources/__init__.py +38 -0
  19. mcp_botnex-0.1.0/app/resources/base.py +93 -0
  20. mcp_botnex-0.1.0/app/resources/cve_resources.py +33 -0
  21. mcp_botnex-0.1.0/app/resources/scan_resources.py +56 -0
  22. mcp_botnex-0.1.0/app/tools/__init__.py +35 -0
  23. mcp_botnex-0.1.0/app/tools/base.py +45 -0
  24. mcp_botnex-0.1.0/app/tools/cve_tools.py +86 -0
  25. mcp_botnex-0.1.0/app/tools/report_tools.py +87 -0
  26. mcp_botnex-0.1.0/app/tools/scan_tools.py +158 -0
  27. mcp_botnex-0.1.0/mcp_botnex.egg-info/PKG-INFO +141 -0
  28. mcp_botnex-0.1.0/mcp_botnex.egg-info/SOURCES.txt +36 -0
  29. mcp_botnex-0.1.0/mcp_botnex.egg-info/dependency_links.txt +1 -0
  30. mcp_botnex-0.1.0/mcp_botnex.egg-info/entry_points.txt +3 -0
  31. mcp_botnex-0.1.0/mcp_botnex.egg-info/requires.txt +12 -0
  32. mcp_botnex-0.1.0/mcp_botnex.egg-info/top_level.txt +1 -0
  33. mcp_botnex-0.1.0/pyproject.toml +75 -0
  34. mcp_botnex-0.1.0/setup.cfg +4 -0
  35. mcp_botnex-0.1.0/tests/test_backend_client.py +73 -0
  36. mcp_botnex-0.1.0/tests/test_config.py +34 -0
  37. mcp_botnex-0.1.0/tests/test_resources.py +49 -0
  38. mcp_botnex-0.1.0/tests/test_tools.py +69 -0
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-botnex
3
+ Version: 0.1.0
4
+ Summary: MCP Server for BotNEX - VAPT scans, reports and CVE intelligence for AI clients
5
+ Author: BotNEX Team
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ivedha-tech/botnex-mcp-server
8
+ Project-URL: Repository, https://github.com/ivedha-tech/botnex-mcp-server
9
+ Project-URL: Documentation, https://github.com/ivedha-tech/botnex-mcp-server#readme
10
+ Keywords: mcp,botnex,vapt,security,cve,vulnerability,model-context-protocol
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Security
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: mcp>=1.0.0
22
+ Requires-Dist: fastapi>=0.109.0
23
+ Requires-Dist: uvicorn[standard]>=0.27.0
24
+ Requires-Dist: httpx>=0.25.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Requires-Dist: pydantic-settings>=2.0.0
27
+ Requires-Dist: python-dotenv>=1.0.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
30
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
31
+ Requires-Dist: pytest-httpx>=0.30.0; extra == "dev"
32
+
33
+ # mcp-botnex
34
+
35
+ MCP (Model Context Protocol) server for **BotNEX** — exposes VAPT scans,
36
+ reports, and CVE intelligence to AI clients such as Cursor and the NEXA
37
+ platform. It is a thin, secure bridge to the existing `botnex-backend` REST
38
+ API and authenticates with a **user-scoped API token** (no JWT).
39
+
40
+ ## Features
41
+
42
+ - **10 tools** for scan lifecycle, reports, and CVE lookup
43
+ - **6 resources** (`botnex://…`) for `@`-mentionable context in Cursor
44
+ - **Dual transport**: stdio (Cursor / Claude Desktop) and HTTP (NEXA / Docker)
45
+ - **API-token auth** mapped per-user on the backend — RBAC enforced server-side
46
+ - AI-friendly formatters (severity-first findings, normalized CVE output)
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install mcp-botnex
52
+ ```
53
+
54
+ ## Authentication
55
+
56
+ 1. Log in to the BotNEX UI.
57
+ 2. Create an API token: **API Tokens → Create** (calls `POST /api/v1/users/api-tokens/`).
58
+ 3. Copy the raw token once and provide it to the MCP server via `BOTNEX_API_KEY`.
59
+
60
+ The token is sent as `Authorization: Bearer <token>` on every backend request.
61
+ The backend resolves it to your user account and enforces all authorization
62
+ and per-user data scoping.
63
+
64
+ ## Configuration
65
+
66
+ | Variable | Required | Default | Description |
67
+ |----------|----------|---------|-------------|
68
+ | `BOTNEX_BACKEND_URL` | Yes | – | Backend origin, e.g. `https://botnex.example.com` (no `/api/v1`) |
69
+ | `BOTNEX_API_KEY` | Yes | – | User API token |
70
+ | `BOTNEX_MCP_SERVER_PORT` | No | `8001` | HTTP mode port |
71
+ | `BOTNEX_LOG_LEVEL` | No | `INFO` | Log level |
72
+ | `BOTNEX_BACKEND_TIMEOUT` | No | `60.0` | Backend HTTP timeout (s) |
73
+ | `BOTNEX_FINDINGS_PAGE_SIZE` | No | `50` | Default findings page size |
74
+
75
+ See `.env.example` for the full list.
76
+
77
+ ## Usage — Cursor (stdio)
78
+
79
+ Add to your Cursor MCP config:
80
+
81
+ ```json
82
+ {
83
+ "mcpServers": {
84
+ "botnex": {
85
+ "command": "mcp-botnex",
86
+ "env": {
87
+ "BOTNEX_BACKEND_URL": "https://botnex.example.com",
88
+ "BOTNEX_API_KEY": "your-api-token"
89
+ }
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ ## Usage — HTTP (NEXA / Docker)
96
+
97
+ ```bash
98
+ mcp-botnex-http
99
+ # or
100
+ uvicorn app.http_server:app --host 0.0.0.0 --port 8001
101
+ ```
102
+
103
+ Endpoints: `GET /mcp/tools`, `POST /mcp/tools/call`, `GET /mcp/resources`,
104
+ `POST /mcp/resources/read`, `GET /health`.
105
+
106
+ ## Tools
107
+
108
+ | Tool | Description |
109
+ |------|-------------|
110
+ | `list_scans` | List the user's scans and statuses |
111
+ | `trigger_scan` | Start a security or asset-discovery scan |
112
+ | `get_scan_findings` | Paginated, severity-ordered findings for a scan |
113
+ | `get_scan_summary` | Aggregated dashboard summary |
114
+ | `list_scheduled_scans` | Upcoming scheduled scans |
115
+ | `generate_report` | Generate a PDF/CSV/DOCX report |
116
+ | `get_report` | Report metadata by id |
117
+ | `search_cves` | Full-text CVE search |
118
+ | `get_cve_by_id` | CVE detail by id |
119
+ | `search_cves_by_vendor` | CVEs by vendor + version |
120
+
121
+ ## Resources
122
+
123
+ | URI | Description |
124
+ |-----|-------------|
125
+ | `botnex://scans/all` | All scans |
126
+ | `botnex://scans/summary` | Scan dashboard summary |
127
+ | `botnex://scans/scheduled` | Scheduled scans |
128
+ | `botnex://scan/{scan_id}/findings` | Findings for a scan |
129
+ | `botnex://cve/{cve_id}` | CVE detail |
130
+ | `botnex://cve/latest` | Latest CVEs |
131
+
132
+ ## Development
133
+
134
+ ```bash
135
+ pip install -e ".[dev]"
136
+ pytest
137
+ ```
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,109 @@
1
+ # mcp-botnex
2
+
3
+ MCP (Model Context Protocol) server for **BotNEX** — exposes VAPT scans,
4
+ reports, and CVE intelligence to AI clients such as Cursor and the NEXA
5
+ platform. It is a thin, secure bridge to the existing `botnex-backend` REST
6
+ API and authenticates with a **user-scoped API token** (no JWT).
7
+
8
+ ## Features
9
+
10
+ - **10 tools** for scan lifecycle, reports, and CVE lookup
11
+ - **6 resources** (`botnex://…`) for `@`-mentionable context in Cursor
12
+ - **Dual transport**: stdio (Cursor / Claude Desktop) and HTTP (NEXA / Docker)
13
+ - **API-token auth** mapped per-user on the backend — RBAC enforced server-side
14
+ - AI-friendly formatters (severity-first findings, normalized CVE output)
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install mcp-botnex
20
+ ```
21
+
22
+ ## Authentication
23
+
24
+ 1. Log in to the BotNEX UI.
25
+ 2. Create an API token: **API Tokens → Create** (calls `POST /api/v1/users/api-tokens/`).
26
+ 3. Copy the raw token once and provide it to the MCP server via `BOTNEX_API_KEY`.
27
+
28
+ The token is sent as `Authorization: Bearer <token>` on every backend request.
29
+ The backend resolves it to your user account and enforces all authorization
30
+ and per-user data scoping.
31
+
32
+ ## Configuration
33
+
34
+ | Variable | Required | Default | Description |
35
+ |----------|----------|---------|-------------|
36
+ | `BOTNEX_BACKEND_URL` | Yes | – | Backend origin, e.g. `https://botnex.example.com` (no `/api/v1`) |
37
+ | `BOTNEX_API_KEY` | Yes | – | User API token |
38
+ | `BOTNEX_MCP_SERVER_PORT` | No | `8001` | HTTP mode port |
39
+ | `BOTNEX_LOG_LEVEL` | No | `INFO` | Log level |
40
+ | `BOTNEX_BACKEND_TIMEOUT` | No | `60.0` | Backend HTTP timeout (s) |
41
+ | `BOTNEX_FINDINGS_PAGE_SIZE` | No | `50` | Default findings page size |
42
+
43
+ See `.env.example` for the full list.
44
+
45
+ ## Usage — Cursor (stdio)
46
+
47
+ Add to your Cursor MCP config:
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "botnex": {
53
+ "command": "mcp-botnex",
54
+ "env": {
55
+ "BOTNEX_BACKEND_URL": "https://botnex.example.com",
56
+ "BOTNEX_API_KEY": "your-api-token"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Usage — HTTP (NEXA / Docker)
64
+
65
+ ```bash
66
+ mcp-botnex-http
67
+ # or
68
+ uvicorn app.http_server:app --host 0.0.0.0 --port 8001
69
+ ```
70
+
71
+ Endpoints: `GET /mcp/tools`, `POST /mcp/tools/call`, `GET /mcp/resources`,
72
+ `POST /mcp/resources/read`, `GET /health`.
73
+
74
+ ## Tools
75
+
76
+ | Tool | Description |
77
+ |------|-------------|
78
+ | `list_scans` | List the user's scans and statuses |
79
+ | `trigger_scan` | Start a security or asset-discovery scan |
80
+ | `get_scan_findings` | Paginated, severity-ordered findings for a scan |
81
+ | `get_scan_summary` | Aggregated dashboard summary |
82
+ | `list_scheduled_scans` | Upcoming scheduled scans |
83
+ | `generate_report` | Generate a PDF/CSV/DOCX report |
84
+ | `get_report` | Report metadata by id |
85
+ | `search_cves` | Full-text CVE search |
86
+ | `get_cve_by_id` | CVE detail by id |
87
+ | `search_cves_by_vendor` | CVEs by vendor + version |
88
+
89
+ ## Resources
90
+
91
+ | URI | Description |
92
+ |-----|-------------|
93
+ | `botnex://scans/all` | All scans |
94
+ | `botnex://scans/summary` | Scan dashboard summary |
95
+ | `botnex://scans/scheduled` | Scheduled scans |
96
+ | `botnex://scan/{scan_id}/findings` | Findings for a scan |
97
+ | `botnex://cve/{cve_id}` | CVE detail |
98
+ | `botnex://cve/latest` | Latest CVEs |
99
+
100
+ ## Development
101
+
102
+ ```bash
103
+ pip install -e ".[dev]"
104
+ pytest
105
+ ```
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,15 @@
1
+ """BotNEX MCP Server package.
2
+
3
+ A Model Context Protocol bridge between AI clients (Cursor, NEXA) and the
4
+ BotNEX backend. Exposes VAPT scans, reports and CVE intelligence as MCP tools
5
+ and resources, authenticating to the backend with a user-scoped API token.
6
+ """
7
+
8
+ from importlib import metadata
9
+
10
+ try:
11
+ __version__ = metadata.version("mcp-botnex")
12
+ except metadata.PackageNotFoundError: # running from source without install
13
+ __version__ = "0.0.0+local"
14
+
15
+ __all__ = ["__version__"]
@@ -0,0 +1,5 @@
1
+ """HTTP client for the BotNEX backend."""
2
+
3
+ from app.client.backend_client import BackendClient
4
+
5
+ __all__ = ["BackendClient"]
@@ -0,0 +1,270 @@
1
+ """Async HTTP client for the BotNEX backend.
2
+
3
+ Authentication
4
+ --------------
5
+ Every request carries the user-scoped API token::
6
+
7
+ Authorization: Bearer <BOTNEX_API_KEY>
8
+
9
+ The backend resolves the token to a user (``request.user``) and enforces both
10
+ the API-token endpoint allowlist and per-user data scoping. The MCP server is
11
+ therefore a thin proxy - it never makes authorization decisions itself.
12
+
13
+ Error mapping
14
+ -------------
15
+ HTTP status codes are translated into the domain exception hierarchy so tools
16
+ and resources can render safe, useful messages.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ from typing import Any
23
+
24
+ import httpx
25
+
26
+ from app import __version__
27
+ from app.core.config import Settings, get_settings
28
+ from app.core.exceptions import (
29
+ AccessForbiddenError,
30
+ AuthenticationError,
31
+ BackendConnectionError,
32
+ CveNotFoundError,
33
+ MCPServerError,
34
+ ReportNotFoundError,
35
+ ScanNotFoundError,
36
+ )
37
+ from app.core.logging import get_logger, mask_token
38
+
39
+ logger = get_logger(__name__)
40
+
41
+
42
+ class BackendClient:
43
+ """Thin async wrapper over the BotNEX ``/api/v1`` REST API."""
44
+
45
+ def __init__(self, settings: Settings | None = None):
46
+ self.settings = settings or get_settings()
47
+ self._client: httpx.AsyncClient | None = None
48
+
49
+ # -- lifecycle ---------------------------------------------------------
50
+
51
+ async def _get_client(self) -> httpx.AsyncClient:
52
+ """Lazily build the shared httpx client after validating config."""
53
+ self.settings.require_backend_config()
54
+ if self._client is None:
55
+ self._client = httpx.AsyncClient(
56
+ base_url=self.settings.api_base_url,
57
+ timeout=self.settings.botnex_backend_timeout,
58
+ verify=self.settings.botnex_verify_ssl,
59
+ headers=self._default_headers(),
60
+ )
61
+ logger.info(
62
+ "Backend client initialised: %s (token=%s)",
63
+ self.settings.api_base_url,
64
+ mask_token(self.settings.botnex_api_key),
65
+ )
66
+ return self._client
67
+
68
+ def _default_headers(self) -> dict[str, str]:
69
+ return {
70
+ "Authorization": f"Bearer {self.settings.botnex_api_key.strip()}",
71
+ "Accept": "application/json",
72
+ "User-Agent": f"{self.settings.botnex_mcp_server_name}/{__version__}",
73
+ }
74
+
75
+ async def aclose(self) -> None:
76
+ if self._client is not None:
77
+ await self._client.aclose()
78
+ self._client = None
79
+
80
+ async def __aenter__(self) -> "BackendClient":
81
+ await self._get_client()
82
+ return self
83
+
84
+ async def __aexit__(self, *exc: object) -> None:
85
+ await self.aclose()
86
+
87
+ # -- request core ------------------------------------------------------
88
+
89
+ async def request(
90
+ self,
91
+ method: str,
92
+ path: str,
93
+ *,
94
+ params: dict[str, Any] | None = None,
95
+ json: dict[str, Any] | None = None,
96
+ extra_headers: dict[str, str] | None = None,
97
+ not_found: type[MCPServerError] = ScanNotFoundError,
98
+ expect_binary: bool = False,
99
+ ) -> Any:
100
+ """Perform an HTTP request with retries and error mapping.
101
+
102
+ ``path`` is relative to ``/api/v1`` (e.g. ``"/scans/list"``).
103
+ Returns parsed JSON, or raw bytes when ``expect_binary`` is True.
104
+ """
105
+ client = await self._get_client()
106
+ attempts = max(1, self.settings.botnex_backend_retry_attempts)
107
+ last_exc: Exception | None = None
108
+
109
+ for attempt in range(1, attempts + 1):
110
+ try:
111
+ response = await client.request(
112
+ method,
113
+ path,
114
+ params=params,
115
+ json=json,
116
+ headers=extra_headers,
117
+ )
118
+ return self._handle_response(
119
+ response, not_found=not_found, expect_binary=expect_binary
120
+ )
121
+ except (httpx.ConnectError, httpx.ConnectTimeout, httpx.ReadTimeout) as exc:
122
+ last_exc = exc
123
+ logger.warning(
124
+ "Backend %s %s failed (attempt %d/%d): %s",
125
+ method,
126
+ path,
127
+ attempt,
128
+ attempts,
129
+ exc,
130
+ )
131
+ if attempt < attempts:
132
+ await asyncio.sleep(self.settings.botnex_backend_retry_delay * attempt)
133
+ continue
134
+ raise BackendConnectionError(
135
+ "Could not reach the BotNEX backend. Verify BOTNEX_BACKEND_URL "
136
+ "and that the service is reachable."
137
+ ) from exc
138
+ except httpx.HTTPError as exc:
139
+ # Non-retryable transport error.
140
+ raise BackendConnectionError(f"HTTP transport error: {exc}") from exc
141
+
142
+ # Should be unreachable, but keep the type checker happy.
143
+ raise BackendConnectionError(str(last_exc) if last_exc else "Unknown error")
144
+
145
+ def _handle_response(
146
+ self,
147
+ response: httpx.Response,
148
+ *,
149
+ not_found: type[MCPServerError],
150
+ expect_binary: bool,
151
+ ) -> Any:
152
+ status = response.status_code
153
+
154
+ if status == 401:
155
+ raise AuthenticationError(
156
+ "The API token is invalid, expired, or revoked. Create a new "
157
+ "token in the BotNEX UI."
158
+ )
159
+ if status == 403:
160
+ raise AccessForbiddenError(
161
+ "This API token is not permitted to access the requested "
162
+ "resource."
163
+ )
164
+ if status == 404:
165
+ raise not_found("The requested item does not exist or is not "
166
+ "accessible with this token.")
167
+ if status == 429:
168
+ raise BackendConnectionError(
169
+ "Rate limit exceeded on the BotNEX backend. Try again later."
170
+ )
171
+ if status >= 500:
172
+ raise BackendConnectionError(
173
+ f"BotNEX backend returned a server error ({status})."
174
+ )
175
+ if status >= 400:
176
+ raise MCPServerError(self._extract_error_message(response, status))
177
+
178
+ if expect_binary:
179
+ return response.content
180
+ if not response.content:
181
+ return {}
182
+ try:
183
+ return response.json()
184
+ except ValueError:
185
+ return {"raw": response.text}
186
+
187
+ @staticmethod
188
+ def _extract_error_message(response: httpx.Response, status: int) -> str:
189
+ try:
190
+ body = response.json()
191
+ if isinstance(body, dict):
192
+ for key in ("message", "error", "detail"):
193
+ if body.get(key):
194
+ return str(body[key])
195
+ except ValueError:
196
+ pass
197
+ return f"Request failed with status {status}."
198
+
199
+ # -- convenience verbs -------------------------------------------------
200
+
201
+ async def get(self, path: str, **kwargs: Any) -> Any:
202
+ return await self.request("GET", path, **kwargs)
203
+
204
+ async def post(self, path: str, **kwargs: Any) -> Any:
205
+ return await self.request("POST", path, **kwargs)
206
+
207
+ # ======================================================================
208
+ # Domain methods (one per backend endpoint used by the MCP surface)
209
+ # ======================================================================
210
+
211
+ # --- scans ---
212
+ async def list_scans(self) -> Any:
213
+ return await self.get("/scans/list")
214
+
215
+ async def trigger_scan(
216
+ self, payload: dict[str, Any], *, idempotency_key: str | None = None
217
+ ) -> Any:
218
+ headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
219
+ return await self.post("/scans/scan", json=payload, extra_headers=headers)
220
+
221
+ async def get_scan_report(self, payload: dict[str, Any]) -> Any:
222
+ return await self.post("/scans/report", json=payload)
223
+
224
+ async def get_scan_summary(self) -> Any:
225
+ return await self.get("/scans/summary/")
226
+
227
+ async def list_scheduled_scans(self) -> Any:
228
+ return await self.get("/scans/scheduled/")
229
+
230
+ # --- reports ---
231
+ async def generate_report(self, payload: dict[str, Any]) -> bytes:
232
+ return await self.post(
233
+ "/reports/report",
234
+ json=payload,
235
+ not_found=ReportNotFoundError,
236
+ expect_binary=True,
237
+ )
238
+
239
+ async def get_report_metadata(self, report_id: str) -> Any:
240
+ return await self.get(
241
+ f"/reports/{report_id}", not_found=ReportNotFoundError
242
+ )
243
+
244
+ # --- cve ---
245
+ async def search_cves(self, search_term: str) -> Any:
246
+ return await self.get(
247
+ "/cve/search/all_fields/",
248
+ params={"search_term": search_term},
249
+ not_found=CveNotFoundError,
250
+ )
251
+
252
+ async def get_cve_by_id(self, cve_id: str) -> Any:
253
+ return await self.get(
254
+ f"/cve/searchid/{cve_id}/", not_found=CveNotFoundError
255
+ )
256
+
257
+ async def search_cves_by_vendor(self, vendor: str, version: str) -> Any:
258
+ return await self.get(
259
+ "/cve/search/",
260
+ params={"vendor": vendor, "version": version},
261
+ not_found=CveNotFoundError,
262
+ )
263
+
264
+ async def get_latest_cves(self) -> Any:
265
+ return await self.get("/cve/search/latest/", not_found=CveNotFoundError)
266
+
267
+ # --- identity ---
268
+ async def verify_token(self) -> Any:
269
+ """Return the authenticated user's identity (verify connection)."""
270
+ return await self.get("/users/token/verification")
@@ -0,0 +1 @@
1
+ """Core configuration, exceptions and registry for the BotNEX MCP server."""
@@ -0,0 +1,83 @@
1
+ """Application settings.
2
+
3
+ Settings load from environment variables (and a local ``.env`` file when
4
+ present). Validation of required values is *lazy*: the server must start even
5
+ when ``BOTNEX_BACKEND_URL`` / ``BOTNEX_API_KEY`` are missing, because host
6
+ platforms (NEXA, K8s) may inject configuration late or spawn the process just
7
+ to list tools. The first backend call raises a clear, actionable error
8
+ instead.
9
+
10
+ ``extra="ignore"`` ensures unrelated platform environment variables never
11
+ crash the server.
12
+ """
13
+
14
+ from functools import lru_cache
15
+
16
+ from pydantic_settings import BaseSettings, SettingsConfigDict
17
+
18
+ from app.core.exceptions import ConfigurationError
19
+
20
+
21
+ class Settings(BaseSettings):
22
+ model_config = SettingsConfigDict(
23
+ env_file=".env",
24
+ env_file_encoding="utf-8",
25
+ case_sensitive=False,
26
+ extra="ignore",
27
+ )
28
+
29
+ # Required for backend access (lazy-validated).
30
+ botnex_backend_url: str = ""
31
+ botnex_api_key: str = ""
32
+
33
+ # Server identity.
34
+ botnex_mcp_server_name: str = "botnex-mcp-server"
35
+ botnex_mcp_server_port: int = 8001
36
+
37
+ # Logging.
38
+ botnex_log_level: str = "INFO"
39
+
40
+ # Backend HTTP behaviour. Report generation can be slow, default generous.
41
+ botnex_backend_timeout: float = 60.0
42
+ botnex_backend_retry_attempts: int = 3
43
+ botnex_backend_retry_delay: float = 1.0
44
+ botnex_verify_ssl: bool = True
45
+
46
+ # Output caps (keep LLM context manageable).
47
+ botnex_findings_page_size: int = 50
48
+ botnex_cve_result_limit: int = 25
49
+
50
+ @property
51
+ def backend_origin(self) -> str:
52
+ """Backend origin without trailing slash."""
53
+ return self.botnex_backend_url.strip().rstrip("/")
54
+
55
+ @property
56
+ def api_base_url(self) -> str:
57
+ """Full ``/api/v1`` base used by the backend client."""
58
+ return f"{self.backend_origin}/api/v1"
59
+
60
+ def require_backend_config(self) -> None:
61
+ """Raise ConfigurationError when backend credentials are missing.
62
+
63
+ Called by the backend client before the first request, never at
64
+ process start.
65
+ """
66
+ missing = []
67
+ if not self.botnex_backend_url.strip():
68
+ missing.append("BOTNEX_BACKEND_URL")
69
+ if not self.botnex_api_key.strip():
70
+ missing.append("BOTNEX_API_KEY")
71
+ if missing:
72
+ raise ConfigurationError(
73
+ "Missing required environment variable(s): "
74
+ + ", ".join(missing)
75
+ + ". Set the BotNEX backend URL and a user API token "
76
+ "(create one in the BotNEX UI under API Tokens)."
77
+ )
78
+
79
+
80
+ @lru_cache
81
+ def get_settings() -> Settings:
82
+ """Return a cached Settings instance."""
83
+ return Settings()