Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added cyberai/mcp/__init__.py
Empty file.
80 changes: 80 additions & 0 deletions cyberai/mcp/server.py
Original file line number Diff line number Diff line change
@@ -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()
170 changes: 170 additions & 0 deletions cyberai/mcp/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""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,
)


# ── 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,
)


# ── 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,
)
99 changes: 99 additions & 0 deletions docs/mcp/integration.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading