Architecture Overview

July 30, 2026 · View on GitHub

System diagram

Claude / MCP client  (Claude Code, Codex, curl)


HTTP Transport (axum, port 9158)


Auth Middleware  (bearer token check)


RMCP Streamable HTTP service  (stateless JSON-response mode)


mcp/tools.rs  (thin shim: parse JSON args → call service)


GotifyService (app.rs)  (business logic, destructive gate)


GotifyClient (gotify.rs)  (reqwest HTTP client)
         │  X-Gotify-Key: <token>

Gotify REST API  (external server)

Mode dispatch (main.rs)

main.rs reads the first CLI argument and routes to one of three entry points:

ArgsEntry pointDescription
(none), serve, serve mcpserve_mcp()RMCP Streamable HTTP on port 9158
mcpserve_stdio_mcp()RMCP stdio transport for local child-process clients
anything elserun_cli()Direct CLI execution

All three entry points load the same Config and construct the same GotifyService. The transport layer is the only difference.

Layer responsibilities

gotify.rs — HTTP REST client

  • Owns the reqwest::Client instance and base URL.
  • Implements all Gotify REST API calls: GET, POST, PUT, DELETE.
  • Attaches X-Gotify-Key header with the appropriate token per operation type.
  • send_message uses app_token; all other methods use client_token.
  • HTTP 204 responses are normalized to {"status":"ok"}.
  • Non-2xx responses bail with the HTTP status and response body.
  • No business logic — only HTTP mechanics.

app.rs — GotifyService (business layer)

  • Single GotifyService struct shared by CLI and MCP.
  • destructive_gate(confirm) — central safety check for all destructive operations. Returns Err unless confirm == true or allow_destructive == true.
  • All methods delegate directly to GotifyClient. No logic beyond the gate.

mcp/tools.rs — MCP shim

  • execute_tool(state, name, args) — top-level tool dispatch. Only tool name is "gotify".
  • dispatch(state, args) — matches on action string, extracts typed args from serde_json::Value, calls GotifyService.
  • Zero business logic. If parsing fails, returns a typed anyhow::Error.

cli.rs — CLI shim

  • Parses Vec<String> args into a typed CliCommand enum.
  • Calls GotifyService methods identically to the MCP shim.
  • Formats output as human-readable text or JSON (--json flag).

mcp.rs — HTTP server

  • Builds the axum::Router with POST /mcp and GET /health routes.
  • AuthPolicy enum selects bearer-token enforcement or loopback-dev bypass.
  • Mounts the RMCP service as a tower-compatible layer.

Request flow (MCP)

POST /mcp  { "method": "tools/call", "params": { "name": "gotify", "arguments": { "action": "send", ... } } }

Auth middleware  (checks Authorization: Bearer header)

RMCP layer  (validates JSON-RPC envelope, routes to tool handler)

execute_tool("gotify", args)

dispatch() → match "send" → GotifyService::send(...)

GotifyClient::send_message(...)  →  POST /message  (X-Gotify-Key: <app_token>)

Gotify server returns created message JSON

Value returned up the call stack → wrapped in MCP content → JSON-RPC response

Auth model

ConditionAuth behavior
GOTIFY_MCP_TOKEN setBearer token required on all /mcp requests
GOTIFY_MCP_NO_AUTH=trueAuth disabled
Bind host starts with 127.Auth disabled (loopback dev mode)
stdio modeNo auth (process boundary is the trust boundary)

/health is always unauthenticated.

Error handling

SourceErrorBehavior
Auth middlewareMissing/invalid bearer tokenHTTP 401
Tool dispatchUnknown tool nameRMCP tool error
Tool handlerMissing required argRMCP invalid params error
Destructive gateNo confirm, no env flaganyhow::Error → MCP isError: true
Gotify clientNon-2xx HTTP responseanyhow::bail! with status + body
ConfigEmpty GOTIFY_URLanyhow::bail! at startup

Cross-references