Skip to content

MCP server

The MCP server (agent/mcp/, container image ai-mcp, port 8085) exposes the ClickHouse analytics as callable tools over the Model Context Protocol. The FastAPI agent is its only client — the agent's LLM asks for a tool by name, the agent calls it here, and this server runs the SQL.

What it is

  • Built on FastMCP (mcp.server.fastmcp.FastMCP), pinned to mcp==1.29.0 (the 2.0.0 release moved this import — don't bump without checking), Python 3.10+.
  • Runs over streamable-http (transport="streamable-http"), not stdio — so it's a standalone, network-reachable server the separate agent container can call at http://ai-mcp:8085/mcp. (A raw GET /mcp returns 406 — it needs proper MCP headers; that 406 just means "reachable".)
  • mcp_server.py is a thin entrypoint: create the app, call each tool module's register(mcp) to attach its @mcp.tool() functions, then mcp.run(...).

The 27 tools

Grouped by module under agent/mcp/tools/:

Module Tools (count) Reads
security_tools.py 9 — suspicious_ip_activity, ip_request_history, top_suspicious_paths, suspicious_ip_range_activity, component_traffic_breakdown, pop_traffic_breakdown, pop_attack_summary, node_attack_summary, device_ip_correlation cproxy_events (+ elb_events via UNION ALL)
qoe_tools.py 5 — cmcd_qoe_score, qoe_score_trend, ip_range_qoe_scores, device_qoe_scores, pop_qoe_scores cproxy_events (CMCD columns)
stream_tools.py 6 — find_new_streams, count_active_streams, streams_with_recent_traffic, current_viewer_counts, get_stream_status, stream_lifecycle_events streamer_events; two hit the streamer REST API, not ClickHouse
log_tools.py 4 — find_http_errors, log_level_breakdown, top_source_functions, top_clients cproxy_events (status/clients) + streamer_events (level/source)
query_tools.py 3 — list_tables, describe_table, run_query any cdn_ai table (read-only)

The full tool → table map (which table each tool actually queries) lives in the pilot reference so it stays in one place: CDN-AI reference — tools → table.

All 27 target the cdn_ai database (the retarget off the retired cdn_access_logs table landed in 769d78b). run_query only accepts SELECT/WITH; anything else is rejected before it reaches ClickHouse.

Shared plumbing (common.py)

  • The ClickHouse client factory (client()), reading CLICKHOUSE_* env — see settings.
  • The empty-result retry wrapper (8 × 0.3 s): retries a query that returns zero rows, a workaround for a past pod replication skew. It adds latency to genuinely-empty queries and is a candidate to trim.
  • SAFE_STREAM_ID and table-name validation to keep tool arguments from reaching SQL unchecked.

Adding a tool

  1. Add a @mcp.tool()-decorated function inside a module's register(mcp).
  2. Query cdn_ai via common.client(); return json.dumps(...).
  3. If the LLM should reach it, add its name to the right category in the agent's TOOL_CATEGORIES so tool scoping surfaces it.
  4. Prefer a purpose-built tool with the correct SQL baked in over expecting the small model to write SQL — that's the whole reason these exist.

Run / deploy

  • Container: agent/Dockerfile.mcp → image ai-mcp; serves 8085.
  • Staging: k8s Deployment ai-mcp (services/ai/mcp), one replica. The agent finds it via MCP_STREAMER_URL=http://ai-mcp:8085/mcp.
  • Two images, one repo: ai-agent and ai-mcp are built by the same CI from the same agent/ context with different Dockerfiles, and their tags are bumped together in the GitOps repo per push.