Threat Assessment for an Indicator
explain_indicatorAssess 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
| Name | Required | Description | Default |
|---|---|---|---|
| indicator | Yes | IPv4 / IPv6 / hostname / CIDR / ASN. Examples: "185.220.101.1", "google.com", "3.64.0.0/12", "AS13335". |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| rows | Yes |
Implementation Reference
- src/tools/indicator-tools.ts:45-79 (handler)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.", }, ], }; } } - src/server.ts:63-162 (schema)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( - src/tools/descriptions.ts:57-63 (schema)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); }, ); - src/tools/indicator-tools.ts:27-29 (helper)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); }