Objective
When the MCP app generates an Attack Discovery, route it through the AD 2.0 workflow/skill pipeline — identical to a default UI "Generate" — instead of the legacy LangGraph path it uses today. Running Attack Discovery from the MCP app should behave the same way as running it from the Security UI, including using the skill-based generation pipeline.
Related Kibana work: AD 2.0 "Workflows Integration" PR stack (branch attack-discovery-workflows-integration/10-scheduling).
Background / current behavior
Today the MCP app triggers generation via the legacy route:
AttackDiscoveryClient.generate() → POST /api/attack_discovery/_generate (elastic_assistant plugin, LangGraph path).
- This bypasses the new AD 2.0 workflow pipeline and its skill usage entirely.
The Kibana UI, when the feature flag securitySolution.attackDiscoveryWorkflowsEnabled is on:
- Posts to
POST /internal/attack_discovery/_generate (discoveries plugin) → executeGenerationWorkflow() → the 3-phase managed-workflow pipeline (alert retrieval → generation → validation), with workflow_config.skill_enabled: true.
Why this is a small change
- The MCP Kibana client already sends the headers internal routes require (
kbn-xsrf, x-elastic-internal-origin: Kibana) — see src/elastic/kibana-client/create-kibana-client.ts.
- Status polling is unchanged. Both the legacy and workflow paths write to the same Kibana event log, and
GET /api/attack_discovery/generations returns workflow runs too. So AttackDiscoveryService.getGenerations() and the app's progress banner keep working with no changes.
- The only gap is the start call: endpoint + request-body shape.
Design decisions
- Route strategy: Prefer the internal workflow route; on
404/403 (flag off, or the API key lacks workflow privileges) automatically fall back to the legacy route.
- Config surface (Option A): No new tool inputs. The
generate-attack-discovery tool keeps its current schema (connectorName, size, start, end, filter). Internally we attach a fixed, UI-matching workflow_config.
Internal (workflow) request body
```json
{
"alerts_index_pattern": ".alerts-security.alerts-default",
"api_config": { "connector_id": "...", "action_type_id": "...", "model": "..." },
"size": 50,
"start": "now-7d",
"end": "now",
"source": "interactive",
"workflow_config": {
"alert_retrieval_mode": "custom_query",
"alert_retrieval_workflow_ids": [],
"alert_retrieval_workflows_enabled": false,
"default_retrieval_enabled": false,
"skill_enabled": true,
"validation_workflow_id": "default"
}
}
```
Note: the internal body is snake_case and does not take anonymizationFields / replacements / subAction — those belong only to the legacy fallback body.
Scope
In scope
AttackDiscoveryClient: add generateViaWorkflow() (internal route) + snake_case body type; keep legacy generate().
AttackDiscoveryService.generateAttackDiscovery(): build the UI-matching workflow body and try the workflow route, falling back to legacy on 404/403; return { execution_uuid } unchanged.
- Unit tests for both paths (workflow happy path + fallback).
- Live end-to-end verification against local Kibana.
Out of scope
- Exposing
workflow_config knobs as tool inputs (Option B).
- Changes to status polling / progress banner.
- Any Kibana-side changes; any UI changes beyond the MCP app.
Done criteria
- MCP
generate-attack-discovery triggers a workflow-based (skill_enabled) generation identical to a default UI Generate when the flag is on.
- Falls back cleanly to legacy when the workflow route is unavailable (
404/403).
- Unit tests cover both paths.
- Verified end-to-end against local Kibana on
attack-discovery-workflows-integration/10-scheduling.
Risks / notes
- The MCP API key must carry
workflowsManagement:read+execute and ATTACK_DISCOVERY_API_ACTION_ALL+ALERTS_API_READ, else the internal route returns 403 and we fall back to legacy (acceptable, but worth surfacing in the returned status message / logs).
Work breakdown
Wave 1 — foundation
Wave 2 — parallel (depend on Wave 1)
Objective
When the MCP app generates an Attack Discovery, route it through the AD 2.0 workflow/skill pipeline — identical to a default UI "Generate" — instead of the legacy LangGraph path it uses today. Running Attack Discovery from the MCP app should behave the same way as running it from the Security UI, including using the skill-based generation pipeline.
Related Kibana work: AD 2.0 "Workflows Integration" PR stack (branch
attack-discovery-workflows-integration/10-scheduling).Background / current behavior
Today the MCP app triggers generation via the legacy route:
AttackDiscoveryClient.generate()→POST /api/attack_discovery/_generate(elastic_assistantplugin, LangGraph path).The Kibana UI, when the feature flag
securitySolution.attackDiscoveryWorkflowsEnabledis on:POST /internal/attack_discovery/_generate(discoveriesplugin) →executeGenerationWorkflow()→ the 3-phase managed-workflow pipeline (alert retrieval → generation → validation), withworkflow_config.skill_enabled: true.Why this is a small change
kbn-xsrf,x-elastic-internal-origin: Kibana) — seesrc/elastic/kibana-client/create-kibana-client.ts.GET /api/attack_discovery/generationsreturns workflow runs too. SoAttackDiscoveryService.getGenerations()and the app's progress banner keep working with no changes.Design decisions
404/403(flag off, or the API key lacks workflow privileges) automatically fall back to the legacy route.generate-attack-discoverytool keeps its current schema (connectorName,size,start,end,filter). Internally we attach a fixed, UI-matchingworkflow_config.Internal (workflow) request body
```json
{
"alerts_index_pattern": ".alerts-security.alerts-default",
"api_config": { "connector_id": "...", "action_type_id": "...", "model": "..." },
"size": 50,
"start": "now-7d",
"end": "now",
"source": "interactive",
"workflow_config": {
"alert_retrieval_mode": "custom_query",
"alert_retrieval_workflow_ids": [],
"alert_retrieval_workflows_enabled": false,
"default_retrieval_enabled": false,
"skill_enabled": true,
"validation_workflow_id": "default"
}
}
```
Note: the internal body is snake_case and does not take
anonymizationFields/replacements/subAction— those belong only to the legacy fallback body.Scope
In scope
AttackDiscoveryClient: addgenerateViaWorkflow()(internal route) + snake_case body type; keep legacygenerate().AttackDiscoveryService.generateAttackDiscovery(): build the UI-matching workflow body and try the workflow route, falling back to legacy on404/403; return{ execution_uuid }unchanged.Out of scope
workflow_configknobs as tool inputs (Option B).Done criteria
generate-attack-discoverytriggers a workflow-based (skill_enabled) generation identical to a default UI Generate when the flag is on.404/403).attack-discovery-workflows-integration/10-scheduling.Risks / notes
workflowsManagement:read+executeandATTACK_DISCOVERY_API_ACTION_ALL+ALERTS_API_READ, else the internal route returns403and we fall back to legacy (acceptable, but worth surfacing in the returned status message / logs).Work breakdown
Wave 1 — foundation
src/elastic/client/attackDiscoveryClient.ts: addgenerateViaWorkflow(body)→POST /internal/attack_discovery/_generate; keep legacygenerate(body); add snake_case body type mirroring Kibana'sPostGenerateRequestBody.src/elastic/service/attackDiscoveryService.ts: build the internal body with the fixedworkflow_configdefaults +source: "interactive"; try workflow route, catch404/403, fall back to legacy body. Return{ execution_uuid }unchanged.src/tools/attack-discovery.ts: (optional) report which path ran (workflowvslegacy) in the status message; no schema change.Wave 2 — parallel (depend on Wave 1)
src/elastic/service/attackDiscoveryService.test.tsandsrc/tools/attack-discovery.test.tsto assert the internal route is called with the correct snake_case body +workflow_configdefaults, and that404/403triggers the legacy fallback.POST /internal/attack_discovery/_generateis hit and a workflow/skill pipeline runs (event-log step events); confirm the progress banner shows in-progress → succeeded and results refresh; confirm legacy fallback when the flag is off / privileges are missing.