Skip to main content
Glama
whisper-sec

WhisperGraph MCP Server

Official

Threat Assessment for an Indicator

explain_indicator
Read-onlyIdempotent

Assess threat level of any IP, hostname, CIDR, or ASN to get a risk score, severity level, and detailed explanation.

Instructions

Run a comprehensive threat assessment on a single indicator. The indicator can be an IPv4, IPv6, hostname, CIDR network, or ASN — the procedure auto-detects the type.

Returns a single structured row: { indicator, type, available, cached, found, score, level (NONE/INFO/LOW/MEDIUM/HIGH/CRITICAL), explanation, factors[], sources[] }. For ASN inputs the row also includes a breakdown object with composite sub-scores (threatDensityScore, graphMetricsScore, historicalScore, prefixAgeScore). For CIDR inputs the explanation field carries threat-density stats (listed IPs, density %).

Prefer this tool over manual ASN→PREFIX→IP→LISTED_IN walks — those time out on large ASNs (AWS, GCP, Azure, Cloudflare). Performance: 3-25ms for IP/domain/network, up to ~80ms for ASN.

Argument: indicator (string, required). Allowed characters: letters, digits, '.', '-', ':', '/', '_'. Cypher-special characters are rejected.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
indicatorYesIPv4 / IPv6 / hostname / CIDR / ASN. Examples: "185.220.101.1", "google.com", "3.64.0.0/12", "AS13335".

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
rowsYes

Implementation Reference

  • The main handler function for the explain_indicator tool. Validates the indicator, calls the backend Cypher procedure 'CALL explain($indicator)', and returns results or a structured error row.
    async explainIndicator(
      indicator: string | null | undefined,
      credential: Credential | null,
    ): Promise<{ rows: Row[] }> {
      if (!isValidIndicator(indicator)) {
        return {
          rows: [
            {
              indicator: indicator ?? "",
              available: false,
              error: "invalid_indicator",
              explanation:
                "Indicator must be an IP, hostname, CIDR, or ASN. " +
                "Allowed characters: letters, digits, '.', '-', ':', '/', '_'.",
            },
          ],
        };
      }
      try {
        const raw = await this.backend.execute("CALL explain($indicator)", { indicator }, credential);
        return { rows: raw.rows ?? [] };
      } catch (error) {
        log.warn(`explain_indicator failed for "${indicator}": ${describeError(error)}`);
        return {
          rows: [
            {
              indicator,
              available: false,
              error: "lookup_failed",
              explanation: "Threat assessment is temporarily unavailable. Try again in a moment.",
            },
          ],
        };
      }
    }
  • Registration of the explain_indicator MCP tool with its input schema (a single required 'indicator' string validated via Zod) and output schema (array of rows).
      suggestion: z.string().optional(),
      errorCode: z.string().optional(),
      retryable: z.boolean().optional(),
    };
    
    const describeLabelOutputShape = {
      name: z.string(),
      exists: z.boolean(),
      count: z.number().optional(),
      properties: z.array(z.string()).optional(),
      edgesDoc: z.string().optional(),
      error: z.string().optional(),
      suggestion: z.string().optional(),
      propertiesError: z.string().optional(),
    };
    
    function toolResult(payload: object, isError = false): CallToolResult {
      return {
        content: [{ type: "text", text: JSON.stringify(payload) }],
        structuredContent: payload as Record<string, unknown>,
        isError,
      };
    }
    
    /**
     * Builds a fully wired MCP server: 6 tools, 6 resources, 8 prompts. Cheap to
     * call — in HTTP mode a fresh server is created per request for stateless
     * request isolation, while `tools` (with its cache) is shared across requests.
     */
    export function createServer(deps: ServerDeps, tools: ServerTools): McpServer {
      const { config, backend } = deps;
      const { queryTool, schemaTools, indicatorTools } = tools;
    
      const server = new McpServer(
        { name: SERVER_NAME, title: SERVER_TITLE, version: VERSION },
        { instructions: SERVER_INSTRUCTIONS },
      );
    
      server.registerTool(
        "query",
        {
          title: "WhisperGraph Cypher Query",
          description: QUERY_TOOL_DESCRIPTION,
          inputSchema: {
            cypher: z
              .string()
              .describe(
                "Cypher query string. Must include LIMIT for exploration queries. " +
                  'Use {name: "value"} property syntax for lookups.',
              ),
          },
          outputSchema: queryOutputShape,
          annotations: { ...READ_ONLY_ANNOTATIONS, openWorldHint: true },
        },
        async (args, extra) => {
          const credential = resolveCredential(extra.requestInfo?.headers, config.apiKey);
          const result = await queryTool.run(args.cypher, credential);
          return toolResult(result, !result.success);
        },
      );
    
      server.registerTool(
        "list_labels",
        {
          title: "List WhisperGraph Labels",
          description: LIST_LABELS_DESCRIPTION,
          inputSchema: {},
          outputSchema: { labels: z.array(rowSchema) },
          annotations: READ_ONLY_ANNOTATIONS,
        },
        async (_args, extra) => {
          const credential = resolveCredential(extra.requestInfo?.headers, config.apiKey);
          const result = await schemaTools.listLabels(credential);
          return toolResult(result);
        },
      );
    
      server.registerTool(
        "describe_label",
        {
          title: "Describe WhisperGraph Label",
          description: DESCRIBE_LABEL_DESCRIPTION,
          inputSchema: {
            label: z
              .string()
              .describe(
                "Label name. Uppercase letters, digits, underscores. Examples: HOSTNAME, IPV4, ASN.",
              ),
          },
          outputSchema: describeLabelOutputShape,
          annotations: READ_ONLY_ANNOTATIONS,
        },
        async (args, extra) => {
          const credential = resolveCredential(extra.requestInfo?.headers, config.apiKey);
          const result = await schemaTools.describeLabel(args.label, credential);
          return toolResult(result);
        },
      );
    
      server.registerTool(
  • The EXPLAIN_INDICATOR_DESCRIPTION constant defining the tool's full description for LLM consumption, detailing the return shape, performance characteristics, and supported indicator types.
    export const EXPLAIN_INDICATOR_DESCRIPTION = `Run a comprehensive threat assessment on a single indicator. The indicator can be an IPv4, IPv6, hostname, CIDR network, or ASN — the procedure auto-detects the type.
    
    Returns a single structured row: { indicator, type, available, cached, found, score, level (NONE/INFO/LOW/MEDIUM/HIGH/CRITICAL), explanation, factors[], sources[] }. For ASN inputs the row also includes a \`breakdown\` object with composite sub-scores (threatDensityScore, graphMetricsScore, historicalScore, prefixAgeScore). For CIDR inputs the explanation field carries threat-density stats (listed IPs, density %).
    
    Prefer this tool over manual ASN→PREFIX→IP→LISTED_IN walks — those time out on large ASNs (AWS, GCP, Azure, Cloudflare). Performance: 3-25ms for IP/domain/network, up to ~80ms for ASN.
    
    Argument: indicator (string, required). Allowed characters: letters, digits, '.', '-', ':', '/', '_'. Cypher-special characters are rejected.`;
  • src/server.ts:162-182 (registration)
    The server.registerTool call that wires the explain_indicator name, schema, description, and handler into the MCP server under the name 'explain_indicator'.
    server.registerTool(
      "explain_indicator",
      {
        title: "Threat Assessment for an Indicator",
        description: EXPLAIN_INDICATOR_DESCRIPTION,
        inputSchema: {
          indicator: z
            .string()
            .describe(
              'IPv4 / IPv6 / hostname / CIDR / ASN. Examples: "185.220.101.1", "google.com", "3.64.0.0/12", "AS13335".',
            ),
        },
        outputSchema: { rows: z.array(rowSchema) },
        annotations: READ_ONLY_ANNOTATIONS,
      },
      async (args, extra) => {
        const credential = resolveCredential(extra.requestInfo?.headers, config.apiKey);
        const result = await indicatorTools.explainIndicator(args.indicator, credential);
        return toolResult(result);
      },
    );
  • The isValidIndicator helper function (and the INDICATOR_PATTERN regex) used to validate indicator input before calling the backend.
    function isValidIndicator(indicator: string | null | undefined): indicator is string {
      return indicator != null && INDICATOR_PATTERN.test(indicator);
    }
Behavior4/5

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

Annotations already declare readOnlyHint=true and destructiveHint=false, so the tool is safe to use. The description adds auto-detection of indicator type, special output for ASN (breakdown) and CIDR (stats), performance details, and character restrictions. No contradictions.

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 with the purpose first, followed by output details, usage guidance, and parameter constraints. It is slightly verbose but every sentence adds value.

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?

The description covers input format, output schema details, special behaviors for different indicator types, performance, and restrictions. Combined with annotations and output schema, it provides complete context for correct usage.

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

Parameters4/5

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

Schema coverage is 100% with a descriptive example. The description adds allowed characters, rejection of Cypher-special characters, and auto-detection behavior, providing extra clarity beyond the schema.

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

Purpose5/5

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

The description clearly states the tool runs 'a comprehensive threat assessment on a single indicator' and lists supported types (IPv4, IPv6, hostname, CIDR, ASN). It distinguishes itself from sibling tools like 'describe_label' or 'query' by focusing on threat assessment.

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 explicitly recommends preferring this tool over manual walks for large ASNs due to timeout risks, and mentions performance benchmarks. It could improve by stating when not to use (e.g., for batch indicators), but provides solid guidance.

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/whisper-sec/whisper-graph-mcp'

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