name: "shodan_search"
command: "python3"
args:
  - "-c"
  - |
    import sys
    import json
    import requests
    import os
    import math

    # ==================== Shodan配置 ====================
    # 请在此处配置您的Shodan API Key
    # 您也可以在环境变量中设置：SHODAN_API_KEY
    # enable 默认为 false，需开启才能调用该MCP
    SHODAN_API_KEY = ""  # 请替换为您自己的Shodan API Key
    # ==================================================

    # Shodan API基础URL
    base_url = "https://api.shodan.io"

    # 解析参数（从JSON字符串或命令行参数）
    def parse_args():
        # 尝试从第一个参数读取JSON配置
        if len(sys.argv) > 1:
            try:
                arg1 = str(sys.argv[1])
                config = json.loads(arg1)
                if isinstance(config, dict):
                    return config
            except (json.JSONDecodeError, TypeError, ValueError):
                pass

        # 传统位置参数方式（向后兼容）
        # 兼容两种序列：
        # 1) query,page,facets,minify,fields,count_only,size
        # 2) query,page,minify,fields,count_only,size   (facets省略时执行器会压缩参数)
        config = {}
        if len(sys.argv) > 1:
            config["query"] = str(sys.argv[1])
        if len(sys.argv) > 2:
            try:
                config["page"] = int(sys.argv[2])
            except (ValueError, TypeError):
                pass

        def is_bool_like(val):
            if isinstance(val, bool):
                return True
            if not isinstance(val, str):
                return False
            return val.strip().lower() in ("true", "false", "1", "0", "yes", "no")

        remaining = [str(x) for x in sys.argv[3:]]
        if remaining:
            # facets 省略时，第一个剩余参数通常是 minify（布尔）
            first_is_bool = is_bool_like(remaining[0])
            idx = 0
            if not first_is_bool:
                config["facets"] = remaining[idx]
                idx += 1

            if idx < len(remaining):
                val = remaining[idx]
                config["minify"] = val.lower() in ("true", "1", "yes")
                idx += 1

            if idx < len(remaining):
                config["fields"] = remaining[idx]
                idx += 1

            if idx < len(remaining):
                val = remaining[idx]
                config["count_only"] = val.lower() in ("true", "1", "yes")
                idx += 1

            if idx < len(remaining):
                try:
                    config["size"] = int(remaining[idx])
                except (ValueError, TypeError):
                    pass

        return config

    def normalize_bool(value, default_value):
        if value is None:
            return default_value
        if isinstance(value, bool):
            return value
        if isinstance(value, str):
            return value.lower() in ("true", "1", "yes")
        if isinstance(value, (int, float)):
            return value != 0
        return default_value

    try:
        config = parse_args()

        if not isinstance(config, dict):
            error_result = {
                "status": "error",
                "message": f"参数解析错误: 期望字典类型，但得到 {type(config).__name__}",
                "type": "TypeError"
            }
            print(json.dumps(error_result, ensure_ascii=False, indent=2))
            sys.exit(1)

        api_key = os.getenv("SHODAN_API_KEY", SHODAN_API_KEY).strip()
        query = str(config.get("query", "")).strip()

        if not api_key:
            error_result = {
                "status": "error",
                "message": "缺少Shodan配置: api_key（Shodan API密钥）",
                "required_config": ["api_key"],
                "note": "请在YAML文件的SHODAN_API_KEY配置项中填写您的API密钥，或在环境变量SHODAN_API_KEY中设置。API密钥可在Shodan账户页面查看: https://account.shodan.io/"
            }
            print(json.dumps(error_result, ensure_ascii=False, indent=2))
            sys.exit(1)

        if not query:
            error_result = {
                "status": "error",
                "message": "缺少必需参数: query（搜索查询语句）",
                "required_params": ["query"],
                "examples": [
                    "product:nginx",
                    "apache country:DE",
                    "port:22",
                    "ssl.cert.subject.cn:example.com",
                    "org:\"Amazon\" port:443"
                ]
            }
            print(json.dumps(error_result, ensure_ascii=False, indent=2))
            sys.exit(1)

        count_only = normalize_bool(config.get("count_only"), False)
        minify = normalize_bool(config.get("minify"), True)
        requested_size = config.get("size", None)
        if requested_size is not None:
            try:
                requested_size = int(requested_size)
                if requested_size <= 0:
                    requested_size = None
                else:
                    # 防止单次请求过大导致额度和响应时间问题
                    requested_size = min(requested_size, 1000)
            except (ValueError, TypeError):
                requested_size = None

        # 根据 count_only 选择搜索端点
        endpoint = "/shodan/host/count" if count_only else "/shodan/host/search"
        url = f"{base_url}{endpoint}"

        params = {
            "key": api_key,
            "query": query
        }

        # 可选参数 facets（search 和 count 都支持）
        if "facets" in config and config["facets"]:
            facets_value = str(config["facets"]).strip()
            if facets_value:
                params["facets"] = facets_value

        # search 接口的可选参数
        if not count_only:
            if "page" in config and config["page"] is not None:
                try:
                    page = int(config["page"])
                    if page > 0:
                        params["page"] = page
                except (ValueError, TypeError):
                    pass
            minify_effective = minify

            if "fields" in config and config["fields"]:
                fields_value = str(config["fields"]).strip()
                if fields_value:
                    params["fields"] = fields_value
                    # Shodan API约束：fields 与 minify=true 互斥
                    minify_effective = False

            params["minify"] = "true" if minify_effective else "false"

        try:
            if count_only:
                response = requests.get(url, params=params, timeout=30)
                response.raise_for_status()
                result_data = response.json()

                if isinstance(result_data, dict) and result_data.get("error"):
                    error_result = {
                        "status": "error",
                        "message": f"Shodan API错误: {result_data.get('error', '未知错误')}",
                        "suggestion": "请检查API密钥、查询语法和账户查询额度"
                    }
                    print(json.dumps(error_result, ensure_ascii=False, indent=2))
                    sys.exit(1)

                output = {
                    "status": "success",
                    "mode": "count",
                    "query": query,
                    "total": result_data.get("total", 0),
                    "facets": result_data.get("facets", {}),
                    "size": requested_size,
                    "note": "count模式仅返回统计，不返回明细结果",
                    "message": "统计查询完成（未返回资产明细）"
                }
            else:
                start_page = int(params.get("page", 1))
                # Shodan search 每页固定最多100条
                # 如果未指定 size，则保持原始行为（单页）
                target_size = requested_size if requested_size else 100
                pages_needed = 1 if not requested_size else max(1, int(math.ceil(target_size / 100.0)))

                all_matches = []
                last_result_data = {}
                current_page = start_page
                pages_fetched = 0

                for _ in range(pages_needed):
                    page_params = dict(params)
                    page_params["page"] = current_page

                    response = requests.get(url, params=page_params, timeout=30)
                    response.raise_for_status()
                    result_data = response.json()
                    last_result_data = result_data if isinstance(result_data, dict) else {}
                    pages_fetched += 1

                    if isinstance(last_result_data, dict) and last_result_data.get("error"):
                        error_result = {
                            "status": "error",
                            "message": f"Shodan API错误: {last_result_data.get('error', '未知错误')}",
                            "suggestion": "请检查API密钥、查询语法和账户查询额度"
                        }
                        print(json.dumps(error_result, ensure_ascii=False, indent=2))
                        sys.exit(1)

                    page_matches = last_result_data.get("matches", []) if isinstance(last_result_data, dict) else []
                    if not page_matches:
                        break

                    all_matches.extend(page_matches)
                    if len(all_matches) >= target_size:
                        break
                    current_page += 1

                matches = all_matches[:target_size]
                output = {
                    "status": "success",
                    "mode": "search",
                    "query": query,
                    "page": start_page,
                    "size": target_size,
                    "pages_fetched": pages_fetched,
                    "total": last_result_data.get("total", 0),
                    "results_count": len(matches),
                    "facets": last_result_data.get("facets", {}),
                    "results": matches,
                    "message": f"成功获取 {len(matches)} 条结果"
                }

            print(json.dumps(output, ensure_ascii=False, indent=2))

        except requests.exceptions.RequestException as e:
            response_body = ""
            status_code = None
            if hasattr(e, "response") and e.response is not None:
                status_code = e.response.status_code
                try:
                    response_body = e.response.text[:500]
                except Exception:
                    response_body = ""

            error_result = {
                "status": "error",
                "message": f"请求失败: {str(e)}",
                "status_code": status_code,
                "response": response_body,
                "suggestion": "请检查网络连接、Shodan API状态、API密钥与查询额度"
            }
            print(json.dumps(error_result, ensure_ascii=False, indent=2))
            sys.exit(1)

    except Exception as e:
        error_result = {
            "status": "error",
            "message": f"执行出错: {str(e)}",
            "type": type(e).__name__
        }
        print(json.dumps(error_result, ensure_ascii=False, indent=2))
        sys.exit(1)
enabled: false
short_description: "Shodan网络空间搜索，支持search与count模式"
description: |
  Shodan 资产搜索工具，基于官方 Developer API 实现，支持快速检索和统计分析。

  **主要功能：**
  - 使用 `/shodan/host/search` 进行资产搜索
  - 使用 `/shodan/host/count` 进行无明细统计（节省查询信用）
  - 支持按 `size` 控制返回条数（自动翻页聚合）
  - 支持分页（page）
  - 支持分面统计（facets）
  - 支持结果字段裁剪（fields）
  - 支持 `minify` 控制返回数据体积

  **鉴权方式：**
  - Query 参数使用 `key`
  - 可在本文件中填写 `SHODAN_API_KEY`，或通过环境变量 `SHODAN_API_KEY` 注入

  **查询语法示例：**
  - `product:nginx`
  - `apache country:DE`
  - `port:22`
  - `org:"Amazon" port:443`
  - `ssl.cert.subject.cn:example.com`

  **注意事项：**
  - 带过滤器的查询通常会消耗 query credits
  - 翻页（超过第1页）会额外消耗额度
  - `size` 大于 100 时会自动请求更多页（每页最多 100）
  - `size` 最大限制为 1000（防止过量请求）
  - `count_only=true` 使用统计接口，不返回 matches 明细
parameters:
  - name: "query"
    type: "string"
    description: |
      Shodan 搜索语句（必需）。

      支持 Shodan filter 语法（`filter:value`）与关键字组合。
      示例：
      - `product:nginx`
      - `apache country:DE`
      - `port:22`
      - `org:"Amazon" port:443`
    required: true
    position: 1
    format: "positional"
  - name: "page"
    type: "int"
    description: |
      页码（可选，仅 search 模式生效），从 1 开始，默认 1。
    required: false
    position: 2
    format: "positional"
    default: 1
  - name: "facets"
    type: "string"
    description: |
      分面统计字段（可选）。

      多个字段用英文逗号分隔，也可指定数量：
      - `org,os`
      - `country:20,org:10`
    required: false
    position: 3
    format: "positional"
  - name: "minify"
    type: "bool"
    description: |
      是否精简返回字段（可选，仅 search 模式生效）。
      默认 `true`。
    required: false
    position: 4
    format: "positional"
    default: true
  - name: "fields"
    type: "string"
    description: |
      指定返回字段（可选，仅 search 模式生效）。

      多个字段用英文逗号分隔，例如：
      - `ip_str,port,org,hostnames,http.title`
      - `tags,http.title,http.favicon.hash`
    required: false
    position: 5
    format: "positional"
  - name: "count_only"
    type: "bool"
    description: |
      是否仅统计总数（可选）。

      - `false`（默认）：调用 `/shodan/host/search` 返回明细
      - `true`：调用 `/shodan/host/count` 仅返回 total 和 facets
    required: false
    position: 6
    format: "positional"
    default: false
  - name: "size"
    type: "int"
    description: |
      返回结果数量（可选，仅 search 模式生效）。

      - 支持 `10 / 20 / 100 / n`
      - Shodan 单页最多 100，超过 100 时会自动翻页拼接
      - 为避免额度和时延问题，最大值限制为 1000
      - 未传时默认返回单页结果（最多 100 条）
    required: false
    position: 7
    format: "positional"
