MCP in HydraSRT

July 17, 2026 · View on GitHub

HydraSRT exposes a Model Context Protocol (MCP) server for external clients (Cursor, Claude Desktop, and similar tools). Through MCP, an assistant can manage routes, inspect logs and node stats, and perform other curated operations without copying data from the UI.

This document describes what is implemented in the project as of the current version.


What works today

ComponentDescription
MCP serverHydraSrt.Mcp.Server built on hermes_mcp
TransportStreamable HTTP at the /mcp endpoint
AuthenticationBearer tokens from the tokens table (separate from the UI session)
Token managementREST API /api/tokens + Settings → MCP tokens tab (/settings/tokens)
Tools43 curated tools (routes, sources, destinations, tags, interfaces, nodes, observability)

Architecture

flowchart LR
  Client[MCP client] -->|HTTP Bearer| McpAuth[McpAuth plug]
  McpAuth -->|hash lookup in DB| Hermes[Hermes StreamableHTTP]
  Hermes --> Server[HydraSrt.Mcp.Server]
  Server --> Registry[HydraSrt.Mcp.ToolRegistry]
  Registry --> Db[(SQLite / VictoriaMetrics / VictoriaLogs)]
  1. Phoenix accepts requests at /mcp.
  2. HydraSrtWeb.Plugs.McpAuth validates the Authorization: Bearer <token> header.
  3. Hermes.Server.Transport.StreamableHTTP.Plug handles the MCP protocol (JSON-RPC, SSE).
  4. HydraSrt.Mcp.Server delegates to HydraSrt.Mcp.ToolRegistry, which dispatches to domain modules (Db, HydraSrt, Analytics, SystemInterfaces, NodeStats) without HTTP self-calls.

The MCP server starts with the application (HydraSrt.Application), alongside Hermes.Server.Registry.


/mcp endpoint

  • URL: http://<host>:<port>/mcp (default: http://localhost:4000/mcp)
  • Methods: GET (SSE), POST (JSON-RPC), DELETE (session close) — per MCP Streamable HTTP
  • Without a token: 401 Unauthorized and the WWW-Authenticate: Bearer header

MCP does not use the session token from POST /api/login. /mcp requires a dedicated MCP token.


Access tokens

Purpose

Tokens are long-lived API keys for MCP clients. You can create several (for example, one for Cursor and one for another agent) and revoke them independently.

Storage

tokens table (SQLite):

ColumnDescription
idUUID (binary_id)
nameHuman-readable name (unique)
hashSHA-256 hash of the secret (not plaintext)
inserted_at, updated_atCreated / updated timestamps

On create, a random secret is generated (30 bytes, URL-safe Base64). Only the hash is stored in the database; the full value is shown once in the UI after creation.

Managing via the UI

  1. Sign in to the web UI (normal login).
  2. Settings → MCP tokens (/settings/tokens).
  3. Add token — enter a name and save.
  4. In the Copy your MCP token modal — copy the secret (Copy token button).
  5. If the secret is lost — Delete the old token and create a new one (the value cannot be recovered).

You can rename a token (Edit); you cannot change the secret of an existing token.

Managing via the REST API

Requires session authentication from the UI (Authorization: Bearer <session_token> from /api/login).

MethodPathAction
GET/api/tokensList tokens (no secrets)
POST/api/tokensCreate; secret only in the response
PUT/api/tokens/:idRename
DELETE/api/tokens/:idRevoke

Payload details and response examples are in docs/api.md (MCP Tokens and MCP Endpoint sections).


MCP client setup

Cursor (example)

In MCP configuration (mcp.json or Cursor settings):

{
  "mcpServers": {
    "hydrasrt": {
      "url": "http://localhost:4000/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_MCP_TOKEN"
      }
    }
  }
}

Replace:

  • http://localhost:4000 — with your HydraSRT URL (host and port from PORT / reverse proxy).
  • YOUR_MCP_TOKEN — the secret copied when creating the token in Settings.

After changes, restart the MCP client or reconnect the server.

Google Antigravity

Antigravity uses serverUrl, not url (Cursor-style configs will fail with a JSON-RPC decode error on tools/list because the Bearer token is never sent).

Shared config file: ~/.gemini/config/mcp_config.json (or ~/.gemini/antigravity/mcp_config.json on some installs).

{
  "mcpServers": {
    "hydrasrt": {
      "type": "streamable-http",
      "serverUrl": "http://localhost:4000/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_MCP_TOKEN"
      }
    }
  }
}

Do not use "url" or Cursor-style "transport": "streamable_http" — Antigravity expects serverUrl and optionally "type": "streamable-http".

If the server still shows a red error but lists tools, that is a known Antigravity quirk — try calling a tool anyway. If tools/list keeps failing, use the mcp-remote stdio bridge (proven workaround for HTTP MCP servers in Antigravity):

{
  "mcpServers": {
    "hydrasrt": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "http://localhost:4000/mcp",
        "--header",
        "Authorization:${HYDRA_MCP_AUTH}",
        "--allow-http",
        "--transport",
        "http-only"
      ],
      "env": {
        "HYDRA_MCP_AUTH": "Bearer YOUR_MCP_TOKEN"
      }
    }
  }
}

After editing, open Settings → Customizations → Installed MCP Servers → Refresh, or restart Antigravity.

Verification

  • Without an Authorization header — 401.
  • With an invalid token — 401.
  • With a valid MCP token — the client completes MCP initialization and sees 43 tools (see catalog below).
  • A UI session token does not work for /mcp.

Response contract

MCP tools return structured JSON via Hermes:

  • Success: {"data": ...} in most cases. List endpoints may also include "meta" (pagination).
  • Errors: {"error": "message"} or {"errors": {...}} for validation failures.
  • Node tools: REST returns raw JSON for nodes; MCP normalizes into {"data": ...} for consistency.
  • Unknown tool: structured error {"error": "Unknown tool: <name>"} with isError: true.

Payload field shapes match docs/api.md where applicable.


Tool contracts

Scoped endpoints

All source and destination tools require route_id in addition to entity IDs. MCP uses destination_id (REST paths use dest_id).

Time ranges (analytics / logs)

Supported window values: last_30_min, last_hour, last_6_hour, last_24_hour, or custom from + to (ISO8601).

window: live is not supported. The UI converts “live” to rolling from/to client-side. MCP clients must send explicit from and to for polling (for example, the last 5 minutes).

Nodes

  • get_self_node — local node only; no node_id parameter.
  • get_node_analyticsnode_id must match the host field from list_nodes / get_self_node.

Probes

test_route_source and test_source run an active ffprobe network probe. MCP tools use a shorter timeout (~3.5s via mcp_probe_timeout_ms); REST probes may block up to 15 seconds.

Out of scope

MCP token CRUD, backup/restore, WebSocket live push, signal generation, pipeline kill, and UI login remain REST/UI only. See docs/api.md for those endpoints.


Available tools (43)

Implementation reads from HydraSrt.Db and related modules directly (no internal HTTP calls).

Routes (10)

ToolDescription
list_routesList routes (page, limit, sort_by optional)
get_routeRoute by route_id (includes sources and destinations)
create_routeCreate route (route object)
update_routeUpdate route (route_id, route)
delete_routeDelete route (route_id)
start_routeStart pipeline (route_id)
stop_routeStop pipeline (route_id)
restart_routeRestart pipeline (route_id)
switch_route_sourceSwitch active source (route_id, source_id)
test_route_sourceProbe route source config (route object; MCP ~3.5s, REST up to 15s)

Sources (7) — require route_id

ToolDescription
list_sourcesList sources for a route
get_sourceGet source (source_id)
create_sourceCreate source (source object)
update_sourceUpdate source (source_id, source)
delete_sourceDelete source (source_id)
reorder_sourcesReorder sources (source_ids array)
test_sourceProbe saved source (source_id; MCP ~3.5s, REST up to 15s)

Destinations (5) — require route_id

ToolDescription
list_destinationsList destinations for a route
get_destinationGet destination (destination_id)
create_destinationCreate destination (destination object)
update_destinationUpdate destination (destination_id, destination)
delete_destinationDelete destination (destination_id)

Tags (4)

ToolDescription
list_tagsList route tags
create_tagCreate tag (tag object with name)
update_tagUpdate tag (tag_id, tag)
delete_tagDelete tag (tag_id)

Interfaces (8)

ToolDescription
list_interfacesConfigured interface aliases (SQLite)
get_interfaceGet configured interface (interface_id)
create_interfaceCreate alias (interface object)
update_interfaceUpdate alias (interface_id, interface)
delete_interfaceDelete alias (interface_id)
list_system_interfacesOS interfaces from ifconfig (sys_name, ip, …)
get_system_interfaceOne OS interface by sys_name
get_system_interfaces_rawRaw ifconfig text

The ip field is "-" when no address was parsed. IPv6-only interfaces may show an IPv6 value per the parser.

Nodes (3)

ToolDescription
list_nodesCluster nodes with CPU/RAM/network (local node today)
get_self_nodeLocal node snapshot (no parameters)
get_node_analyticsNode metrics time-series (node_id, time range)

Observability (6)

ToolDescription
get_route_eventsRoute event log (route_id, time range, filters)
get_route_pipeline_logsGStreamer pipeline logs (route_id, time range)
get_route_pipeline_log_distinctDistinct log values (route_id, column: level or category)
get_routes_status_historyRoute status change history (optional route_id, status)
get_route_analyticsRoute metrics time-series (route_id, time range)
get_routes_status_analyticsFleet status time-series (time range)

WebSocket live stats from the UI are not replicated over MCP; poll analytics/log tools instead.


Security

TopicBehavior
Secret in DBSHA-256 hash stored, not plaintext
Secret displayOnly once on POST /api/tokens / UI create
UI session vs MCPDifferent tables / checks; not interchangeable
RevocationDELETE /api/tokens/:id — immediate
Empty token list/mcp unavailable to everyone (all requests 401)
Token scopeA valid MCP token grants the same operational power as the REST API for curated tools: route/source/destination CRUD, start/stop/restart, ffprobe network probes (may reach internal hosts; MCP ~3.5s, REST up to ~15s), raw ifconfig, and analytics reads. Treat leaked tokens like leaked admin API keys — revoke immediately and issue narrowly scoped tokens per client.
HermesAuthentication is our Plug before forward; Hermes only passes conn.assigns into the frame

Secrets in SQLite are not encrypted with Cloak (as in Supavisor): a one-way hash is enough for MCP tokens because plaintext is needed only once by the client.


Repository files

PathPurpose
lib/hydra_srt/mcp/server.exHermes MCP server entrypoint
lib/hydra_srt/mcp/tool_registry.exTool registration and dispatch
lib/hydra_srt/mcp/input_schema.exJSON Schema to Hermes input schema conversion
lib/hydra_srt/mcp/helpers.exResponse envelopes and error mapping
lib/hydra_srt/mcp/tools/*.exTool handlers by domain
lib/hydra_srt/route_control.exShared route switch/update logic
lib/hydra_srt_web/plugs/mcp_auth.exBearer check for /mcp
lib/hydra_srt_web/controllers/token_controller.exREST CRUD for tokens
lib/hydra_srt/db.excreate_token, authenticate_mcp_token, …
lib/hydra_srt/api/token.exEcto schema for tokens
lib/hydra_srt_web/router.ex/mcp, /api/tokens
web_app/src/pages/settings/McpTokensTab.tsxMCP tokens tab UI
web_app/src/utils/tokensApi.tsTyped API client
priv/repo/migrations/20260523120000_create_tokens.exsTable migration

Database migration

On a new or updated instance:

mix ecto.migrate

Creates the tokens table with indexes on name and hash.


Current limitations

  • Curated toolset only — not every REST endpoint has an MCP equivalent (see Out of scope above).
  • No MCP resources or prompts yet (tools only).
  • No token expiry (expires_at) or last_used_at — tokens remain valid until deleted.
  • No Playwright E2E tests for /settings/tokens yet; MCP tools and auth are covered by unit tests and opt-in HTTP E2E tests under test/e2e_mcp/ (E2E_MCP=true mix test --only e2e_mcp).