Skip to main content

MCP server reference

Riptide embeds a Model Context Protocol (MCP) server that lets an AI agent query the flow tables in ClickHouse. It is off by default and only ever reads.

Settings​

Set them in /etc/riptide/config.yaml, as environment variables, or as --key=value arguments.

NameTypeDefaultDescription
riptide.mcp.enabledboolfalseStarts the server and registers the skills.
riptide.mcp.transportstdio or ssestdiostdio reads JSON-RPC lines from stdin and answers on stdout. sse serves HTTP on the address and port below.
riptide.mcp.bind-addressaddress127.0.0.1Listen address of the sse transport.
riptide.mcp.sse-portint8081Port of the sse transport. 8080 belongs to riptide.management.port in the same process.
riptide.mcp.max-sse-sessionsint64Open SSE streams. The next stream request gets 503.
riptide.mcp.sse-keep-alive-intervalduration15sKeep-alive comment on an idle stream. A client that vanished is noticed when that write fails, so this is also how long its session lingers.
riptide.mcp.query-timeout-secondsint5Sent as ClickHouse max_execution_time and used as the client-side wait.
riptide.mcp.max-result-rowsint50Rows read from a ClickHouse response. Further rows are discarded.
riptide.mcp.clickhouse.usernamesecret referenceriptide.clickhouse.usernameClickHouse identity for MCP queries. Resolved once at startup.
riptide.mcp.clickhouse.passwordsecret referenceriptide.clickhouse.password while the MCP username is also unset; otherwise emptyPassword of that identity. Username and password move together: setting the reader username without a password authenticates with an empty one. Resolved once at startup.
riptide.mcp.auth.enabledboolfalseRequires a token on every request.
riptide.mcp.auth.tokens[n]secret referencenoneAccepted tokens. Resolved once when the server starts. A token that fails to resolve is logged and skipped. With auth.enabled=true and no usable token every request is refused.

In stdio mode the log moves to stderr so that stdout carries only JSON-RPC frames. Set riptide.logging.console-target yourself to override that.

ClickHouse identity​

DeploymentSet riptide.mcp.clickhouse.* toReason
Single-tenant manage modeleave unsetThe ingest user reads and writes anyway.
Provisioned (multi-tenant)bi_<tenant>@<database>The reader holds flow_reader@<database>: SELECT on flows and every rollup, readonly = 2, allow_ddl = 0, and it is already on every row policy. The ingest writer holds only INSERT on the rollups, so a rollup-routed query as the writer fails with ACCESS_DENIED.

The @ is written literally in a properties or YAML value. Only a URL-embedded credential needs %40. A database onboarded before the rename still has the unqualified bi_<tenant> account until you re-run onboard, see Object names carry their database.

Example for a tenant acme in database riptide:

riptide.mcp.clickhouse.username=bi_acme@riptide
riptide.mcp.clickhouse.password=vault://secret/riptide/clickhouse#bi_acme

Tools​

Every tool returns a JSON array of rows, serialised as one text content item. Arguments arrive untyped. A numeric argument that is missing or malformed falls back to its default, and one out of range is clamped.

ToolParametersReadsReturns per row
riptide_get_top_talkerstime_range_minutes (int, default 15, max 43200), group_by (one of application, protocol, srcAddr, dstAddr, srcAs, dstAs; default application)flows, or the matching rollup at 60 minutes or morethe group_by column, total_bytes, total_packets; 20 rows
riptide_get_interface_utilizationtime_range_minutes (default 15), limit (int, default 20, max 500)flows, or flows_by_exporter_iface_1m at 60 minutes or moreexporterAddr, exporterName, inputSnmp, outputSnmp, total_bytes
riptide_trace_host_flowip_address (required, IPv4 or IPv6), time_range_minutes (default 15)flows onlysrcAddr, dstAddr, srcPort, dstPort, protocol, application, tcpFlags, bytes, packets; 50 rows
riptide_get_geo_asn_distributiontime_range_minutes (default 60)flows, or flows_by_geo_asn_1m at 60 minutes or moredstAs, dstCountry, total_bytes; 20 rows
riptide_detect_traffic_spikestime_range_minutes (default 15)flows, or flows_by_conversation_1m at 60 minutes or moredstAddr, total_packets, total_bytes, flow_count; 20 rows
riptide_generate_mitigation_rulestarget_ip (required, IPv4 or IPv6), attack_type (string, default Volumetric Flood)nothingtarget_ip (IPv6 in compressed form), attack_type, bgp_flowspec, iptables, rtbh_null_route; one row. An IPv6 target gets a /128 prefix, ip6tables and ipv6 route.

The tools/list schema marks time_range_minutes, group_by and attack_type as required, but a missing value takes the default above.

The rollup tables are SummingMergeTree tables and carry samplingInterval and flowProtocol, so the sampling-corrected scaling expression is the same whichever table answers. The tools report counters as the exporter sent them and do not apply that correction, see sampling-corrected volume. A rollup whose shape does not match this version is declined and the query falls back to flows, see rollups.

Result array​

The array holds data rows and at most two non-row entries. A client iterating rows skips an entry carrying either key.

EntryWhenExample
errorthe tool refused an argument, ClickHouse refused the query, or the query timed out{"error": "Invalid group_by dimension. Allowed: [...]"}; the order of the listed dimensions varies between runs, so match on the prefix only
coverage_warningthe table holds fewer minutes than asked for, or the request exceeded the 43200-minute capsee below

A request for 129600 minutes against a table holding two days:

{"coverage_warning": "answered from `riptide`.flows_by_exporter_iface_1m, which holds data from 2026-09-21 01:12:00. This answer covers 3421 of the 129600 minutes you asked for, and riptide caps a single query at 43200 minutes. The rest is not missing from your network."}

An empty table produces rows only, never a warning.

Prompts and resources​

Seven skills ship inside the jar under mcp/skills/*.md. Each is listed by prompts/list under its name and by resources/list as resource://riptide/skills/<name> with MIME type text/markdown. prompts/get and resources/read return the Markdown unchanged.

Prompt nameSlash commandDescription
riptide-ddos-mitigation-triage/riptide-investigate-ddosDDoS attack classification from RFC 4732 taxonomy, Shannon entropy, TCP flag histograms and NIST SP 800-189 amplification heuristics.
riptide-cause-analysis-triage/riptide-cause-analysisCompares the current 15-minute window against the same window 24 hours earlier.
riptide-interface-capacity-analysis/riptide-capacity-planInterface utilisation against SNMP ifSpeed, 95th percentile and headroom projection.
riptide-peering-geo-analysis/riptide-peering-analysisASN and country breakdown for transit decisions.
riptide-application-performance-triage/riptide-app-auditApplication protocol distribution, unclassified traffic and public/private locality.
riptide-host-forensic-investigation/riptide-trace-hostPeer matrix, active ports, VLANs and flow durations for one host.
riptide-ddos-auto-mitigation-playbook/riptide-auto-mitigateBGP FlowSpec (RFC 8955), RTBH (RFC 7999) and iptables rules.

The slash command is appended to the prompt description as [Command: /...]. Whether a client exposes it under that name is up to the client.

JSON-RPC methods​

The server speaks protocol version 2024-11-05 and answers initialize with that version whatever the client asked for.

MethodResult
initializeprotocolVersion, capabilities (tools, prompts, resources), serverInfo (riptide-flows-mcp, the riptide version)
ping{}
tools/listthe six tool definitions
tools/callcontent with one text item holding the result array
prompts/list, prompts/getthe skills as prompts
resources/list, resources/readthe skills as resources
notifications/*no response

Connect Claude Code​

Prerequisites: the DEB or RPM package is installed, so the jar is at /usr/share/riptide/riptide.jar and its ClickHouse settings are in /etc/riptide/config.yaml.

  1. Register the server:

    claude mcp add riptide -- java -jar /usr/share/riptide/riptide.jar --riptide.mcp.enabled=true --riptide.mcp.transport=stdio

    Expected output:

    Added stdio MCP server riptide with command: java -jar /usr/share/riptide/riptide.jar --riptide.mcp.enabled=true --riptide.mcp.transport=stdio to local config
  2. Verify:

    claude mcp get riptide

    Expected output:

    riptide:
    Scope: Local config (private to you in this project)
    Status: ✔ Connected
    Type: stdio
    Command: java
    Args: -jar /usr/share/riptide/riptide.jar --riptide.mcp.enabled=true --riptide.mcp.transport=stdio

Each Claude Code session starts its own riptide process. Any other client that launches a stdio server takes the same command and arguments.

To drive the stdio transport by hand, write one JSON-RPC object per line:

printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"shell","version":"0"}}}' \
| java -jar /usr/share/riptide/riptide.jar --riptide.mcp.enabled=true --riptide.mcp.transport=stdio 2>/dev/null

Expected output:

{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"resources":{},"prompts":{},"tools":{}},"serverInfo":{"name":"riptide-flows-mcp","version":"0.15.0"}}}

Connect over HTTP (SSE)​

warning

With riptide.mcp.auth.enabled=false the endpoint answers anyone who can reach it, and every tool reads flow telemetry. Enable token authentication before setting riptide.mcp.bind-address to a non-loopback address. The server logs a warning at startup while it runs unauthenticated.

Settings:

riptide.mcp.enabled=true
riptide.mcp.transport=sse
riptide.mcp.auth.enabled=true
riptide.mcp.auth.tokens[0]=file:///etc/riptide/mcp-token

Expected log lines at startup:

Riptide MCP Server HTTP/SSE Transport listening at http://127.0.0.1:8081/mcp/sse

The token travels in the Authorization: Bearer <token> header. A ?token= query parameter is ignored, because it would land in proxy and access logs. On the stdio transport a token goes in params._meta.authToken of each request instead. No CORS headers are sent, so a browser page cannot call the endpoint cross-origin.

GET /mcp/sse​

Opens the event stream. The first frame names the URL to POST requests to.

curl -N -H 'Authorization: Bearer e6f1c0b2a9d84f3d9c1b7a5e2f4d6c8a' http://127.0.0.1:8081/mcp/sse

Expected output:

event: endpoint
data: /mcp/sse?sessionId=ff2bd462-c1dd-45ca-b56d-8a11ef85a301

Every response to a request POSTed with that sessionId arrives on the stream as event: message, and a : keep-alive comment follows each idle interval.

StatusMeaning
200Stream open.
401Auth is enabled and the token is missing or wrong.
503riptide.mcp.max-sse-sessions streams are open, or the process is shutting down.

POST /mcp/sse​

ParameterInRequiredDescription
sessionIdquerynoAn open stream. With it the response goes to the stream and the POST returns 202 with an empty body. Without it, or with a closed session, the response is the POST body. A notification (no id, or a notifications/* method) always gets 202 and no response.
Authorizationheaderwhen auth is enabledBearer <token>
bodybodyyesone JSON-RPC 2.0 request
curl -s -X POST -H 'Authorization: Bearer e6f1c0b2a9d84f3d9c1b7a5e2f4d6c8a' -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"riptide_generate_mitigation_rules","arguments":{"target_ip":"203.0.113.10","attack_type":"TCP SYN Flood"}}}' \
http://127.0.0.1:8081/mcp/sse

Expected output:

{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"[{\"target_ip\":\"203.0.113.10\",\"attack_type\":\"TCP SYN Flood\",\"bgp_flowspec\":\"match destination-prefix 203.0.113.10/32 protocol tcp flags syn -> rate-limit 0\",\"iptables\":\"iptables -A INPUT -d 203.0.113.10 -p tcp --tcp-flags SYN,ACK SYN -j DROP\",\"rtbh_null_route\":\"ip route 203.0.113.10/32 Null0 tag 666\"}]"}]}}
StatusMeaning
200Response in the body.
202Response queued on the stream named by sessionId, or the request was a notification.
400Empty body, or the body is not JSON.
401Auth is enabled and the token is missing or wrong.
500The handler threw; the body is a -32603 Internal error: ... object.

Any other HTTP method on /mcp/sse gets 405.

Error catalog​

JSON-RPC errors come back in an error object. Tool-level errors come back as an error row inside a successful result.

CodeMessageProbable causeRecovery
-32001Unauthorized: invalid MCP authentication tokenNo bearer token, a wrong one, or auth.enabled=true with no token that resolvedSend the token from one of riptide.mcp.auth.tokens; check the startup log for Failed to resolve MCP auth secret reference
-32000Too many active MCP SSE sessionsmax-sse-sessions streams are open, often a client reconnect loopClose streams, or raise the limit
-32000Server is shutting downStream requested during shutdownReconnect after restart
-32600Empty payload, Invalid RequestEmpty POST body, or no methodSend one JSON-RPC 2.0 object
-32700Parse error: ...Body is not valid JSONFix the body
-32601Method not found: ...Unsupported methodUse one from the methods table
-32602Tool not found: ..., Prompt not found: ..., Resource not found: ..., Missing tool name in paramsWrong name or URITake the name from tools/list, prompts/list or resources/list
-32603Tool execution error: ..., Internal error: ...A tool threw, or the SSE handler threw (HTTP 500)See the riptide log
rowMissing required parameter 'ip_address', Missing required parameter 'target_ip'Required argument absentPass it
rowInvalid IP address parameter: ..., Invalid target IP address parameter: ...Not an IPv4 or IPv6 literalPass a literal address, not a hostname
rowInvalid group_by dimension. Allowed: [...]group_by outside the allowed setUse one of the listed dimensions
rowClickHouse client is unavailable.The MCP service was built without a ClickHouse client. The client bean exists whenever riptide.mcp.enabled=true, so this is not expected in a running collectorReport it with the startup log
rowa ClickHouse or timeout messageQuery refused or slower than query-timeout-seconds; ACCESS_DENIED means the identity lacks SELECT on the tableUse the reader identity, or raise the timeout

Open questions​

  • An earlier revision listed Google Antigravity (agy mcp add ...) as a client. No reference for that CLI could be verified, so it was removed.
  • riptide_get_geo_asn_distribution falls back to 60 minutes when time_range_minutes is absent, but the coverage check then compares against a 15-minute request. Which default is intended is (unverified).