Skip to main content
Glama
panther-labs

Panther MCP Server

Official

list_detections

Read-only

Retrieve and filter security detections from Panther by type, severity, state, tags, and other criteria to monitor rules and policies.

Instructions

List detections from your Panther instance with support for multiple detection types and filtering.

Note: The output_ids filter is applied client-side after fetching all results from the API, as the Panther REST API does not support server-side filtering by outputID. For more efficient API-level filtering, consider using the 'tag' parameter if your detections are tagged by environment.

Permissions:{'all_of': ['View Rules', 'View Policies']}

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
detection_typesNoOne or more detection types - rules, scheduled_rules, simple_rules, or policies.
cursorNoOptional cursor for pagination from a previous query (only supported for single detection type)
limitNoMaximum number of results to return per detection type
name_containsNoSubstring search by name (case-insensitive)
stateNoFilter by state - 'enabled' or 'disabled'
severityNoFilter by severity levels - INFO, LOW, MEDIUM, HIGH, or CRITICAL.
tagNoA case-insensitive list of tags to filter by.
log_typeNoA list of log types to filter by (applies to rules and simple-rules only).
resource_typeNoFilter by resource types (applies to policies only) - list of resource type names
compliance_statusNoFilter by compliance status (applies to policies only) - 'PASS', 'FAIL', or 'ERROR'
created_byNoFilter by creator user ID or actor ID
last_modified_byNoFilter by last modifier user ID or actor ID
output_idsNoClient-side filter by destination output IDs. Filters results after fetching from API to include only detections with at least one matching outputID.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • Core handler for list_detections tool. Includes @mcp_tool decorator, input schema via Annotated[Field], validation helpers integrated, API interaction logic, client-side filtering, and structured response formatting.
    @mcp_tool(
        annotations={
            "permissions": all_perms(Permission.RULE_READ, Permission.POLICY_READ),
            "readOnlyHint": True,
        }
    )
    async def list_detections(
        detection_types: Annotated[
            list[str],
            Field(
                description="One or more detection types - rules, scheduled_rules, simple_rules, or policies.",
                examples=[
                    ["rules", "simple_rules", "scheduled_rules"],
                    ["policies"],
                ],
            ),
        ] = ["rules"],
        cursor: Annotated[
            str | None,
            Field(
                description="Optional cursor for pagination from a previous query (only supported for single detection type)"
            ),
        ] = None,
        limit: Annotated[
            int,
            Field(
                description="Maximum number of results to return per detection type",
                default=100,
                ge=1,
                le=1000,
            ),
        ] = 100,
        name_contains: Annotated[
            str | None, Field(description="Substring search by name (case-insensitive)")
        ] = None,
        state: Annotated[
            str,
            Field(
                description="Filter by state - 'enabled' or 'disabled'", default="enabled"
            ),
        ] = "",
        severity: Annotated[
            list[str],
            Field(
                description="Filter by severity levels - INFO, LOW, MEDIUM, HIGH, or CRITICAL.",
                examples=[
                    ["MEDIUM", "HIGH", "CRITICAL"],
                    ["INFO", "LOW"],
                ],
            ),
        ] = [],
        tag: Annotated[
            list[str],
            Field(
                description="A case-insensitive list of tags to filter by.",
                examples=[["Initial Access", "Persistence"]],
            ),
        ] = [],
        log_type: Annotated[
            list[str],
            Field(
                description="A list of log types to filter by (applies to rules and simple-rules only).",
                examples=[["AWS.CloudTrail", "GCP.AuditLog"]],
            ),
        ] = [],
        resource_type: Annotated[
            list[str],
            Field(
                description="Filter by resource types (applies to policies only) - list of resource type names",
                examples=[["AWS.S3.Bucket", "AWS.EC2.SecurityGroup"]],
            ),
        ] = [],
        compliance_status: Annotated[
            str | None,
            Field(
                description="Filter by compliance status (applies to policies only) - 'PASS', 'FAIL', or 'ERROR'"
            ),
        ] = None,
        created_by: Annotated[
            str | None, Field(description="Filter by creator user ID or actor ID")
        ] = None,
        last_modified_by: Annotated[
            str | None, Field(description="Filter by last modifier user ID or actor ID")
        ] = None,
        output_ids: Annotated[
            list[str],
            Field(
                description="Client-side filter by destination output IDs. Filters results after fetching from API to include only detections with at least one matching outputID.",
                examples=[["destination-id-123"], ["prod-slack", "prod-pagerduty"]],
            ),
        ] = [],
    ) -> dict[str, Any]:
        """List detections from your Panther instance with support for multiple detection types and filtering.
    
        Note: The output_ids filter is applied client-side after fetching all results from the API,
        as the Panther REST API does not support server-side filtering by outputID. For more efficient
        API-level filtering, consider using the 'tag' parameter if your detections are tagged by environment.
        """
        # Validate detection types
        validation_error = validate_detection_types(detection_types)
        if validation_error:
            return validation_error
    
        logger.info(f"Fetching {limit} detections per type for types: {detection_types}")
    
        # For multiple detection types, cursor pagination is not supported
        if len(detection_types) > 1 and cursor:
            return {
                "success": False,
                "message": "Cursor pagination is not supported when querying multiple detection types. Please query one type at a time for pagination.",
            }
    
        # Validate filtering parameters
        if state and state not in VALID_STATES:
            return {
                "success": False,
                "message": f"Invalid state value. Must be one of: {', '.join(VALID_STATES)}",
            }
    
        if severity:
            invalid_severities = [s for s in severity if s not in VALID_SEVERITIES]
            if invalid_severities:
                return {
                    "success": False,
                    "message": f"Invalid severity values: {invalid_severities}. Valid values are: {', '.join(VALID_SEVERITIES)}",
                }
    
        if compliance_status and compliance_status not in VALID_COMPLIANCE_STATUSES:
            return {
                "success": False,
                "message": f"Invalid compliance_status value. Must be one of: {', '.join(VALID_COMPLIANCE_STATUSES)}",
            }
    
        # Validate detection-type-specific parameters
        if log_type and not any(dt in ["rules", "simple_rules"] for dt in detection_types):
            return {
                "success": False,
                "message": "log_type parameter is only valid for 'rules' and 'simple_rules' detection types.",
            }
    
        if resource_type and "policies" not in detection_types:
            return {
                "success": False,
                "message": "resource_type parameter is only valid for 'policies' detection type.",
            }
    
        if compliance_status and "policies" not in detection_types:
            return {
                "success": False,
                "message": "compliance_status parameter is only valid for 'policies' detection type.",
            }
    
        # Use the centralized field mapping
        field_map = LIST_FIELD_MAP
    
        try:
            all_results = {}
            has_next_pages = {}
            next_cursors = {}
    
            async with get_rest_client() as client:
                for detection_type in detection_types:
                    # Build query parameters using helper function
                    params = build_detection_params(
                        limit,
                        cursor,
                        detection_types,
                        name_contains,
                        state,
                        severity,
                        tag,
                        created_by,
                        last_modified_by,
                        log_type,
                        resource_type,
                        compliance_status,
                        detection_type,
                    )
    
                    result, _ = await client.get(
                        get_endpoint_for_detection(detection_type), params=params
                    )
    
                    # Extract detections and pagination info
                    detections = result.get("results", [])
                    next_cursor = result.get("next")
    
                    # Store results for this detection type
                    all_results[detection_type] = detections
                    next_cursors[detection_type] = next_cursor
                    has_next_pages[detection_type] = bool(next_cursor)
    
            # Process results for each detection type
            response_data = {"success": True}
    
            for detection_type in detection_types:
                detections = all_results[detection_type]
    
                # Keep only specific fields for each detection to limit the amount of data returned
                if detection_type == "policies":
                    filtered_metadata = [
                        {
                            "id": item["id"],
                            "description": item.get("description"),
                            "displayName": item.get("displayName"),
                            "enabled": item.get("enabled", False),
                            "severity": item.get("severity"),
                            "resourceTypes": item.get("resourceTypes", []),
                            "tags": item.get("tags", []),
                            "reports": item.get("reports", {}),
                            "managed": item.get("managed", False),
                            "outputIDs": item.get("outputIDs", []),
                            "createdBy": item.get("createdBy"),
                            "createdAt": item.get("createdAt"),
                            "lastModified": item.get("lastModified"),
                        }
                        for item in detections
                    ]
                elif detection_type == "scheduled_rules":
                    filtered_metadata = [
                        {
                            "id": item["id"],
                            "description": item.get("description"),
                            "displayName": item.get("displayName"),
                            "enabled": item.get("enabled", False),
                            "severity": item.get("severity"),
                            "scheduledQueries": item.get("scheduledQueries", []),
                            "tags": item.get("tags", []),
                            "reports": item.get("reports", {}),
                            "managed": item.get("managed", False),
                            "outputIDs": item.get("outputIDs", []),
                            "threshold": item.get("threshold"),
                            "dedupPeriodMinutes": item.get("dedupPeriodMinutes"),
                            "createdBy": item.get("createdBy"),
                            "createdAt": item.get("createdAt"),
                            "lastModified": item.get("lastModified"),
                        }
                        for item in detections
                    ]
                else:  # rules and simple_rules
                    filtered_metadata = [
                        {
                            "id": item["id"],
                            "description": item.get("description"),
                            "displayName": item.get("displayName"),
                            "enabled": item.get("enabled"),
                            "severity": item.get("severity"),
                            "logTypes": item.get("logTypes"),
                            "tags": item.get("tags"),
                            "reports": item.get("reports", {}),
                            "managed": item.get("managed"),
                            "outputIDs": item.get("outputIDs", []),
                            "threshold": item.get("threshold"),
                            "dedupPeriodMinutes": item.get("dedupPeriodMinutes"),
                            "createdBy": item.get("createdBy"),
                            "createdAt": item.get("createdAt"),
                            "lastModified": item.get("lastModified"),
                        }
                        for item in detections
                    ]
    
                # Apply client-side output_ids filtering if requested
                if output_ids:
                    filtered_metadata = [
                        item
                        for item in filtered_metadata
                        if any(
                            output_id in item.get("outputIDs", [])
                            for output_id in output_ids
                        )
                    ]
                    logger.info(
                        f"Applied client-side output_ids filter for {detection_type}: {len(filtered_metadata)} results matched"
                    )
    
                # Add to response
                response_data[field_map[detection_type]] = filtered_metadata
                response_data[f"total_{field_map[detection_type]}"] = len(filtered_metadata)
    
                # Add pagination info (only for single detection type queries)
                if len(detection_types) == 1:
                    response_data["has_next_page"] = has_next_pages[detection_type]
                    response_data["next_cursor"] = next_cursors[detection_type]
                else:
                    response_data[f"{detection_type}_has_next_page"] = has_next_pages[
                        detection_type
                    ]
                    response_data[f"{detection_type}_next_cursor"] = next_cursors[
                        detection_type
                    ]
    
            # Add overall summary for multi-type queries
            if len(detection_types) > 1:
                total_detections = sum(len(all_results[dt]) for dt in detection_types)
                response_data["total_all_detections"] = total_detections
                response_data["detection_types_queried"] = detection_types
    
            logger.info(f"Successfully retrieved detections for types: {detection_types}")
            return response_data
        except Exception as e:
            logger.error(f"Failed to list detection types {detection_types}: {str(e)}")
            return {
                "success": False,
                "message": f"Failed to list detection types {detection_types}: {str(e)}",
            }
  • Central point where register_all_tools(mcp) is called, which iterates over all @mcp_tool decorated functions (including list_detections) and registers them with the FastMCP server instance.
    # Register all tools with MCP using the registry
    register_all_tools(mcp)
    # Register all prompts with MCP using the registry
    register_all_prompts(mcp)
    # Register all resources with MCP using the registry
    register_all_resources(mcp)
  • Pydantic input schema defined via Annotated parameters with Field metadata for description, examples, validation (e.g., limit ge=1 le=1000), making it self-describing for MCP clients.
    async def list_detections(
        detection_types: Annotated[
            list[str],
            Field(
                description="One or more detection types - rules, scheduled_rules, simple_rules, or policies.",
                examples=[
                    ["rules", "simple_rules", "scheduled_rules"],
                    ["policies"],
                ],
            ),
        ] = ["rules"],
        cursor: Annotated[
            str | None,
            Field(
                description="Optional cursor for pagination from a previous query (only supported for single detection type)"
            ),
        ] = None,
        limit: Annotated[
            int,
            Field(
                description="Maximum number of results to return per detection type",
                default=100,
                ge=1,
                le=1000,
            ),
        ] = 100,
        name_contains: Annotated[
            str | None, Field(description="Substring search by name (case-insensitive)")
        ] = None,
        state: Annotated[
            str,
            Field(
                description="Filter by state - 'enabled' or 'disabled'", default="enabled"
            ),
        ] = "",
        severity: Annotated[
            list[str],
            Field(
                description="Filter by severity levels - INFO, LOW, MEDIUM, HIGH, or CRITICAL.",
                examples=[
                    ["MEDIUM", "HIGH", "CRITICAL"],
                    ["INFO", "LOW"],
                ],
            ),
        ] = [],
        tag: Annotated[
            list[str],
            Field(
                description="A case-insensitive list of tags to filter by.",
                examples=[["Initial Access", "Persistence"]],
            ),
        ] = [],
        log_type: Annotated[
            list[str],
            Field(
                description="A list of log types to filter by (applies to rules and simple-rules only).",
                examples=[["AWS.CloudTrail", "GCP.AuditLog"]],
            ),
        ] = [],
        resource_type: Annotated[
            list[str],
            Field(
                description="Filter by resource types (applies to policies only) - list of resource type names",
                examples=[["AWS.S3.Bucket", "AWS.EC2.SecurityGroup"]],
            ),
        ] = [],
        compliance_status: Annotated[
            str | None,
            Field(
                description="Filter by compliance status (applies to policies only) - 'PASS', 'FAIL', or 'ERROR'"
            ),
        ] = None,
        created_by: Annotated[
            str | None, Field(description="Filter by creator user ID or actor ID")
        ] = None,
        last_modified_by: Annotated[
            str | None, Field(description="Filter by last modifier user ID or actor ID")
        ] = None,
        output_ids: Annotated[
            list[str],
            Field(
                description="Client-side filter by destination output IDs. Filters results after fetching from API to include only detections with at least one matching outputID.",
                examples=[["destination-id-123"], ["prod-slack", "prod-pagerduty"]],
            ),
        ] = [],
    ) -> dict[str, Any]:
  • Input validation helper for detection_types parameter used in list_detections.
    def validate_detection_types(detection_types: list[str]) -> dict[str, Any] | None:
        """Validate detection types and return error dict if invalid, None if valid."""
        if not detection_types:
            return {
                "success": False,
                "message": "At least one detection type must be specified.",
            }
    
        invalid_types = [dt for dt in detection_types if dt not in DETECTION_TYPES]
        if invalid_types:
            valid_types = ", ".join(DETECTION_TYPES.keys())
            return {
                "success": False,
                "message": f"Invalid detection_types {invalid_types}. Valid values are: {valid_types}",
            }
        return None
  • Helper to build API request parameters, ensuring compatibility across detection types and validating param applicability.
    def build_detection_params(
        limit: int,
        cursor: str | None,
        detection_types: list[str],
        name_contains: str | None,
        state: str | None,
        severity: list[str] | None,
        tag: list[str] | None,
        created_by: str | None,
        last_modified_by: str | None,
        log_type: list[str] | None,
        resource_type: list[str] | None,
        compliance_status: str | None,
        detection_type: str,
    ) -> dict[str, Any]:
        """Build query parameters for detection API calls."""
        params = {"limit": limit}
    
        # Add cursor for single detection type queries only
        if cursor and len(detection_types) == 1:
            params["cursor"] = cursor
            logger.info(f"Using cursor for pagination: {cursor}")
    
        # Add common filtering parameters
        if name_contains:
            params["name-contains"] = name_contains
        if state:
            params["state"] = state
        if severity:
            params["severity"] = severity
        if tag:
            params["tag"] = tag
        if created_by:
            params["created-by"] = created_by
        if last_modified_by:
            params["last-modified-by"] = last_modified_by
    
        # Add detection-type-specific parameters
        if detection_type in ["rules", "simple_rules"] and log_type:
            params["log-type"] = log_type
        elif detection_type == "policies":
            if resource_type:
                params["resource-type"] = resource_type
            if compliance_status:
                params["compliance-status"] = compliance_status
    
        return params
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description adds valuable behavioral context beyond the readOnlyHint annotation, such as the client-side filtering caveat for 'output_ids' and the efficiency tip for using 'tag'. It also includes permissions information ('View Rules', 'View Policies'), which is not covered by annotations. No contradictions with annotations exist.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured and front-loaded with the core purpose, followed by important notes and permissions. It avoids unnecessary repetition, but the permissions section could be integrated more smoothly, and the note about client-side filtering is slightly verbose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (13 parameters), rich schema coverage (100%), annotations (readOnlyHint), and the presence of an output schema, the description is complete. It covers key behavioral aspects like filtering nuances and permissions, leaving no critical gaps for the agent to understand the tool's use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 100% schema description coverage, the input schema already fully documents all 13 parameters. The description does not add significant semantic details beyond what the schema provides, such as explaining interactions between parameters or edge cases, so it meets the baseline of 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'List detections from your Panther instance with support for multiple detection types and filtering.' It specifies the verb ('List'), resource ('detections'), and scope ('Panther instance'), but does not explicitly differentiate it from sibling tools like 'get_detection' or 'list_alerts', which prevents a perfect score.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear usage context by noting that 'output_ids' filtering is client-side and suggesting 'tag' for more efficient API-level filtering. However, it does not explicitly state when to use this tool versus alternatives like 'get_detection' or 'list_alerts', which would be needed for a score of 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/panther-labs/mcp-panther'

If you have feedback or need assistance with the MCP directory API, please join our Discord server