"""MCP server for the NetScope network scanner.

Exposes internet-scale scan intelligence to AI assistants via the
Model Context Protocol.  Supports both stdio and Streamable HTTP
transports (MCP spec 2025-03-26).

Usage:
    netscope-mcp                                       # stdio (default)
    netscope-mcp --transport streamable-http           # HTTP on 127.0.0.1:8080
    netscope-mcp --transport streamable-http --port 9000
"""

from __future__ import annotations

import argparse
import asyncio
import logging
import os
import sys
import time
from collections import deque
from threading import Lock
from typing import Any

import mcp.server.stdio
from mcp import types
from mcp.server.lowlevel import NotificationOptions, Server
from mcp.server.models import InitializationOptions

from netscope_mcp import __version__
from netscope_mcp import api_client
from netscope_mcp import formatters
from netscope_mcp import validators
from netscope_mcp.validators import ValidationError

logging.basicConfig(level=logging.WARNING, stream=sys.stderr)
logger = logging.getLogger("netscope-mcp")

_INSTRUCTIONS = """\
You have access to NetScope — an internet-scale network scanner with \
Shodan-style search across discovered hosts, services, banners, TLS \
certificates, HTTP responses, SNMP data, and Nuclei security findings.

TOOL SELECTION GUIDE:
- User asks about services on a network/port/product → search_services
- User asks about hosts by IP/country/ASN → search_hosts
- User wants full detail on a specific service (banner, TLS, findings) → get_service
- User wants everything about an IP (all ports, SNMP, GeoIP) → get_host
- User asks "what's the distribution of ports/services/countries?" → get_facets
- User wants a high-level overview of the scan data → get_dashboard
- User asks "what vulnerabilities were found?" or "what's exposed?" → get_findings
- User asks about web applications or HTTP titles → get_webapps
- User asks about open ports across the network → get_ports
- User wants to browse all products/services/countries/technologies → explore
- User asks about the scanning infrastructure or drones → get_drones
- User asks about Nuclei templates or specific CVE templates → search_templates
- User asks about screenshots or what sites look like → get_gallery

SEARCH SYNTAX (Shodan-style operators for query parameters):
  port:443          ip:10.0.0.0/24       service:http
  product:nginx     tech:WordPress        country:US
  title:"Login"     banner:OpenSSH        status:200
  has:screenshot    has:nuclei            has:tls
  severity:critical finding:CVE-2024-*    hostname:DC*
  os:Windows        signing:disabled      seen:<7d

MULTI-TOOL WORKFLOWS:
- "Tell me about 10.0.0.1": get_host for overview, then get_service on interesting ports
- "What's vulnerable?": get_findings for top issues, then search_services to find affected hosts
- "Show me the attack surface": get_dashboard + get_findings + get_ports
- "Any web apps?": get_webapps, then get_gallery for visual confirmation
"""

server = Server("netscope-mcp", version=__version__, instructions=_INSTRUCTIONS)


# ---------------------------------------------------------------------------
# Local safety controls (copied from eip-mcp)
# ---------------------------------------------------------------------------

class LocalRateLimitError(Exception):
    def __init__(self, retry_after_s: float):
        self.retry_after_s = max(0.0, float(retry_after_s))
        super().__init__(f"Rate limited. Retry after {self.retry_after_s:.1f}s")


class RateLimiter:
    """Simple sliding-window limiter for tool calls."""

    def __init__(self, max_calls: int, period_s: float):
        self.max_calls = int(max_calls)
        self.period_s = float(period_s)
        self._times: deque[float] = deque()
        self._lock = Lock()

    def enabled(self) -> bool:
        return self.max_calls > 0 and self.period_s > 0

    def check_or_raise(self) -> None:
        if not self.enabled():
            return
        now = time.monotonic()
        with self._lock:
            cutoff = now - self.period_s
            while self._times and self._times[0] < cutoff:
                self._times.popleft()
            if len(self._times) >= self.max_calls:
                retry_after = (self._times[0] + self.period_s) - now
                raise LocalRateLimitError(retry_after)
            self._times.append(now)


def _int_env(name: str, default: int) -> int:
    raw = os.environ.get(name)
    if raw is None or raw == "":
        return default
    try:
        return int(raw)
    except ValueError:
        return default


def _float_env(name: str, default: float) -> float:
    raw = os.environ.get(name)
    if raw is None or raw == "":
        return default
    try:
        return float(raw)
    except ValueError:
        return default


_MAX_CONCURRENCY = max(0, _int_env("NETSCOPE_MCP_MAX_CONCURRENCY", 4))
_CONCURRENCY_SEM: asyncio.Semaphore | None = None

_MAX_CALLS = max(0, _int_env("NETSCOPE_MCP_MAX_CALLS", 60))
_CALL_PERIOD_S = max(0.0, _float_env("NETSCOPE_MCP_CALL_PERIOD_SECONDS", 60.0))
_RATE_LIMITER = RateLimiter(_MAX_CALLS, _CALL_PERIOD_S)


# ---------------------------------------------------------------------------
# Tool definitions
# ---------------------------------------------------------------------------

TOOLS = [
    types.Tool(
        name="search_services",
        description=(
            "Search NetScope for discovered network services using Shodan-style "
            "query syntax. Returns matching services with IP, port, banner, TLS, "
            "HTTP info, technologies, and GeoIP. Supports operators like "
            "port:443, ip:10.0.0.0/24, service:http, product:nginx, tech:WordPress, "
            "country:US, title:\"Login\", has:screenshot, severity:critical, "
            "finding:CVE-*, seen:<7d. "
            "Use this for broad searches like 'port:443 country:US' or "
            "'product:apache has:nuclei'."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Shodan-style search query (e.g. 'port:443 tech:nginx')"},
                "limit": {"type": "integer", "description": "Results per page (1-200, default: 50)"},
                "offset": {"type": "integer", "description": "Pagination offset (default: 0)"},
                "order": {"type": "string", "enum": ["last_seen", "first_seen", "port", "ip"], "description": "Sort order (default: last_seen)"},
            },
        },
    ),
    types.Tool(
        name="search_hosts",
        description=(
            "Search for hosts (grouped by IP) with aggregated port and service "
            "information. Each result shows the IP, all open ports, service names, "
            "GeoIP, and last seen time. Use when you need a host-level view rather "
            "than per-service results. Same Shodan-style query syntax as search_services."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Shodan-style search query"},
                "limit": {"type": "integer", "description": "Results per page (1-200, default: 50)"},
                "offset": {"type": "integer", "description": "Pagination offset (default: 0)"},
            },
        },
    ),
    types.Tool(
        name="get_service",
        description=(
            "Get full detail for a specific service by its service_id. Returns "
            "complete banner, HTTP response, TLS certificate, technologies, "
            "enumeration data (SMB, hostnames, domains, OS), screenshots, and "
            "all Nuclei security findings for that service. Use when you have a "
            "service_id from search results and want deep inspection."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "service_id": {"type": "integer", "description": "Service ID from search results"},
            },
            "required": ["service_id"],
        },
    ),
    types.Tool(
        name="get_host",
        description=(
            "Get complete information for a specific IP address: all open services, "
            "host-level metadata (GeoIP, ASN, reverse DNS, open port count), and "
            "SNMP data if available (sysDescr, sysName, sysContact, sysLocation). "
            "Use when you need everything about a specific IP."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "ip": {"type": "string", "description": "IP address (e.g. '10.0.0.1')"},
            },
            "required": ["ip"],
        },
    ),
    types.Tool(
        name="get_facets",
        description=(
            "Get aggregated facet counts for a search query — distribution of "
            "services, ports, products, technologies, countries, HTTP status codes, "
            "and finding severities. Empty query returns global facets (cached, instant). "
            "Use for overview questions like 'what's the port distribution?' or "
            "'what countries are in this network?'."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Shodan-style search query (empty = global facets)"},
                "top_n": {"type": "integer", "description": "Number of top values per facet (1-100, default: 20)"},
            },
        },
    ),
    types.Tool(
        name="get_dashboard",
        description=(
            "Get a high-level dashboard with scan statistics (total hosts, services, "
            "findings) and default facet distributions. Use as a starting point to "
            "understand the scope and composition of the scan data."
        ),
        inputSchema={"type": "object", "properties": {}},
    ),
    types.Tool(
        name="get_findings",
        description=(
            "Get top security findings from Nuclei scans, grouped by template and "
            "ordered by severity (critical first). Shows affected host count per "
            "finding. Includes both CVE-based findings and non-CVE issues like "
            "misconfigurations, exposures, and technology detections. Optionally "
            "filter by search query to see findings for a specific network segment. "
            "Example: query='ip:10.0.0.0/24' to see findings for a subnet."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Shodan-style query to filter findings (optional)"},
                "limit": {"type": "integer", "description": "Number of top findings (1-100, default: 20)"},
            },
        },
    ),
    types.Tool(
        name="get_webapps",
        description=(
            "Get top web applications (HTTP titles) ranked by host count. Excludes "
            "error pages and redirects. Use to discover what web applications are "
            "running across the scanned network. Optionally filter by search query."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Shodan-style query to filter (optional)"},
                "limit": {"type": "integer", "description": "Number of results (1-200, default: 50)"},
            },
        },
    ),
    types.Tool(
        name="get_ports",
        description=(
            "Get top open ports across all scanned hosts, with the primary service "
            "name for each port. Use to understand the network's port landscape — "
            "which ports are most common and what services run on them."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "limit": {"type": "integer", "description": "Number of top ports (1-200, default: 50)"},
            },
        },
    ),
    types.Tool(
        name="explore",
        description=(
            "Browse full listings for a category — all products, services, HTTP "
            "titles, countries, security findings, ports, technologies, or scan "
            "history. Returns every unique value with host counts. Use when you "
            "need the complete picture, not just top results. "
            "Valid categories: products, services, titles, countries, findings, "
            "ports, technologies, scans."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "category": {
                    "type": "string",
                    "enum": ["products", "services", "titles", "countries",
                             "findings", "ports", "technologies", "scans"],
                    "description": "Category to explore",
                },
            },
            "required": ["category"],
        },
    ),
    types.Tool(
        name="get_drones",
        description=(
            "List all scanning drones in the fleet with their status (idle, "
            "scanning, offline), IP address, capabilities (zmap, zgrab2, nuclei, "
            "screenshots, SNMP, etc.), and current scan assignment. Use to check "
            "scanner health and capacity."
        ),
        inputSchema={"type": "object", "properties": {}},
    ),
    types.Tool(
        name="search_templates",
        description=(
            "Search the Nuclei template catalog (12K+ templates). Filter by "
            "text search (template ID, name, description, CVE), severity level, "
            "or category. Use to find available detection templates for specific "
            "vulnerabilities or technologies. "
            "Examples: query='CVE-2024' for recent CVE templates; "
            "severity='critical' for critical-only; query='apache' for Apache-related."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Search text (template ID, name, description, CVE)"},
                "severity": {"type": "string", "enum": ["info", "low", "medium", "high", "critical"], "description": "Filter by severity"},
                "category": {"type": "string", "description": "Filter by template category"},
                "limit": {"type": "integer", "description": "Results per page (1-200, default: 50)"},
                "offset": {"type": "integer", "description": "Pagination offset (default: 0)"},
            },
        },
    ),
    types.Tool(
        name="get_gallery",
        description=(
            "Get services that have HTTP or RDP screenshots. Returns service "
            "metadata plus screenshot details (type, final URL, final title). "
            "Optionally filter by search query. Use to visually identify web "
            "applications, login pages, or interesting services. "
            "Example: query='title:\"Login\"' to see login page screenshots."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Shodan-style query to filter screenshots"},
                "limit": {"type": "integer", "description": "Results per page (1-96, default: 48)"},
                "offset": {"type": "integer", "description": "Pagination offset (default: 0)"},
            },
        },
    ),
]


# ---------------------------------------------------------------------------
# Tool registration
# ---------------------------------------------------------------------------

@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
    return TOOLS


@server.call_tool()
async def handle_call_tool(name: str, arguments: dict[str, Any] | None):
    """Route tool calls with validation, rate limiting, and error handling."""
    args = arguments or {}

    try:
        _RATE_LIMITER.check_or_raise()

        global _CONCURRENCY_SEM
        if _MAX_CONCURRENCY and _CONCURRENCY_SEM is None:
            _CONCURRENCY_SEM = asyncio.Semaphore(_MAX_CONCURRENCY)

        if _CONCURRENCY_SEM is None:
            result = await asyncio.to_thread(_dispatch, name, args)
        else:
            async with _CONCURRENCY_SEM:
                result = await asyncio.to_thread(_dispatch, name, args)

        return _tool_result(result, is_error=False)

    except LocalRateLimitError as exc:
        return _tool_result(
            f"Rate limited locally. Retry after {exc.retry_after_s:.1f}s.",
            is_error=True,
        )
    except ValidationError as exc:
        return _tool_result(f"Validation error: {exc}", is_error=True)
    except api_client.APIError as exc:
        if exc.status_code == 404:
            ident = args.get("service_id", args.get("ip", "?"))
            return _tool_result(f"Not found: {ident}", is_error=True)
        if exc.status_code == 401:
            return _tool_result("Authentication failed — check NETSCOPE_USER and NETSCOPE_PASS.", is_error=True)
        if exc.status_code == 429:
            return _tool_result(f"Rate limited by NetScope API. {exc.message}", is_error=True)
        if exc.status_code == 0:
            return _tool_result(f"Network error contacting NetScope. {exc.message}", is_error=True)
        return _tool_result(f"API error: {exc.message}", is_error=True)
    except Exception:
        logger.exception("Unexpected error in tool %s", name)
        return _tool_result("Internal error processing request. Please try again.", is_error=True)


def _tool_result(text: str, *, is_error: bool) -> Any:
    content = [types.TextContent(type="text", text=text)]
    if hasattr(types, "CallToolResult"):
        try:
            return types.CallToolResult(content=content, isError=is_error)
        except TypeError:
            return types.CallToolResult(content=content, is_error=is_error)  # type: ignore[arg-type]
    return content


# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------

def _dispatch(name: str, args: dict[str, Any]) -> str:
    if name == "search_services":
        return _tool_search_services(args)
    elif name == "search_hosts":
        return _tool_search_hosts(args)
    elif name == "get_service":
        return _tool_get_service(args)
    elif name == "get_host":
        return _tool_get_host(args)
    elif name == "get_facets":
        return _tool_get_facets(args)
    elif name == "get_dashboard":
        return _tool_get_dashboard()
    elif name == "get_findings":
        return _tool_get_findings(args)
    elif name == "get_webapps":
        return _tool_get_webapps(args)
    elif name == "get_ports":
        return _tool_get_ports(args)
    elif name == "explore":
        return _tool_explore(args)
    elif name == "get_drones":
        return _tool_get_drones()
    elif name == "search_templates":
        return _tool_search_templates(args)
    elif name == "get_gallery":
        return _tool_get_gallery(args)
    else:
        raise ValidationError(f"Unknown tool: {name}")


# ---------------------------------------------------------------------------
# Tool handlers
# ---------------------------------------------------------------------------

def _tool_search_services(args: dict) -> str:
    params: dict[str, Any] = {}
    if args.get("query"):
        params["q"] = validators.validate_query(args["query"])
    if args.get("limit") is not None:
        params["limit"] = validators.validate_limit(args["limit"])
    if args.get("offset") is not None:
        params["offset"] = validators.validate_offset(args["offset"])
    if args.get("order"):
        params["order"] = validators.validate_sort(args["order"])

    data = api_client.search_services(params)
    return formatters.format_services(data)


def _tool_search_hosts(args: dict) -> str:
    params: dict[str, Any] = {}
    if args.get("query"):
        params["q"] = validators.validate_query(args["query"])
    if args.get("limit") is not None:
        params["limit"] = validators.validate_limit(args["limit"])
    if args.get("offset") is not None:
        params["offset"] = validators.validate_offset(args["offset"])

    data = api_client.search_hosts(params)
    return formatters.format_hosts(data)


def _tool_get_service(args: dict) -> str:
    service_id = validators.validate_service_id(args.get("service_id", ""))
    data = api_client.get_service_detail(service_id)
    return formatters.format_service_detail(data)


def _tool_get_host(args: dict) -> str:
    ip = validators.validate_ip(args.get("ip", ""))
    data = api_client.get_host_detail(ip)
    return formatters.format_host_detail(data)


def _tool_get_facets(args: dict) -> str:
    params: dict[str, Any] = {}
    if args.get("query"):
        params["q"] = validators.validate_query(args["query"])
    if args.get("top_n") is not None:
        params["top_n"] = validators.validate_limit(args["top_n"], max_val=100, default=20)

    data = api_client.get_facets(params)
    return formatters.format_facets(data)


def _tool_get_dashboard() -> str:
    data = api_client.get_dashboard()
    return formatters.format_dashboard(data)


def _tool_get_findings(args: dict) -> str:
    params: dict[str, Any] = {}
    if args.get("query"):
        params["q"] = validators.validate_query(args["query"])
    if args.get("limit") is not None:
        params["limit"] = validators.validate_limit(args["limit"], max_val=100, default=20)

    data = api_client.get_findings(params)
    return formatters.format_findings(data)


def _tool_get_webapps(args: dict) -> str:
    params: dict[str, Any] = {}
    if args.get("query"):
        params["q"] = validators.validate_query(args["query"])
    if args.get("limit") is not None:
        params["limit"] = validators.validate_limit(args["limit"])

    data = api_client.get_webapps(params)
    return formatters.format_webapps(data)


def _tool_get_ports(args: dict) -> str:
    params: dict[str, Any] = {}
    if args.get("limit") is not None:
        params["limit"] = validators.validate_limit(args["limit"])

    data = api_client.get_ports(params)
    return formatters.format_ports(data)


def _tool_explore(args: dict) -> str:
    category = validators.validate_explore_category(args.get("category", ""))
    data = api_client.get_explore(category)
    return formatters.format_explore(data)


def _tool_get_drones() -> str:
    data = api_client.get_drones()
    return formatters.format_drones(data)


def _tool_search_templates(args: dict) -> str:
    params: dict[str, Any] = {}
    if args.get("query"):
        params["q"] = validators.validate_query(args["query"])
    if args.get("severity"):
        params["severity"] = validators.validate_severity(args["severity"])
    if args.get("category"):
        params["category"] = validators.clean_string(args["category"])
    if args.get("limit") is not None:
        params["limit"] = validators.validate_limit(args["limit"])
    if args.get("offset") is not None:
        params["offset"] = validators.validate_offset(args["offset"])

    data = api_client.search_templates(params)
    return formatters.format_templates(data)


def _tool_get_gallery(args: dict) -> str:
    params: dict[str, Any] = {}
    if args.get("query"):
        params["q"] = validators.validate_query(args["query"])
    if args.get("limit") is not None:
        params["limit"] = validators.validate_limit(args["limit"], max_val=96, default=48)
    if args.get("offset") is not None:
        params["offset"] = validators.validate_offset(args["offset"])

    data = api_client.get_gallery(params)
    return formatters.format_gallery(data)


# ---------------------------------------------------------------------------
# Transport: stdio
# ---------------------------------------------------------------------------

async def _run_stdio() -> None:
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="netscope-mcp",
                server_version=__version__,
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )


# ---------------------------------------------------------------------------
# Transport: Streamable HTTP
# ---------------------------------------------------------------------------

def _run_http_server(*, host: str, port: int, stateless: bool) -> None:
    try:
        import contextlib
        from collections.abc import AsyncIterator

        import uvicorn
        from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
        from mcp.server.transport_security import TransportSecuritySettings
        from starlette.applications import Starlette
        from starlette.middleware.cors import CORSMiddleware
        from starlette.routing import Mount
        from starlette.types import Receive, Scope, Send
    except ImportError as exc:
        logger.error(
            "Streamable HTTP transport requires additional packages.  "
            "Install them with:  pip install 'netscope-mcp[http]'"
        )
        raise SystemExit(1) from exc

    allowed_hosts: set[str] = set()
    allowed_hosts.add(f"{host}:{port}")
    if host in ("127.0.0.1", "localhost", "::1", "0.0.0.0", "::"):
        for alias in ("127.0.0.1", "localhost"):
            allowed_hosts.add(f"{alias}:{port}")

    extra_hosts = os.environ.get("NETSCOPE_MCP_ALLOWED_HOSTS", "")
    for h in extra_hosts.split(","):
        h = h.strip()
        if h:
            allowed_hosts.add(h)

    allowed_origins: list[str] = []
    extra_origins = os.environ.get("NETSCOPE_MCP_ALLOWED_ORIGINS", "")
    for o in extra_origins.split(","):
        o = o.strip()
        if o:
            allowed_origins.append(o)

    security_settings = TransportSecuritySettings(
        enable_dns_rebinding_protection=True,
        allowed_hosts=sorted(allowed_hosts),
        allowed_origins=allowed_origins,
    )

    sm_kwargs: dict[str, Any] = dict(
        app=server,
        json_response=False,
        stateless=stateless,
        security_settings=security_settings,
    )
    import inspect
    _sm_params = inspect.signature(StreamableHTTPSessionManager.__init__).parameters
    if "session_idle_timeout" in _sm_params and not stateless:
        sm_kwargs["session_idle_timeout"] = 1800.0

    session_manager = StreamableHTTPSessionManager(**sm_kwargs)

    async def handle_mcp(scope: Scope, receive: Receive, send: Send) -> None:
        await session_manager.handle_request(scope, receive, send)

    @contextlib.asynccontextmanager
    async def lifespan(_app: Starlette) -> AsyncIterator[None]:
        async with session_manager.run():
            logger.warning(
                "netscope-mcp v%s  transport=streamable-http  "
                "endpoint=http://%s:%d/mcp  mode=%s",
                __version__, host, port,
                "stateless" if stateless else "stateful",
            )
            yield

    starlette_app = Starlette(
        debug=False,
        routes=[Mount("/mcp", app=handle_mcp)],
        lifespan=lifespan,
    )

    cors_app = CORSMiddleware(
        starlette_app,
        allow_origins=allowed_origins,
        allow_methods=["GET", "POST", "DELETE"],
        allow_headers=[
            "Content-Type", "Accept", "Mcp-Session-Id",
            "Last-Event-Id", "Mcp-Protocol-Version",
        ],
        expose_headers=["Mcp-Session-Id"],
    )

    uvicorn.run(cors_app, host=host, port=port, log_level="warning")  # type: ignore[arg-type]


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def _parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(
        prog="netscope-mcp",
        description="MCP server for the NetScope network scanner",
    )
    p.add_argument("--version", "-V", action="version", version=f"netscope-mcp {__version__}")
    p.add_argument(
        "--transport",
        choices=["stdio", "streamable-http"],
        default=os.environ.get("NETSCOPE_MCP_TRANSPORT", "stdio"),
        help="Transport protocol (default: stdio). Env: NETSCOPE_MCP_TRANSPORT",
    )
    p.add_argument(
        "--host",
        default=os.environ.get("NETSCOPE_MCP_HOST", "127.0.0.1"),
        help="Bind address for HTTP transport (default: 127.0.0.1). Env: NETSCOPE_MCP_HOST",
    )
    p.add_argument(
        "--port", type=int,
        default=_int_env("NETSCOPE_MCP_PORT", 8080),
        help="Bind port for HTTP transport (default: 8080). Env: NETSCOPE_MCP_PORT",
    )
    p.add_argument(
        "--stateless", action="store_true",
        default=os.environ.get("NETSCOPE_MCP_STATELESS", "").lower() in ("1", "true", "yes"),
        help="Disable session tracking. Env: NETSCOPE_MCP_STATELESS",
    )
    return p.parse_args()


def main():
    """Entry point for the MCP server."""
    args = _parse_args()
    if args.transport == "streamable-http":
        _run_http_server(host=args.host, port=args.port, stateless=args.stateless)
    else:
        asyncio.run(_run_stdio())


if __name__ == "__main__":
    main()
