From ba3374324aa7b90756ddb731bc3eef5b32e34dbe Mon Sep 17 00:00:00 2001 From: Evgeny Kiriyak <224408464+evkir@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:15:58 +0300 Subject: [PATCH 1/4] feat(mcp): MCP server skeleton via mcp-python-sdk --- cyberai/mcp/__init__.py | 0 cyberai/mcp/server.py | 80 +++++++++++++++++++++++++++++++++++++++++ cyberai/mcp/tools.py | 33 +++++++++++++++++ pyproject.toml | 1 + 4 files changed, 114 insertions(+) create mode 100644 cyberai/mcp/__init__.py create mode 100644 cyberai/mcp/server.py create mode 100644 cyberai/mcp/tools.py diff --git a/cyberai/mcp/__init__.py b/cyberai/mcp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cyberai/mcp/server.py b/cyberai/mcp/server.py new file mode 100644 index 0000000..3b3651f --- /dev/null +++ b/cyberai/mcp/server.py @@ -0,0 +1,80 @@ +"""CyberAI MCP server — exposes recon/intel capabilities as MCP tools. + +Uses the official mcp Python SDK (low-level Server API). Tools are defined in +cyberai.mcp.tools as a registry of (Tool spec, sync handler) pairs; this module +wires them into the MCP list_tools / call_tool handlers and serves over stdio +so MCP clients (Claude Desktop, Cursor) can drive CyberAI. +""" + +from __future__ import annotations + +import json +from typing import Any, Dict, List + +from mcp.server import Server +from mcp.server.stdio import stdio_server +from mcp.types import TextContent, Tool + +from cyberai.mcp.tools import TOOL_REGISTRY + +SERVER_NAME = "cyberai" +SERVER_VERSION = "0.4.0" + +server: Server = Server(SERVER_NAME, version=SERVER_VERSION) + + +@server.list_tools() +async def list_tools() -> List[Tool]: + """Advertise all registered CyberAI tools.""" + return [ + Tool( + name=name, + description=spec["description"], + inputSchema=spec["inputSchema"], + ) + for name, spec in TOOL_REGISTRY.items() + ] + + +@server.call_tool() +async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]: + """Dispatch a tool call to its registered sync handler. + + Handlers are plain CyberAI functions; results are JSON-serialized into a + single TextContent block. Unknown tools and handler errors are reported as + text rather than raised, so the client always gets a structured reply. + """ + spec = TOOL_REGISTRY.get(name) + if spec is None: + return [TextContent(type="text", text=json.dumps({"error": f"unknown tool: {name}"}))] + try: + result = spec["handler"](**(arguments or {})) + except Exception as exc: # noqa: BLE001 — surface errors to the client + return [ + TextContent( + type="text", + text=json.dumps({"error": f"{type(exc).__name__}: {exc}"}), + ) + ] + return [TextContent(type="text", text=json.dumps(result, default=str))] + + +async def run_stdio() -> None: + """Serve the CyberAI MCP server over stdio.""" + async with stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + server.create_initialization_options(), + ) + + +def main() -> None: + """Entry point: `python -m cyberai.mcp.server`.""" + import asyncio + + asyncio.run(run_stdio()) + + +if __name__ == "__main__": + main() diff --git a/cyberai/mcp/tools.py b/cyberai/mcp/tools.py new file mode 100644 index 0000000..84770ca --- /dev/null +++ b/cyberai/mcp/tools.py @@ -0,0 +1,33 @@ +"""CyberAI MCP tool registry. + +Each entry maps a tool name to its MCP spec (description + JSON Schema) and a +sync handler. Recon tools land in commit 2, intel tools in commit 3. +""" + +from __future__ import annotations + +from typing import Any, Callable, Dict, TypedDict + + +class ToolSpec(TypedDict): + description: str + inputSchema: Dict[str, Any] + handler: Callable[..., Any] + + +# Populated by register() calls below; recon/intel tools added in later commits. +TOOL_REGISTRY: Dict[str, ToolSpec] = {} + + +def register( + name: str, + description: str, + input_schema: Dict[str, Any], + handler: Callable[..., Any], +) -> None: + """Register a tool in the global MCP registry.""" + TOOL_REGISTRY[name] = ToolSpec( + description=description, + inputSchema=input_schema, + handler=handler, + ) diff --git a/pyproject.toml b/pyproject.toml index ee917f1..53023fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ dependencies = [ "openai>=2.0,<3", "anthropic>=0.28.0,<1", + "mcp>=1.0,<2", "click>=8.1.7,<9", "rich>=13.7.0,<14", "pydantic>=2.7.0,<3", From 09e3562240019ec86e8dafe0cb7ea021ea1eb280 Mon Sep 17 00:00:00 2001 From: Evgeny Kiriyak <224408464+evkir@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:17:26 +0300 Subject: [PATCH 2/4] feat(mcp): expose recon tools as MCP tools --- cyberai/mcp/tools.py | 78 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/cyberai/mcp/tools.py b/cyberai/mcp/tools.py index 84770ca..3a07fd9 100644 --- a/cyberai/mcp/tools.py +++ b/cyberai/mcp/tools.py @@ -31,3 +31,81 @@ def register( inputSchema=input_schema, handler=handler, ) + + +# ── recon tools (day 25 commit 2) ───────────────────────────────────── + +from cyberai.agents.recon.dns_tool import ( # noqa: E402 + detect_subdomains, + run_dns, + run_whois, +) +from cyberai.agents.recon.nmap_tool import run_nmap # noqa: E402 + +register( + name="nmap_scan", + description="Port-scan a target host with nmap and return parsed results.", + input_schema={ + "type": "object", + "properties": { + "target": {"type": "string", "description": "Host or IP to scan"}, + "flags": { + "type": "string", + "description": "nmap flags (whitelisted)", + "default": "-sV -T4 --top-ports 1000", + }, + }, + "required": ["target"], + }, + handler=run_nmap, +) + +register( + name="dns_enum", + description="Resolve DNS records (A/AAAA/MX/NS/TXT) for a domain.", + input_schema={ + "type": "object", + "properties": { + "target": {"type": "string", "description": "Domain to resolve"}, + }, + "required": ["target"], + }, + handler=run_dns, +) + +register( + name="whois_lookup", + description="WHOIS lookup for domain registration info.", + input_schema={ + "type": "object", + "properties": { + "target": {"type": "string", "description": "Domain to query"}, + }, + "required": ["target"], + }, + handler=run_whois, +) + + +def _subdomain_handler(target: str, wordlist: list[str] | None = None) -> dict: + """Adapt detect_subdomains to MCP (wordlist optional).""" + return detect_subdomains(target, wordlist) + + +register( + name="subdomain_enum", + description="Enumerate subdomains for a domain via a wordlist probe.", + input_schema={ + "type": "object", + "properties": { + "target": {"type": "string", "description": "Base domain"}, + "wordlist": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional subdomain candidates", + }, + }, + "required": ["target"], + }, + handler=_subdomain_handler, +) From 9e674af25755ebb1b50024500ecd2769d166b641 Mon Sep 17 00:00:00 2001 From: Evgeny Kiriyak <224408464+evkir@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:44:53 +0300 Subject: [PATCH 3/4] feat(mcp): expose intel tools (cve_search, epss_score) --- cyberai/mcp/tools.py | 59 +++++++++++++++++++++++++++ tests/unit/test_mcp.py | 90 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 tests/unit/test_mcp.py diff --git a/cyberai/mcp/tools.py b/cyberai/mcp/tools.py index 3a07fd9..f49f08a 100644 --- a/cyberai/mcp/tools.py +++ b/cyberai/mcp/tools.py @@ -109,3 +109,62 @@ def _subdomain_handler(target: str, wordlist: list[str] | None = None) -> dict: }, handler=_subdomain_handler, ) + + +# ── intel tools (day 25 commit 3) ───────────────────────────────────── + +from cyberai.agents.intel.epss_client import get_epss_scores # noqa: E402 +from cyberai.agents.intel.nvd_client import get_cve, search_cves # noqa: E402 + +register( + name="cve_search", + description="Search the NVD for CVEs matching a keyword, optionally by severity.", + input_schema={ + "type": "object", + "properties": { + "keyword": {"type": "string", "description": "Search term"}, + "max_results": { + "type": "integer", + "description": "Max CVEs to return", + "default": 10, + }, + "severity": { + "type": "string", + "enum": ["CRITICAL", "HIGH", "MEDIUM", "LOW"], + "description": "Filter by CVSS v3 severity", + }, + }, + "required": ["keyword"], + }, + handler=search_cves, +) + +register( + name="cve_detail", + description="Fetch a single CVE by id (e.g. CVE-2021-44228).", + input_schema={ + "type": "object", + "properties": { + "cve_id": {"type": "string", "description": "CVE identifier"}, + }, + "required": ["cve_id"], + }, + handler=get_cve, +) + +register( + name="epss_score", + description="Fetch EPSS exploitation-probability scores for CVE ids.", + input_schema={ + "type": "object", + "properties": { + "cve_ids": { + "type": "array", + "items": {"type": "string"}, + "description": "List of CVE identifiers", + }, + }, + "required": ["cve_ids"], + }, + handler=get_epss_scores, +) diff --git a/tests/unit/test_mcp.py b/tests/unit/test_mcp.py new file mode 100644 index 0000000..0488ff0 --- /dev/null +++ b/tests/unit/test_mcp.py @@ -0,0 +1,90 @@ +"""Day 25 — MCP server: tool registry, list_tools, call_tool dispatch.""" + +from __future__ import annotations + +import asyncio +import json + +from cyberai.mcp.server import call_tool, list_tools +from cyberai.mcp.tools import TOOL_REGISTRY, register + + +# ── registry ────────────────────────────────────────────────────────── + + +def test_recon_tools_registered(): + assert {"nmap_scan", "dns_enum", "whois_lookup", "subdomain_enum"} <= set(TOOL_REGISTRY) + + +def test_intel_tools_registered(): + assert {"cve_search", "cve_detail", "epss_score"} <= set(TOOL_REGISTRY) + + +def test_all_schemas_are_objects_with_required(): + for name, spec in TOOL_REGISTRY.items(): + sch = spec["inputSchema"] + assert sch["type"] == "object", name + assert "properties" in sch, name + assert isinstance(spec["description"], str) and spec["description"], name + + +# ── list_tools ──────────────────────────────────────────────────────── + + +def test_list_tools_returns_mcp_tools(): + tools = asyncio.run(list_tools()) + names = {t.name for t in tools} + assert "nmap_scan" in names and "cve_search" in names + # every advertised tool carries a non-empty input schema + assert all(t.inputSchema for t in tools) + + +# ── call_tool dispatch ──────────────────────────────────────────────── + + +def test_call_tool_dispatches_to_handler(): + register( + "echo_test", + "echo", + {"type": "object", "properties": {"x": {"type": "string"}}}, + lambda x="": {"echoed": x}, + ) + try: + res = asyncio.run(call_tool("echo_test", {"x": "hi"})) + assert json.loads(res[0].text) == {"echoed": "hi"} + finally: + TOOL_REGISTRY.pop("echo_test", None) + + +def test_call_tool_unknown_is_graceful(): + res = asyncio.run(call_tool("does_not_exist", {})) + assert "unknown tool" in res[0].text + + +def test_call_tool_handler_error_is_graceful(): + register( + "boom_test", + "raises", + {"type": "object", "properties": {}}, + lambda: 1 / 0, + ) + try: + res = asyncio.run(call_tool("boom_test", {})) + assert "ZeroDivisionError" in res[0].text + finally: + TOOL_REGISTRY.pop("boom_test", None) + + +def test_call_tool_result_is_json_text(): + register( + "list_test", + "returns list", + {"type": "object", "properties": {}}, + lambda: {"items": [1, 2, 3]}, + ) + try: + res = asyncio.run(call_tool("list_test", {})) + assert res[0].type == "text" + assert json.loads(res[0].text) == {"items": [1, 2, 3]} + finally: + TOOL_REGISTRY.pop("list_test", None) From db3b1dad5a6fee2b489e4a528d441b60e89d697f Mon Sep 17 00:00:00 2001 From: Evgeny Kiriyak <224408464+evkir@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:46:08 +0300 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20MCP=20integration=20guide=20?= =?UTF-8?q?=E2=80=94=20Claude=20Desktop=20/=20Cursor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/mcp/integration.md | 99 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 docs/mcp/integration.md diff --git a/docs/mcp/integration.md b/docs/mcp/integration.md new file mode 100644 index 0000000..594c4cc --- /dev/null +++ b/docs/mcp/integration.md @@ -0,0 +1,99 @@ +# CyberAI MCP Server — Integration Guide + +The CyberAI MCP server exposes reconnaissance and threat-intel capabilities as +[Model Context Protocol](https://modelcontextprotocol.io) tools, so MCP clients +such as Claude Desktop and Cursor can drive CyberAI directly. + +## Available tools + +| Tool | Purpose | Required args | +|------|---------|---------------| +| `nmap_scan` | Port-scan a host with nmap | `target` | +| `dns_enum` | Resolve DNS records | `target` | +| `whois_lookup` | WHOIS registration lookup | `target` | +| `subdomain_enum` | Enumerate subdomains | `target` | +| `cve_search` | Search NVD by keyword | `keyword` | +| `cve_detail` | Fetch one CVE by id | `cve_id` | +| `epss_score` | EPSS exploitation scores | `cve_ids` | + +## Prerequisites + +- Python 3.11+ with CyberAI installed (`pip install -e .` from the repo root). +- `nmap` on `PATH` for `nmap_scan`. +- `NVD_API_KEY` in the environment for higher NVD rate limits (optional). + +## Running the server + +The server speaks MCP over stdio: + +```bash +python -m cyberai.mcp.server +``` + +It does not print anything on its own — it waits for an MCP client to connect +over stdin/stdout. To run a quick local smoke test, use the MCP Inspector: + +```bash +npx @modelcontextprotocol/inspector python -m cyberai.mcp.server +``` + +## Claude Desktop + +Edit the Claude Desktop config file: + +- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` +- Linux: `~/.config/Claude/claude_desktop_config.json` +- Windows: `%APPDATA%\Claude\claude_desktop_config.json` + +Add a `cyberai` entry under `mcpServers`: + +```json +{ + "mcpServers": { + "cyberai": { + "command": "python", + "args": ["-m", "cyberai.mcp.server"], + "env": { + "NVD_API_KEY": "your-key-here" + } + } + } +} +``` + +Restart Claude Desktop. The CyberAI tools appear in the tool picker; you can ask +Claude to run a scan or look up a CVE, and it will call the tools. + +## Cursor + +Cursor reads MCP servers from `~/.cursor/mcp.json` (global) or +`.cursor/mcp.json` (per-project): + +```json +{ + "mcpServers": { + "cyberai": { + "command": "python", + "args": ["-m", "cyberai.mcp.server"] + } + } +} +``` + +Use an absolute path to the Python interpreter from the CyberAI environment if +`python` on `PATH` is not the right one, e.g. +`/home/you/repo/CyberAI/.venv/bin/python`. + +## Scope and safety + +These tools run real reconnaissance against whatever target the client supplies. +Only point them at systems you are authorized to test. `nmap_scan` enforces a +flag whitelist; the other tools perform read-only lookups. + +## Troubleshooting + +- **No tools appear** — confirm the server starts without import errors: + `python -c "import cyberai.mcp.server"`. +- **`nmap_scan` returns an error** — ensure `nmap` is installed and on `PATH`. +- **NVD rate-limit errors** — set `NVD_API_KEY`; without it NVD allows far fewer + requests per minute.