# MCP Application — Complete Reference An Elixir OTP application that implements an MCP (Model Context Protocol) server with integrated LLM chat, user authentication, alarm scheduling, task management, blog authoring, A2A agent interoperability, and a ZeroMQ-based Python bridge. --- ## Table of Contents - [Architecture Overview](#architecture-overview) - [Supervision Tree](#supervision-tree) - [Module Reference](#module-reference) - [Application & Supervision](#application--supervision) - [HTTP Transport](#http-transport) - [MCP Server Layer](#mcp-server-layer) - [MCP Client Layer](#mcp-client-layer) - [Chat Layer (LLM Integration)](#chat-layer-llm-integration) - [User & Authentication](#user--authentication) - [Database Layer](#database-layer) - [Alarm & Scheduling](#alarm--scheduling) - [Logging Pipeline](#logging-pipeline) - [Auto-Compilation](#auto-compilation) - [Blog System](#blog-system) - [Task Management](#task-management) - [PageScraper (Browser DOM Extraction)](#pagescraper-browser-dom-extraction) - [A2A Protocol](#a2a-protocol) - [ZeroMQ Bridge](#zeromq-bridge) - [Utilities](#utilities) - [Database Schemas](#database-schemas) - [HTTP Routes](#http-routes) - [Configuration](#configuration) - [Dependencies](#dependencies) - [Mermaid Architecture Diagram](#mermaid-architecture-diagram) --- ## Architecture Overview The application is organized into several layers: 1. **HTTP Transport** — Bandit web server (HTTPS :8443 in dev) with Plug.Router for authentication, JSON-RPC, SSE, chat, blog, and task routes. A separate DocsRouter serves documentation on /docs. 2. **MCP Server** — Implements the Hermes.Server behaviour to expose tools and resources with per-user permission filtering. A Manager GenServer bridges the HTTP router to the MCP callbacks. 3. **MCP Client** — Per-user GenServer instances that call the Manager in-process, tracked by MCPClientTracker. 4. **Chat** — Per-user ServerChat GenServers with pluggable LLM providers (Anthropic, OpenRouter, Perplexity) that integrate MCP tools into the conversation loop. 5. **Auth** — Cookie-encrypted sessions with Argon2 password hashing, integer-based permissions, account locking, and audit logging. 6. **Database** — SQLite via Sqler GenServers with prepared statement caching, auto-generated millisecond IDs, and optimistic locking. 7. **Alarm & Scheduling** — Persistent alarm scheduling with natural language time parsing, Timer process for delayed message delivery, and Pushover push notifications. 8. **Task Management** — Full-featured task manager with priorities, due dates, tags, subtasks, and 60+ query functions with a web UI. 9. **PageScraper** — Server-driven browser DOM extraction. Reacts to URL patterns visited in Chrome, orchestrates content script DOM queries/clicks/watches, and stores results in SQLite. 10. **A2A** — Google A2A protocol implementation for agent-to-agent interoperability. 11. **ZeroMQ** — Python process bridge via MuonTrap + ZeroMQ (chumak or erlzmq backends). --- ## Supervision Tree ``` Mcp.Supervisor (one_for_one) ├── Sqler (:users_db) — SQLite database for users & audit logs ├── Sqler (:compile_log_db) — SQLite database for auto-compile logs ├── MCPServer.Manager — MCP server state manager (singleton GenServer) ├── Bandit (HTTPS :8443) — HTTP/HTTPS server running MCPServer.Router ├── AutoCompile — File watcher for hot-reload (dev only, watches lib/ and asset/blogs/) ├── LogSubscriber — ANSI-colored log output to IEx console ├── Alarm — Persistent alarm scheduler (starts its own Sqler + Timer) ├── Pushover — Push notification client (Pushover API) ├── MCPClientTracker — Per-user MCP client process registry ├── Sqler (:chat_db) — SQLite database for chat history ├── ServerChatTracker — Per-user LLM chat process registry ├── Sqler (:scheduler) — SQLite database for scheduler entries ├── Sqler (:a2a_db) — SQLite database for A2A tasks ├── Sqler (:tasks_db) — SQLite database for task management ├── Registry (ZmqCounter) — Registry for ZmqCounter instances ├── Registry (PageScraperRegistry) — Per-user per-pattern PageScraperServer instances ├── Sqler (:page_scraper_db) — SQLite database for page scraper extractions ├── App — General-purpose term storage (starts its own Sqler) └── (dynamic) PageScraperServer — Per {username, pattern_id} extraction orchestrators ``` After the supervisor starts, database schemas are initialized: - `User.setup_database(:users_db)` — users + user_audit_log tables - `CompileLog.setup_database(:compile_log_db)` — auto_compile table - `ServerChat.setup_database(:chat_db)` — chat_log table - `A2AServer.setup_database(:a2a_db)` — a2a_tasks table - `TaskManager.setup_database(:tasks_db)` — tasks table - `PageScraper.setup_database(:page_scraper_db)` — page_scraper_extractions table --- ## Module Reference ### Application & Supervision | Module | File | Type | Purpose | |--------|------|------|---------| | `Mcp.Application` | `lib/mcp/application.ex` | Application | OTP application entry point; builds supervision tree, initializes DB schemas | | `Mcp` | `lib/mcp.ex` | Module | Top-level module (placeholder) | ### HTTP Transport | Module | File | Type | Purpose | |--------|------|------|---------| | `MCPServer.Router` | `lib/mcp_server_router.ex` | Plug.Router | Main HTTPS router: auth, login, chat, calculator, greeting, counter, blog, tasks, A2A, SSE, JSON-RPC | | `DocsRouter` | `lib/docs_router.ex` | Plug.Router | Serves documentation files from `asset/docs/`; forwarded from main router at `/docs` | ### MCP Server Layer | Module | File | Type | Purpose | |--------|------|------|---------| | `MCPServer` | `lib/mcp_server.ex` | Hermes.Server behaviour | MCP tool/resource definitions and handlers with permission-based access control | | `MCPServer.Manager` | `lib/mcp_server_manager.ex` | GenServer (singleton) | Wraps MCPServer state; loads user permissions from DB per-request; bridges Router → MCP callbacks | **MCP Tools:** | Tool | Permission | Description | |------|-----------|-------------| | `add_numbers` | 123_456 (view) | Adds two numbers | | `get_greeting` | none | Returns a personalized greeting | | `is_admin` | none | Checks if user has admin permissions | | `get_counter` | none | Returns current counter value | | `increment_counter` | 11111 (admin) | Increments server counter | **MCP Resources:** | URI | Permission | Description | |-----|-----------|-------------| | `file:///config/settings.json` | 123_456 (view) | Server configuration JSON | ### MCP Client Layer | Module | File | Type | Purpose | |--------|------|------|---------| | `MCPClient` | `lib/mcp_client.ex` | GenServer (per-user) | Delegates tool/resource calls to Manager with user context | | `MCPClientTracker` | `lib/mcp_client_tracker.ex` | GenServer (singleton) | Tracks user_id → MCPClient pid; auto-starts on first access; monitors for cleanup | ### Chat Layer (LLM Integration) | Module | File | Type | Purpose | |--------|------|------|---------| | `ServerChat` | `lib/server_chat.ex` | GenServer (per-user) | Per-user chat with pluggable LLM providers; MCP tool integration; 30-min idle timeout; busy flag prevents concurrent calls | | `ServerChatTracker` | `lib/server_chat_tracker.ex` | GenServer (singleton) | Tracks user_id → ServerChat pid; auto-starts on first access; monitors for cleanup | | `ServerChat.Provider` | `lib/server_chat/provider.ex` | Behaviour | Defines `call_llm/4`, `handle_response/5`, `format_tools/1` callbacks | | `ServerChat.Anthropic` | `lib/server_chat/anthropic.ex` | Provider impl | Anthropic Messages API (`/v1/messages`); tool-use loop | | `ServerChat.OpenRouter` | `lib/server_chat/openrouter.ex` | Provider impl | OpenRouter OpenAI-compatible API (`/v1/chat/completions`); tool-use loop | | `ServerChat.Perplexity` | `lib/server_chat/perplexity.ex` | Provider impl | Perplexity Responses API with citations | **Key ServerChat API:** ```elixir ServerChat.chat(pid, message) # Send message, get reply ServerChat.history(pid) # Get conversation history ServerChat.clear(pid) # Clear history ServerChat.switch_provider(pid, :openrouter, "model-name") # Switch LLM ServerChat.set_model(pid, "model-name") # Change model, keep history ServerChat.model(pid) # Get current model ServerChat.refresh_tools(pid) # Re-fetch MCP tools ``` ### User & Authentication | Module | File | Type | Purpose | |--------|------|------|---------| | `User` | `lib/user.ex` | Module | User CRUD, authentication (Argon2), sessions, permissions, account locking, audit logging | | `Auth.Authorization` | `lib/auth/authorization.ex` | Module | `authorized?/2` helper for integer-based permission checks | | `Auth.AuthorizationPlug` | `lib/auth/authorization_plug.ex` | Plug | Route-level authorization guards | **Permission System:** - Permissions are integer keys stored as JSON arrays in the users table (e.g., `[123456, 11111]`) - At runtime, converted to maps for O(1) lookup: `%{123_456 => true, 11111 => true}` - Guards use `is_map_key(permissions, @permission)` pattern - Known permission constants: `123_456` (view), `11111` (admin) **Authentication Flow:** 1. `POST /login` with email + password 2. `User.find_by_email/1` → `User.authenticate/3` (Argon2 verify) 3. On success: encrypted cookie set with user ID, redirect to `/chat` 4. On failure: failed attempt tracking, account locking after 5 failures (15-min lockout) 5. Subsequent requests: cookie decrypted → user loaded from DB with permissions ### Database Layer | Module | File | Type | Purpose | |--------|------|------|---------| | `Sqler` | `lib/sqler.ex` | GenServer | SQLite wrapper via Exqlite; CRUD, schema introspection, prepared statement caching, auto-generated millisecond IDs, optimistic locking | **Sqler Instances:** | Registered Name | DB File | Purpose | |----------------|---------|---------| | `:users_db` | `data/users.sqlite` | Users, audit logs | | `:compile_log_db` | `data/log.sqlite` | Auto-compile history | | `:chat_db` | `data/chat.sqlite` | Chat conversation logs | | `:scheduler` | `data/scheduler.sqlite` | Scheduler entries | | `:a2a_db` | `data/a2a.sqlite` | A2A protocol tasks | | `:tasks_db` | `data/tasks.sqlite` | Task management | | `:page_scraper_db` | `data/page_scraper.sqlite` | PageScraper extractions | | (inline in Alarm) | `data/alarm.sqlite` | Alarm persistence | | (inline in App) | `data/app.sqlite` | General term storage | **Key Sqler API:** ```elixir Sqler.sql(db, "SELECT * FROM users") # Raw SQL Sqler.sql(db, "SELECT * FROM users WHERE id = ?", [1]) # Parameterized Sqler.insert(db, "users", %{name: "Alice"}) # Auto-generates id + updated_at Sqler.update(db, "users", %{id: 1, updated_at: ts, name: "Bob"}) # Optimistic locking Sqler.select(db, "users", %{name: "Alice"}, limit: 10) # Conditional select Sqler.get(db, "users", 1) # Single row by ID Sqler.delete(db, "users", %{id: 1}) # Conditional delete Sqler.tables(db) # List all tables Sqler.fields(db, "users") # List column names Sqler.schema(db) # Full sqlite_master dump ``` ### Alarm & Scheduling | Module | File | Type | Purpose | |--------|------|------|---------| | `Alarm` | `lib/alarm.ex` | GenServer (singleton) | Persistent alarm scheduler; SQLite-backed; natural language time parsing via Chronic; crash recovery | | `Timer` | `lib/timer.ex` | GenServer (singleton) | Single timer reference manager; `Process.send_after` wrapper; fires messages to target processes | | `Scheduler` | `lib/scheduler.ex` | GenServer | Callback orchestration with `{module, function, args}` persistence (WIP) | | `Pushover` | `lib/pushover.ex` | GenServer (singleton) | Pushover push notification API client; async/sync send; alarm integration | **Alarm Flow:** 1. `Alarm.set_timer("tomorrow at 9am", Pushover, {:send, %{message: "Hello"}})` — parses time, persists to SQLite, updates Timer 2. `Timer` fires at scheduled time → `GenServer.cast(Pushover, {:send, %{message: "Hello"}})` → `Alarm.fired(id)` 3. On restart, Alarm restores pending alarms from database and re-schedules the next one ### Logging Pipeline | Module | File | Type | Purpose | |--------|------|------|---------| | `LoggerNotifier` | `lib/logger_notifier.ex` | :gen_event backend | Bridges Elixir Logger → Notifier pub/sub; filters by log level | | `Notifier` | `lib/notifier.ex` | GenServer | Generic pub/sub with process monitoring; supports string names (gproc) and atom names | | `LogSubscriber` | `lib/log_subscriber.ex` | GenServer (singleton) | Subscribes to `:logger_notifier`; renders ANSI-colored log output to IEx console | **Logging Flow:** ``` Logger → LoggerNotifier (gen_event) → Notifier (:logger_notifier) → LogSubscriber → IO.puts (ANSI) ``` ### Auto-Compilation | Module | File | Type | Purpose | |--------|------|------|---------| | `AutoCompile` | `lib/auto_compile.ex` | GenServer | Watches `lib/` and `asset/blogs/` for file changes; hot-reloads `.ex` files; syncs blogs via unison; runs `test_*` functions; plays beep on completion | | `CompileLog` | `lib/compile_log.ex` | Module | Logs each compilation event with git diff to `:compile_log_db`; stats and cleanup functions | **File Change Handlers:** | Extension | Action | |-----------|--------| | `.ex` | `Code.compile_file/1` → `CompileLog.log/1` → `AutoCompile.test_run/1` | | `.js` | `Notifier.notify(:web_notifier, [:websocket, :reload])` | | `.proto` | `protoc --elixir_out=.` | | `.html` (in `asset/blogs/`) | `unison blogs` sync | ### Blog System | Module | File | Type | Purpose | |--------|------|------|---------| | `Blog` | `lib/blog.ex` | Module | Blog post CRUD (file-based in `asset/blogs/`); Summernote WYSIWYG editor; slug filename derivation; Medium-style reader with dark/light theme toggle | **Blog Routes:** | Route | Purpose | |-------|---------| | `GET /blogs` | List all posts | | `GET /blog` | Summernote editor (new post) | | `GET /blog/edit/:filename` | Editor with existing content | | `POST /blog/save` | Save new post (derives filename from content) | | `POST /blog` | Update existing post | | `DELETE /blog` | Delete a post | | `GET /blog/:filename` | Public reader view (no auth required) | ### Task Management | Module | File | Type | Purpose | |--------|------|------|---------| | `TaskManager` | `lib/task_manager.ex` | Module | SQLite-backed task management with CRUD, 4 statuses, priorities, due dates, tags (JSON), subtasks, and 60+ query functions across 11 categories | | `TaskWeb` | `lib/task_web.ex` | Module | HTML rendering and query dispatch for the TaskManager web UI; dashboard, forms, query index | **Task Schema Fields:** `id`, `updated_at`, `title`, `priority`, `note`, `after_at`, `expires_at`, `completed_at`, `waiting_for`, `status`, `due_at`, `tags`, `parent_id` **Task Statuses:** `todo`, `in_progress`, `blocked`, `completed` **Query Categories (60+ functions):** Status, Due Dates, Waiting/Expiry, Priority, Completion, Tags, Subtasks, Blocking, Dashboard, Analytics, Data Integrity ### PageScraper (Browser DOM Extraction) | Module | File | Type | Purpose | |--------|------|------|---------| | `PageScraper` | `lib/page_scraper.ex` | Module | URL pattern registry and extraction DB; `match_url/1` checks URLs against patterns; CRUD for extraction results in `:page_scraper_db` | | `PageScraperServer` | `lib/page_scraper_server.ex` | GenServer (per {user, pattern}) | Orchestrates browser DOM extraction for a matched URL; sends dom_commands, tracks pending requests, dispatches results to handler; 5s departure grace, 30min idle timeout | | `PageScraper.Handler` | `lib/page_scraper/handler.ex` | Behaviour | Defines `on_match/2`, `on_extraction/3`, `on_departure/1`, `on_mutation/3` callbacks; `use PageScraper.Handler` provides defaults | | `PageScraper.Handlers.GithubPR` | `lib/page_scraper/handlers/github_pr.ex` | Handler impl | Extracts PR title, state, author, body, file count, commit count from GitHub PR pages | **Extraction Types:** `snapshot` (initial), `poll` (periodic), `sequence` (multi-step), `mutation` (DOM change) **Content Script Commands:** `dom_query`, `dom_query_all`, `dom_click`, `dom_wait`, `dom_watch`, `dom_unwatch`, `dom_extract` ### A2A Protocol | Module | File | Type | Purpose | |--------|------|------|---------| | `A2AServer` | `lib/a2a_server.ex` | Module | Google A2A protocol server; agent card discovery; tasks/send, tasks/get, tasks/cancel via JSON-RPC 2.0; persists to `:a2a_db` | | `A2AClient` | `lib/a2a_client.ex` | Module | Stateless A2A HTTP client; discover agents, send/get/cancel tasks | | `A2ADemo` | `lib/a2a_demo.ex` | Module | Demo/example usage of A2A client | **A2A Endpoints:** | Route | Purpose | |-------|---------| | `GET /.well-known/agent.json` | Agent card discovery (public) | | `POST /a2a` | JSON-RPC 2.0: `tasks/send`, `tasks/get`, `tasks/cancel` | ### ZeroMQ Bridge | Module | File | Type | Purpose | |--------|------|------|---------| | `ZmqCounter` | `lib/zmq_counter.ex` | GenServer (per-instance, via Registry) | Wraps a Python counter process via MuonTrap + ZeroMQ; supports chumak (pure Erlang, TCP) and erlzmq_dnif (NIF, TCP+IPC) backends; JSON messages; auto-restarts Python on crash | ### Utilities | Module | File | Type | Purpose | |--------|------|------|---------| | `SystemTimezone` | `lib/system_timezone.ex` | Module | Cross-platform timezone detection (config → TZ env → /etc/timezone → /etc/localtime → UTC); DateTime utilities | | `TextChunker` | `lib/text_chunker.ex` | Module | Semantic text chunker for embeddings/RAG; sentence-aware splitting with overlap | | `Index` (Floki) | `lib/floki/index.ex` | Module | HTML page generator with permission guard | | `App` | `lib/app.ex` | GenServer (singleton) | General-purpose term storage backed by SQLite; serializes Elixir terms via `:erlang.term_to_binary` | | `Example` | `lib/example.ex` | Module | Demo runner for MCP client flows | --- ## Database Schemas ### users (`:users_db`) ```sql CREATE TABLE users ( id INTEGER PRIMARY KEY, username TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, permission_keys TEXT NOT NULL DEFAULT '[]', -- JSON array of integers created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, last_login_at INTEGER, last_login_ip TEXT, failed_login_attempts INTEGER DEFAULT 0, locked_until INTEGER, is_active INTEGER DEFAULT 1, session_token TEXT, session_expires_at INTEGER, password_changed_at INTEGER, must_change_password INTEGER DEFAULT 0, two_factor_enabled INTEGER DEFAULT 0, two_factor_secret TEXT, metadata TEXT DEFAULT '{}' ) ``` ### user_audit_log (`:users_db`) ```sql CREATE TABLE user_audit_log ( id INTEGER PRIMARY KEY, user_id INTEGER, action TEXT NOT NULL, details TEXT, ip_address TEXT, user_agent TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ) ``` ### chat_log (`:chat_db`) ```sql CREATE TABLE chat_log ( id INTEGER PRIMARY KEY, updated_at INTEGER, user_id INTEGER NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL ) ``` ### auto_compile (`:compile_log_db`) ```sql CREATE TABLE auto_compile ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, time INTEGER NOT NULL, git_diff TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ) ``` ### alarm (inline in Alarm) ```sql CREATE TABLE alarm ( id INTEGER PRIMARY KEY, updated_at INTEGER, set_at INTEGER NOT NULL, process TEXT NOT NULL, message BLOB NOT NULL, started_at INTEGER, completed_at INTEGER, reply TEXT, cancelled_at INTEGER ) ``` ### tasks (`:tasks_db`) ```sql CREATE TABLE tasks ( id INTEGER PRIMARY KEY, updated_at INTEGER, title TEXT NOT NULL, priority INTEGER NOT NULL DEFAULT 0, note TEXT, after_at INTEGER, -- deferred until (Unix seconds) expires_at INTEGER, -- expires at (Unix seconds) completed_at INTEGER, -- completion time (Unix seconds) waiting_for TEXT, status TEXT NOT NULL DEFAULT 'todo', due_at INTEGER, -- due date (Unix seconds) tags TEXT, -- JSON array e.g. '["#work","#home"]' parent_id INTEGER -- subtask hierarchy ) ``` ### page_scraper_extractions (`:page_scraper_db`) ```sql CREATE TABLE page_scraper_extractions ( id INTEGER PRIMARY KEY, updated_at INTEGER, user_id INTEGER NOT NULL, pattern_id TEXT NOT NULL, url TEXT NOT NULL, tab_id INTEGER, data TEXT NOT NULL, -- JSON-encoded extraction results extraction_type TEXT NOT NULL, -- snapshot | poll | sequence | mutation metadata TEXT DEFAULT '{}' -- JSON-encoded (captures, extraction_count) ) ``` ### a2a_tasks (`:a2a_db`) ```sql CREATE TABLE a2a_tasks ( id INTEGER PRIMARY KEY, updated_at INTEGER, user_id INTEGER NOT NULL, state TEXT NOT NULL DEFAULT 'submitted', history TEXT NOT NULL DEFAULT '[]', artifacts TEXT NOT NULL DEFAULT '[]' ) ``` ### log (inline in App) ```sql CREATE TABLE log ( id INTEGER PRIMARY KEY, updated_at INTEGER, term_bin TEXT -- Base64-encoded :erlang.term_to_binary ) ``` --- ## HTTP Routes ### MCPServer.Router (HTTPS :8443) | Method | Path | Auth | Purpose | |--------|------|------|---------| | GET | `/login` | No | Login page | | POST | `/login` | No | Authenticate user | | GET | `/chat` | Yes | Chat page with conversation history | | POST | `/chat` | Yes | Send chat message to LLM | | POST | `/chat/clear` | Yes | Clear chat history | | POST | `/message` | Yes | MCP JSON-RPC endpoint | | GET | `/sse` | Yes | Server-sent events stream | | GET | `/calculator` | Yes | Calculator page | | POST | `/calculator` | Yes | Add two numbers via MCP tool | | GET | `/greeting` | Yes | Greeting via MCP tool | | GET | `/counter` | Yes | Counter display (admin check) | | POST | `/counter` | Yes | Increment counter (admin only) | | GET | `/floki/index` | Yes | Permission-guarded index page | | GET | `/page` | Yes | User info page | | GET | `/blog` | Yes | Blog editor (new post) | | GET | `/blog/edit/:filename` | Yes | Blog editor (existing post) | | POST | `/blog` | Yes | Update blog post | | POST | `/blog/save` | Yes | Save new blog post | | DELETE | `/blog` | Yes | Delete blog post | | GET | `/blogs` | Yes | Blog listing | | GET | `/blog/:filename` | No | Read blog post (public) | | POST | `/save` | Yes | Save Summernote content + chunk | | GET | `/tasks` | Yes | Task dashboard | | GET | `/tasks/new` | Yes | New task form | | POST | `/tasks/new` | Yes | Create task | | GET | `/tasks/q` | Yes | Query function index | | GET | `/tasks/q/:name` | Yes | Execute named query | | GET | `/tasks/:id` | Yes | View task detail | | GET | `/tasks/:id/edit` | Yes | Edit task form | | POST | `/tasks/:id/edit` | Yes | Update task | | POST | `/tasks/:id/delete` | Yes | Delete task | | POST | `/tasks/:id/complete` | Yes | Mark task complete | | GET | `/.well-known/agent.json` | No | A2A agent card | | POST | `/a2a` | Yes | A2A JSON-RPC endpoint | | * | `/docs/*` | No | Documentation files (forwarded to DocsRouter) | ### Static Files Served from `asset/` at root: `blogs/`, `dark.css`, `index.html`, `pushover.html`, `summernote/` --- ## Configuration ### config/config.exs ```elixir config :logger, :default_handler, level: :warning config :logger, :logger_notifier, level: :debug config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase config :mcp, llm_provider: :openrouter, # :anthropic | :openrouter | :perplexity openrouter_model: "x-ai/grok-code-fast-1", perplexity_model: "sonar" ``` ### config/dev.exs ```elixir config :mcp, server_scheme: :https, server_port: 8443, server_certfile: "priv/cert/selfsigned.pem", server_keyfile: "priv/cert/selfsigned_key.pem", secure_cookies: true, auto_compile: true, secret_key_base: "...", cookie_salt: "..." ``` ### config/runtime.exs (prod) Required environment variables: `SECRET_KEY_BASE`, `COOKIE_SALT` Optional: `PORT`, `TLS_CERTFILE`, `TLS_KEYFILE` ### Environment Variables (LLM) | Variable | Purpose | |----------|---------| | `ANTHROPIC_API_KEY` | Anthropic API key | | `OPENROUTER_API_KEY` | OpenRouter API key | | `PERPLEXITY_API_KEY` | Perplexity API key | | `LLM_PROVIDER` | Override provider at runtime | | `CLAUDE_MODEL` | Override Anthropic model | | `OPENROUTER_MODEL` | Override OpenRouter model | | `PERPLEXITY_MODEL` | Override Perplexity model | --- ## Dependencies | Package | Version | Purpose | |---------|---------|---------| | `hermes_mcp` | ~> 0.1 | MCP protocol server behaviour | | `bandit` | ~> 1.0 | HTTP/HTTPS server (Plug adapter) | | `jason` | ~> 1.4 | JSON encoding/decoding | | `req` | ~> 0.5 | HTTP client (LLM APIs, Pushover) | | `gproc` | ~> 0.9 | Process registry for Notifier | | `file_system` | ~> 1.1 | File system watcher for AutoCompile | | `tzdata` | ~> 1.1 | Timezone database | | `exqlite` | ~> 0.34 | SQLite NIF bindings | | `floki` | ~> 0.38 | HTML parsing and manipulation | | `chronic` | ~> 3.2 | Natural language time parsing | | `argon2_elixir` | ~> 4.0 | Password hashing (Argon2) | | `logger_backends` | ~> 1.0 | Custom logger backends | | `muontrap` | ~> 1.7 | Managed external process execution | | `chumak` | ~> 1.4 | Pure Erlang ZeroMQ implementation | | `erlzmq_dnif` | ~> 4.1 | ZeroMQ NIF bindings | --- ## Mermaid Architecture Diagram ```mermaid graph TD %% ========================================================================= %% SUPERVISION TREE %% ========================================================================= subgraph Supervisor["Mcp.Supervisor (one_for_one)"] direction TB UsersDB["Sqler
:users_db"] CompileLogDB["Sqler
:compile_log_db"] Manager["MCPServer.Manager"] HTTPS["Bandit HTTPS :8443
MCPServer.Router"] AC["AutoCompile"] LS["LogSubscriber"] AL["Alarm"] PO["Pushover"] MCT["MCPClientTracker"] ChatDB["Sqler
:chat_db"] SCT["ServerChatTracker"] SchedDB["Sqler
:scheduler"] A2ADB["Sqler
:a2a_db"] TasksDB["Sqler
:tasks_db"] ZmqReg["Registry
ZmqCounter"] PSReg["Registry
PageScraperRegistry"] PSDB["Sqler
:page_scraper_db"] APP["App"] end %% ========================================================================= %% EXTERNAL ACTORS %% ========================================================================= Browser(("Browser
(User)")) IEx(("IEx
Console")) FS(("Filesystem
Events")) PushAPI(("Pushover
API")) AnthropicAPI(("Anthropic
API")) OpenRouterAPI(("OpenRouter
API")) PerplexityAPI(("Perplexity
API")) PythonProc(("Python
ZeroMQ")) %% ========================================================================= %% HTTP REQUEST FLOW %% ========================================================================= Browser -- "HTTPS :8443" --> HTTPS subgraph RouterLogic["MCPServer.Router (Plug.Router)"] direction TB Auth["require_authentication
cookie decrypt"] Routes["Routes:
POST /login · POST /message
GET /sse · GET /chat · POST /chat
GET /calculator · POST /calculator
GET /greeting · GET /counter · POST /counter
GET /blog · POST /blog · GET /blogs
GET /tasks · POST /tasks
POST /a2a"] GetUser["get_user/1
loads user from DB"] end HTTPS --> Auth Auth --> Routes Routes --> GetUser %% ========================================================================= %% MCP SERVER LAYER %% ========================================================================= subgraph MCPLayer["MCP Server Layer"] direction TB Manager MCP["MCPServer
(Hermes.Server)"] end Routes -- "tools/call · tools/list
resources/list · resources/read" --> Manager Manager -- "load_user/1" --> UserMod Manager -- "handle_tool_call/3
handle_resource_read/2" --> MCP %% ========================================================================= %% MCP CLIENT LAYER %% ========================================================================= subgraph ClientLayer["MCP Client Layer"] direction TB MCT Client["MCPClient
(per-user)"] end IEx -- "Example.run_demo/0" --> MCT MCT -- "get_or_start(user_id)" --> Client Client -- "call_tool/3 · list_tools/1" --> Manager %% ========================================================================= %% CHAT LAYER %% ========================================================================= subgraph ChatLayer["Chat Layer (LLM Integration)"] direction TB SCT SC["ServerChat
(per-user, 30-min timeout)"] subgraph Providers["Pluggable Providers"] direction LR ProvAnthropic["Anthropic"] ProvOpenRouter["OpenRouter"] ProvPerplexity["Perplexity"] end end Routes -- "POST /chat · POST /message" --> SCT SCT -- "get_or_start(user_id)" --> SC SC -- "list_tools · call_tool" --> Manager SC -- "call_llm/4" --> Providers ProvAnthropic --> AnthropicAPI ProvOpenRouter --> OpenRouterAPI ProvPerplexity --> PerplexityAPI SC -- "persist history" --> ChatDB %% ========================================================================= %% USER & AUTH %% ========================================================================= subgraph UserAuth["User & Authentication"] direction TB UserMod["User
create · authenticate
sessions · permissions"] Argon["Argon2
password hashing"] end GetUser --> UserMod Routes -- "POST /login" --> UserMod UserMod --> Argon UserMod -- "SQL queries" --> UsersDB %% ========================================================================= %% TASK MANAGEMENT %% ========================================================================= subgraph TaskSystem["Task Management"] direction TB TM["TaskManager
60+ query functions"] TW["TaskWeb
HTML rendering"] end Routes -- "/tasks/*" --> TW TW --> TM TM --> TasksDB %% ========================================================================= %% PAGE SCRAPER %% ========================================================================= subgraph PageScraperSystem["PageScraper (Browser DOM Extraction)"] direction TB PS["PageScraper
URL patterns + DB"] PSS["PageScraperServer
(per user+pattern)"] PSH["Handler
(e.g. GithubPR)"] end Browser -- "WebSocket
page_visit · dom_result" --> WsH["WsHandler"] WsH -- "match_url" --> PS WsH -- "get_or_start" --> PSS PSS -- "on_match · on_extraction" --> PSH PSS -- "{:push_json, dom_command}" --> WsH PSS -- "store_extraction" --> PSDB %% ========================================================================= %% BLOG SYSTEM %% ========================================================================= Routes -- "/blog* · /blogs" --> Blog["Blog
file-based posts"] Blog -- "read/write" --> BlogFiles["asset/blogs/
HTML files"] %% ========================================================================= %% A2A PROTOCOL %% ========================================================================= subgraph A2ALayer["A2A Protocol"] direction TB A2AS["A2AServer"] A2AC["A2AClient"] end Routes -- "POST /a2a
GET /.well-known/agent.json" --> A2AS A2AS -- "call_tool_as" --> Manager A2AS --> A2ADB %% ========================================================================= %% DATABASE LAYER %% ========================================================================= subgraph Database["Database Layer (SQLite via Sqler)"] direction LR UsersDB CompileLogDB ChatDB SchedDB A2ADB TasksDB PSDB AlarmDB["Sqler (alarm)"] AppDB["Sqler (app)"] end %% ========================================================================= %% COMPILATION & LOGGING %% ========================================================================= subgraph CompileFlow["Auto-Compilation"] direction TB AC CL["CompileLog"] end FS -- "file changes" --> AC AC -- "Code.compile_file/1" --> AC AC -- "log compilation" --> CL CL --> CompileLogDB %% ========================================================================= %% LOGGING PIPELINE %% ========================================================================= subgraph LogPipeline["Logging Pipeline"] direction LR Logger["Logger"] LN["LoggerNotifier
(gen_event)"] Notifier["Notifier
(gproc pub/sub)"] LS end Logger --> LN LN --> Notifier Notifier --> LS LS -- "IO.puts ANSI" --> IEx %% ========================================================================= %% ALARM SYSTEM %% ========================================================================= subgraph AlarmSystem["Alarm & Scheduling"] direction TB AL TI["Timer
(Process.send_after)"] PO end AL -- "set_timer" --> TI TI -- "fire alarm" --> AL AL --> AlarmDB PO -- "HTTP POST" --> PushAPI AL -- "scheduled push" --> PO %% ========================================================================= %% ZEROMQ BRIDGE %% ========================================================================= subgraph ZmqLayer["ZeroMQ Bridge"] direction TB ZmqReg ZMQ["ZmqCounter
(per-instance)"] end ZMQ -- "TCP/IPC" --> PythonProc %% ========================================================================= %% APP MODULE %% ========================================================================= APP --> AppDB %% ========================================================================= %% DOCS %% ========================================================================= Routes -- "/docs/*" --> DocsRouter["DocsRouter"] DocsRouter --> DocsFiles["asset/docs/
HTML files"] %% ========================================================================= %% STYLING %% ========================================================================= classDef genserver fill:#4a9eff,stroke:#2d6cb4,color:#fff classDef database fill:#50c878,stroke:#2d8b4e,color:#fff classDef external fill:#ff9f43,stroke:#b36f2f,color:#fff classDef router fill:#a55eea,stroke:#7b3fb5,color:#fff classDef module fill:#778ca3,stroke:#4b6078,color:#fff classDef provider fill:#e056a0,stroke:#a03d74,color:#fff class Manager,Client,MCT,AC,LS,AL,TI,PO,APP,SC,SCT,ZMQ,PSS genserver class UsersDB,CompileLogDB,SchedDB,AlarmDB,AppDB,ChatDB,A2ADB,TasksDB,PSDB database class Browser,IEx,FS,PushAPI,AnthropicAPI,OpenRouterAPI,PerplexityAPI,PythonProc external class HTTPS,Routes,Auth,DocsRouter router class MCP,UserMod,CL,LN,Notifier,Argon,Blog,TM,TW,A2AS,A2AC,PS,PSH module class ProvAnthropic,ProvOpenRouter,ProvPerplexity provider ```