# 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
```