Skip to content

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#

  1. In the console, open Settings → Agent access (/settings/mcp).
  2. Enter a name such as prod-triage-agent and select Create token.
  3. 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:

MethodPathPurpose
GET/v1/mcp/configWhether the MCP server is on
GET/v1/mcp/tokensYour tokens, metadata only
POST/v1/mcp/tokensCreate a token. Body: name, optional ttl_days
DELETE/v1/mcp/tokens/{token_id}Revoke one of your tokens

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:

json
{
  "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:

bash
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.

ToolArgumentsReturns
list_scansaccount_id (optional), limit (default 10, max 100)Recent scans, newest first
latest_scanaccount_idThe id of the latest succeeded scan
list_findingsscan_id, severity (optional), rule (optional), limit (default 50, max 200)Up to limit unsuppressed findings, sorted worst severity first, each with its resource ARN and observation ids
get_findingfinding_idOne finding in full, with its observation ids
query_attack_pathsscan_idPath and toxic-path counts, up to 25 paths and up to 10 choke points
compliance_statusscan_id, framework (optional)Pass, fail and not-assessed counts per framework, with the failing controls
get_evidenceobservation_idOne ledger observation: the API call, its payload hash and a redacted locator

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.

Typeconfig keysDelivers
webhookurl, secretA signed JSON POST
slackurl (optional)One line per finding, to url or else KAVEO_SLACK_WEBHOOK_URL
jiranoneOne Jira issue, using the KAVEO_JIRA_* settings
eventbridgebus, source, detail_typeOne event. source and detail_type default to kaveo and kaveo.finding
sqsqueue_urlOne message
s3bucket, key_prefixOne object at <key_prefix>/<finding_id>.json

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-channels and POST /v1/alert-channels
  • PATCH /v1/alert-channels/{channel_id} with any of config, min_severity and enabled. A new config replaces 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:

json
{
  "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.created
  • X-Kaveo-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body keyed with the channel's secret

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:

python
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:

VariablePurpose
KAVEO_JIRA_BASE_URLYour Jira Cloud site, for example https://your-team.atlassian.net
KAVEO_JIRA_EMAILThe account the API token belongs to
KAVEO_JIRA_API_TOKENAn Atlassian API token
KAVEO_JIRA_PROJECT_KEYThe project to file into, for example SEC

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:

bash
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.