kaveo · Guides
MCP server and integrations
Give your own AI agents read-only, evidence-backed access to kaveo over MCP, and route findings to webhooks, Jira, Slack, EventBridge, SQS or S3.
On this page
This page covers the ways kaveo shares what it finds: the read-only MCP server for your own AI agents, alert channels for new findings, Jira and Slack, and SIEM export. Each integration is off until you configure it, and kaveo makes no outbound call for one you have not set up.
MCP server#
The MCP server lets your AI agents query kaveo over the Model Context Protocol. It exposes 7 read-only tools covering scans, findings, attack paths, compliance status and the evidence ledger.
Turn it on#
Set KAVEO_MCP_ENABLED=true in .env, then recreate the api container so it picks up the change. The stock compose file already passes this variable to the api, so you do not need an override file. Caddy routes /mcp to the api by default. While the flag is off, an authenticated POST /mcp returns 403.
Before an agent connects from outside the host, serve kaveo over TLS: set KAVEO_SITE_ADDRESS to a real hostname, which turns on automatic TLS in Caddy. See Deployment.
Create a token#
- In the console, open Settings → Agent access (
/settings/mcp). - Enter a name such as
prod-triage-agentand select Create token. - Copy the token. It starts with
kv_mcp_and is shown only once. kaveo stores only its SHA-256 hash.
A token carries the role and organization of the user who created it, so an agent sees exactly what that user sees. The same page lists your tokens with their last-used and expiry dates, and Revoke disables a token immediately. You can create tokens while the server is off, so you can set up agents before you turn it on.
Tokens created in the console never expire. To set an expiry, create the token through the API with ttl_days, from 1 to 3650:
A token also stops working if its owner is deactivated, or if the owner's approval is revoked while KAVEO_APPROVAL_REQUIRED is on (the default). /mcp accepts only kv_mcp_ tokens. API tokens (kv_api_) and console sessions work on the /v1 API, not here.
Connect a client#
Point your MCP client at https://<your-host>/mcp and send the token as a bearer credential. The console shows an example in this shape:
{
"mcpServers": {
"kaveo": {
"url": "https://<your-host>/mcp",
"headers": { "Authorization": "Bearer kv_mcp_<your-token>" }
}
}
}
Use the path exactly as /mcp, with no trailing slash, because Caddy sends only that path to the api. The server answers over plain HTTP with JSON and does not open a Server-Sent Events stream, so choose your client's HTTP transport, not its SSE transport.
To check the connection by hand, put the token in MCP_TOKEN and list the tools:
curl -s https://<your-host>/mcp \
-H "Authorization: Bearer $MCP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Keep the token in your agent's secret store, not in a config file you share.
Protocol#
The endpoint speaks JSON-RPC 2.0 and implements initialize, tools/list, tools/call and ping. It acknowledges a notifications/* message with HTTP 202 and no JSON-RPC response. It is built directly on JSON-RPC, without an external MCP SDK, reports protocol version 2024-11-05, and answers each request with one JSON response.
A tool result is MCP text content that holds JSON. A missing required argument, or an id that does not exist, comes back as a normal result whose JSON has an error key. If a tool fails while it runs, the error comes back inside the result with isError set to true, not as an HTTP error. An unknown tool name returns a JSON-RPC error.
Each user can make up to 120 tool calls per minute on each api replica; past that, the call returns a JSON-RPC error. Each tool call is written to the audit log as mcp.tool_call, with the tool name.
Tools#
Start with latest_scan to get a scan_id, then pass it to the other tools.
list_findings applies limit before it sorts. On a scan with more findings than limit, pass severity so the result holds the findings you care about.
framework takes a code: CIS_AWS, FSBP, PCI_DSS, SOC2, OWASP_TOP_10, HIPAA, GDPR, CCPA, ISO_27001 or NIST_800_53. compliance_status does not apply accept-risk waivers, so a control you waived in the console reports as failing here. See Compliance and reporting for waivers.
Findings carry the observation ids that evidence them, and failing controls list the finding ids behind them, so an agent can trace an answer back to the ledger with get_evidence.
Alert channels#
An alert channel sends new findings to a destination you own. After each successful scan, the worker compares the scan with the previous succeeded scan of the same account. A finding is new when its rule and resource ARN did not appear in that scan. Suppressed findings are left out on both sides. The worker takes up to 50 new findings per scan, worst severity first, and sends each one to every enabled channel whose min_severity it meets.
kaveo does not check config keys when you save a channel, so match them to this table exactly. A channel with a missing destination key delivers nothing.
Webhook, EventBridge, SQS and S3 all send the same JSON: finding_id, scan_id, rule, severity, source and title. The three AWS types write with kaveo's own base credentials or instance role, never a scanned account's role. Grant those credentials only the one action each target needs: events:PutEvents, sqs:SendMessage or s3:PutObject. kaveo creates these clients in the worker's default AWS region (AWS_REGION in the stock compose file, us-east-1 unless you set it), so keep the EventBridge bus and the SQS queue in that region.
Only admins can manage channels. Settings → Alerts in the console lists each channel and lets you turn it on or off and change its severity floor. Create, reconfigure and delete channels through the API:
GET /v1/alert-channelsandPOST /v1/alert-channelsPATCH /v1/alert-channels/{channel_id}with any ofconfig,min_severityandenabled. A newconfigreplaces the old one, so send every key.DELETE /v1/alert-channels/{channel_id}
With an API token, the token's owner must be an admin and the token needs the manage_accounts scope, since tokens get only read and run-scan scopes by default. A create request looks like this. min_severity defaults to low, which leaves out info findings, and enabled defaults to true:
{
"type": "webhook",
"config": { "url": "https://hooks.example.com/kaveo", "secret": "<random-secret>" },
"min_severity": "high",
"enabled": true
}
Delivery is best-effort. kaveo does not queue a failed delivery for a later retry: it logs the failure or the non-2xx response and moves on. A channel outage never fails the scan.
Webhook signatures#
The body is compact JSON with sorted keys. Each request carries two headers:
X-Kaveo-Event: finding.createdX-Kaveo-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body keyed with the channel'ssecret
This is the same convention as GitHub's X-Hub-Signature-256. Compute the HMAC over the raw bytes you received, before any JSON parsing, and compare in constant time:
import hashlib
import hmac
def verify(secret: str, raw_body: bytes, signature_header: str) -> bool:
digest = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(f"sha256={digest}", signature_header)
Reject any request that fails the check. Always set secret when you create a webhook channel, and give each channel its own long random value, for example from openssl rand -hex 32.
Jira#
Jira is configured in the environment, not per channel:
If any of the four is empty, Jira is off. With all four set, a jira channel opens one Task per new finding through the Jira Cloud REST v3 API. The summary is the finding's title, the description names the rule, severity and finding id, and the issue is labelled kaveo and the finding's severity. The project needs the Task issue type. The stock compose file does not pass these variables, so add them to the api and worker in an override file. See Configuration.
Slack#
Set KAVEO_SLACK_WEBHOOK_URL to a Slack incoming webhook. After each successful scan, the worker posts the scan brief: its headline and summary, up to 5 new findings, and up to 5 findings that fired again after a verified fix. When the autonomous patrol drafts fixes, it also posts a short patrol summary. This variable also needs an override file. Treat the URL as a secret.
For one message per finding, add a slack alert channel as well.
SIEM export#
GET /v1/findings/export downloads findings for a SIEM or data lake. format is ocsf (the default), asff, csv or json. OCSF output uses the Detection Finding class (2004). You can filter by scan_id, severity and source, and include_suppressed=true adds suppressed findings.
For asff, account_id and region fill AwsAccountId and the ProductArn. Without them the file uses 000000000000 and us-east-1, so set both before you import it anywhere.
One export holds at most the 10,000 newest matching findings. If an export holds exactly 10,000, narrow it by scan or severity and export again.
From the CLI:
kaveo findings export --format ocsf --severity high --out findings.json
--scan and --include-suppressed filter the same way. The CLI does not pass source, account_id or region, so call the API directly when you need them. See CLI and API reference.
Sharing, email and pull requests#
Read-only report links and emailed reports are covered in Compliance and reporting. Opening a GitHub pull request for a drafted fix is covered in Remediation and autonomous patrol.