Content
<div align="center">
<img src="image/logo_axomind.png" alt="Axomind Logo" width="150"/>
<h1>axomind-mcp</h1>
<p><strong>MCP server (Model Context Protocol) for Axomind — a harmless proxy to the Bot API and User API.</strong></p>
</div>
---
## Principle
The MCP lives on the **consumer side**, not on the Axomind server. It contains no business logic — it makes HTTP POST requests to `bot_api.php` (bot tools) or `index.php` (user tools) and returns the JSON. All security (auth, rate limiting, IP bans, `bots @>` checks) stays on the PHP side.
```
AI (Hermes/Atlas or Ollama daemon)
→ MCP server Python (FastMCP)
→ HTTP POST → bot_api.php / index.php
→ PHP does the work (auth, DB, WS notify)
← JSON response
← MCP tool result → AI
```
The server supports two AI backends:
- **Hermes mode** (`AXOMIND_USE_HERMES=true`): the daemon holds the WS connection and buffers events. Hermes/Atlas (cloud) polls events via the `user_poll_events` MCP tool and responds directly. All 35 tools are available through the MCP stdio server.
- **Ollama mode** (`AXOMIND_USE_HERMES=false`): the daemon processes incoming messages with a local Ollama model (tool-calling). Fully autonomous, no Hermes dependency. 28 tools are available (7 excluded: login, token renewal, status, poll_events, and 3 tree tools — local filesystem scan is not useful on production servers).
## Installation
```bash
uv pip install -e .
```
Dependencies: `mcp` (official SDK), `httpx` (HTTP client), `websockets` (daemon mode only).
## Configuration
Copy `.env.example` to `.env` and fill in the credentials:
```bash
cp .env.example .env
```
Variables:
- `AXOMIND_BASE_URL` — URL to `bot_api.php` (e.g. `http://xx.xx.xx.xx/app/bot_api.php`)
- `AXOMIND_BOT_ID` — Bot ID (from Axomind UI → bot management)
- `AXOMIND_BOT_KEY` — Bot access key
- `AXOMIND_TIMEOUT` — HTTP timeout in seconds (default: 30)
See the [Full configuration](#full-configuration) section for all variables.
## Available tools (35)
### Mindmap (10) — bot API
| Tool | Description | Destructive? |
|---|---|---|
| `list_mindmaps` | List mindmaps where the bot is assigned (metadata only) | No |
| `get_mindmap` | Read a mindmap (metadata + all nodes). ⚠️ Response can exceed 2 MB for 60+ nodes with descriptions | No |
| `get_mindmap_summary` | **Compact summary** — node count, titles, structure, has_description. Context-safe, no descriptions or styles | No |
| `get_node_description` | Read a single node's description by order_index (capped at ~4 KB). Use after `get_mindmap_summary` | No |
| `sync_nodes` | Replace ALL nodes (full JSON, ~25 fields per node). ⚠️ DESTRUCTIVE — sends 1 node → deletes the other 98 | ⚠️ Yes |
| `add_nodes` | **Append nodes** to an existing mindmap (simplified format). Reads existing, appends, syncs | No |
| `replace_mindmap` | **Replace all nodes** (simplified format). Validates hierarchy before sending | ⚠️ Yes (validated) |
| `update_node` | **Update a single node** — all fields supported (title, descriptions, parent, style, positions, free_links). Reads full mindmap, patches one node, syncs back. The algorithm handles the JSON, not the AI | No (safe) |
| `delete_node` | **Delete a node + its subtree**. Cleans free_links pointing to deleted nodes. Root node (parent=0) cannot be deleted. The algorithm handles the JSON, not the AI | No (safe) |
| `update_nodes_style` | **Update style fields** on multiple nodes (color, bold, size_box, etc.). Reads, patches, syncs back | No (safe) |
#### Safe node modification — the algorithm handles the JSON
`update_node` and `delete_node` are the **safe** way to modify a mindmap. They read the full mindmap, apply targeted changes to specific nodes, and sync everything back. Other nodes (including their descriptions) are preserved untouched.
The AI never builds the full node JSON — it passes only the fields to modify, and the algorithm does the rest:
```json
// update_node: rename node 33
{"title": "messenger.md test"}
// update_node: change description (markdown → Quill Delta conversion is automatic)
{"descriptions": "# Module Messenger\n\nThis module handles..."}
// update_node: re-parent with cycle detection
{"parent": 2}
// update_node: change style + propagate to children
{"color": "0xFFFF6F91", "bold": true, "is_write_children": true}
// delete_node: just the order_index, no JSON at all
// delete_node(id_mindmap=100, order_index=33)
```
Validations enforced by the algorithm (not by the AI):
- Self-reference: `parent == order_index` → rejected
- Cycle detection: `new_parent` is a descendant of `order_index` → rejected
- Parent must exist in the mindmap
- Root node (parent=0) cannot be deleted
- `free_links` cannot target self, all targets must exist
- `size_box` must be 0–11
#### Simplified format for `replace_mindmap` / `add_nodes`
The AI provides a compact JSON — the MCP auto-expands ~25 default fields:
```json
[
{"title": "Root", "parent": 0, "color": "0xFFF0BA6D", "size_box": 2, "bold": true},
{"title": "Category A", "parent": 1, "color": "0xFF7A8FF5", "size_box": 1, "line_style": 1},
{"title": "Item 1", "parent": 2},
{"title": "Item 2", "parent": 2, "color": "0xFFFF6F91", "free_links": [3]}
]
```
Fields:
- `title` (required) — node title
- `parent` (required) — order_index of the parent node (0 = root, 1 = first node)
- `color` (optional) — hex color (default: `0xFF7A8FF5`)
- `pos_x`, `pos_y` (optional) — canvas position (default: 0)
- `size_box` (optional) — 0=normal, 1=category, 2=root (default: 0)
- `bold`, `italic`, `underline` (optional) — text style
- `line_type` (optional) — 0=curve, 1=rounded, 2=square
- `line_style` (optional) — 0=solid, 1=dashed
- `stroke_width`, `dot_radius`, `radius`, `border_size`, `label_size` (optional)
- `icon_id` (optional) — icon ID
- `active_bg_colors` (optional) — active background colors
- `descriptions` (optional) — descriptive text (markdown → Quill Delta)
- `free_links` (optional) — list of order_index for free links between nodes
- `spacing_h`, `spacing_v` (optional) — spacing multipliers (0-10)
- `is_write_children` (optional) — propagate style to children (one-shot)
UID and order_index are assigned automatically. `add_nodes` reads the existing mindmap and appends after existing nodes.
### Tree / Directory scanning (3) — local + bot API
These tools scan the local filesystem to build mindmap structures from directory trees. **Available in Hermes mode only** — excluded from the daemon (no source code to scan on production servers).
| Tool | Description | HTTP? |
|---|---|---|
| `tree_scope` | **Compact telemetry** of a directory (title, type, size, hierarchy). Does NOT read file content. Use before injection to get a reference node count | No (local) |
| `inject_directory_to_mindmap` | **One-shot scan + read + inject** — scans the directory, reads `.md`/`.markdown`/`.txt` files, converts to Quill Delta, and syncs everything into the mindmap. Returns a compact summary for validation | Yes (sync_nodes) |
| `tree_to_mindmap` | **Scan → JSON nodes** (simplified format, no file content). Ready for `replace_mindmap` or `add_nodes` | No (local) |
#### Workflow: inject a directory into a mindmap
```
1. tree_scope(root_path, root_title) → reference count (1 root + N dirs + M files)
2. inject_directory_to_mindmap(root_path, root_title, id_mindmap) → scan + read + Quill Delta + sync
3. Compare the returned summary (total_nodes, descriptions_filled, errors) with tree_scope count
4. If they match and errors is empty → injection validated. DONE.
```
- Only `.md`, `.markdown`, `.txt` files are read and converted to Quill Delta
- Files > 500 KB and non-text formats (`.docx`, `.pdf`, images) get nodes with empty descriptions
- Hidden files and VCS directories (`.git`, `node_modules`, `__pycache__`) are skipped automatically
- **Never** call `get_mindmap` to verify an injection — the summary + `tree_scope` count are sufficient
### Messenger (4) — bot API
| Tool | Description |
|---|---|
| `send_message` | Send a message (targeted or broadcast to all conversations) |
| `get_messages` | Read bot messages in a conversation |
| `update_message` | Update a bot message |
| `delete_message` | Delete a bot message |
### Activity / Planning (9) — bot API
#### High-level tools (prefer these)
| Tool | Description |
|---|---|
| `create_assignment` | Create an assignment (single-day or recursive) with human-friendly params (dates, hours, weekday names). Builds the JSON internally |
| `modify_assignment` | Modify an existing assignment group. Server marks tombstone, removes old slots, creates new ones |
| `verify_assignment` | Read an activity and return a telemetry report (groups, slots, consistency checks) |
| `read_planning` | Read all planning slots for a given year (user API). The only tool that returns actual time slot data |
#### Low-level tools (raw JSON)
| Tool | Description |
|---|---|
| `list_activities` | List activities where the bot is assigned |
| `get_activity` | Read a specific activity (full metadata) |
| `add_assignment` | Assign time slots (raw `planning_list` + `recursive_group` JSON) |
| `update_assignment` | Update an assignment group (raw JSON) |
| `delete_assignment` | Delete an assignment group |
### User API (9) — index.php
| Tool | Description |
|---|---|
| `user_login` | Authenticate as Atlas, store session token. Auto-starts WS listener if `AXOMIND_WS_URL` is set |
| `user_renew_token` | Renew the session token before 24h expiration |
| `user_status` | Check current session status (local, no HTTP) |
| `user_get_pending_invitations` | List pending invitations sent to Atlas |
| `user_accept_invitation` | Accept an invitation → creates relationship + 1-to-1 conversation |
| `user_refuse_invitation` | Refuse an invitation |
| `user_get_conversations` | List all conversations Atlas is a participant in |
| `user_send_message` | Send a message in a conversation as a user (not as a bot) |
| `user_poll_events` | Drain pending WebSocket events (messages, invitations, mindmap syncs) |
## Token-efficient reading strategy
The MCP provides a 3-tier reading strategy to keep AI context small:
1. **`list_mindmaps()`** — metadata only (id, title, participants). No nodes.
2. **`get_mindmap_summary(id_mindmap)`** — compact summary: node count, titles, structure, `has_description` flag. No descriptions, no positions, no styles.
3. **`get_node_description(id_mindmap, order_index)`** — read a single node's description (capped at ~4 KB).
The AI should **never** call `get_mindmap` (full) unless it needs to inspect individual node fields before a modification. For understanding structure, use `get_mindmap_summary`. For reading content, use `get_node_description` on specific nodes.
In daemon mode, `get_mindmap` is automatically redirected to `get_mindmap_summary` to protect the Ollama context.
## Daemon mode (autonomous, without Hermes)
The daemon connects Atlas to the Axomind messaging network without requiring Hermes. It supports two backends controlled by `AXOMIND_USE_HERMES`:
- **Hermes mode** (`true`): holds the WS connection, buffers events. Hermes polls via `user_poll_events` and responds. No Ollama needed.
- **Ollama mode** (`false`): processes incoming messages with a local Ollama model (tool-calling). Fully autonomous.
The daemon reads credentials from a `.env` file. The path is resolved in this order:
1. `AXOMIND_ENV_FILE` env var (explicit — production)
2. `~/.env` (dev fallback)
3. `.env` in the CWD (dev fallback)
⚠️ The path to the production `.env` is **never hardcoded** in the source code — set it via `AXOMIND_ENV_FILE` so it is not discoverable from the public repo.
### Setup
1. Create the `.env` file with the `AXOMIND_*` variables (see `.env.example`)
2. Install websockets: `uv pip install --system --break-system-packages websockets`
3. Set the env file path system-wide (see below)
4. Launch: `python -m axomind_mcp.daemon` or `bash scripts/start_daemon.sh`
The daemon will:
- Load the `.env` file (from `AXOMIND_ENV_FILE` or dev fallback)
- Validate all required variables — clear error if any are missing
- Test Ollama service (Ollama mode only) — clear error if unreachable or model missing
- Test user login — clear error with return code if it fails
- Connect to WebSocket and start listening
### Session management (conversational context)
In Ollama mode, the daemon maintains conversational context per conversation. Without this, each incoming message is processed in isolation — the AI has no memory of previous exchanges.
**Lifecycle:**
1. **Session start** — A new message arrives. If no active session exists for this conversation, the cache file is loaded to restore history. If no cache exists, a fresh session starts.
2. **Session active** — Messages accumulate in memory. The Ollama context is built from the session history (sliding window of the last N messages). Tool results are also stored in the session.
3. **Periodic save** — Every N messages, the session is saved to the cache file (crash protection). The full history is always in the cache.
4. **Session closing** — After each response, a timeout timer starts. If no new message arrives before the timeout, the session is closed: the full conversation is saved to cache, then purged from memory.
5. **Session resume** — The next message on the same conversation reloads the cache. The AI recovers the full context from the previous interaction.
The AI is instructed to naturally ask "Is there anything else I can help with?" after each response. If the user does not respond, the session closes silently.
**Cache files:**
- Stored locally on the daemon's filesystem (default: `/tmp/axomind_conv_cache/`)
- One JSON file per conversation: `<conversation_id>.json`
- Contains the full message history (user + assistant + tool results)
- No server interaction — purely local filesystem
- Survives daemon restarts
**Configuration:**
| Variable | Default | Description |
|---|---|---|
| `AXOMIND_CONV_CACHE_DIR` | `/tmp/axomind_conv_cache` | Directory for conversation cache files |
| `AXOMIND_SESSION_TIMEOUT` | `300` | Inactivity timeout before session closing (seconds) |
| `AXOMIND_MAX_SESSION_MESSAGES` | `30` | Max messages sent to Ollama context (sliding window). Cache keeps full history |
| `AXOMIND_PERIODIC_SAVE_INTERVAL` | `5` | Save session to cache every N messages (crash protection) |
### Setting AXOMIND_ENV_FILE on Debian
#### Option A — System-wide (all users, all services)
```bash
echo 'AXOMIND_ENV_FILE="/etc/axomind/.env"' | sudo tee /etc/environment.d/axomind-mcp.conf
```
Reboot or re-login for the variable to take effect. All processes (daemon, systemd, shell) will inherit it.
#### Option B — Single user (shell profile)
```bash
echo 'export AXOMIND_ENV_FILE="/etc/axomind/.env"' >> ~/.bashrc
source ~/.bashrc
```
#### Option C — systemd service only
```ini
# /etc/systemd/system/axomind-mcp.service
[Service]
Environment=AXOMIND_ENV_FILE=/etc/axomind/.env
ExecStart=/usr/bin/python3 -m axomind_mcp.daemon
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now axomind-mcp
```
### Env var priority
OS env vars > `.env` file. systemd `Environment=` or Hermes config.yaml always override .env file values.
## Full configuration
### Bot API (bot_api.php)
| Variable | Required | Description |
|---|---|---|
| `AXOMIND_BASE_URL` | ✅ | URL to `bot_api.php` (e.g. `http://host/app/bot_api.php`) |
| `AXOMIND_BOT_ID` | ✅ | Bot ID (from Axomind UI → bot management) |
| `AXOMIND_BOT_KEY` | ✅ | Bot access key |
| `AXOMIND_TIMEOUT` | ❌ | HTTP timeout in seconds (default: 30) |
### User API (index.php — Atlas messaging)
| Variable | Required | Description |
|---|---|---|
| `AXOMIND_KEY_PASS` | ✅ | Shared key (KEY_PASS) for all user API calls |
| `AXOMIND_USER_EMAIL` | ✅ | Atlas user email |
| `AXOMIND_USER_PASSWORD` | ✅ | Atlas user password |
| `AXOMIND_USER_TYPE_CLIENT` | ✅ | Client type identifier (e.g. `axomind_desktop`) |
| `AXOMIND_ROUTE_PREFIX` | ✅ | Route prefix for user API routes. Found in `app/core/_main/config_server.php` → `SUFIX_ROUTE` constant |
| `AXOMIND_USER_BASE_URL` | ❌ | Override user API base URL (defaults to `BASE_URL` with `bot_api.php` → `index.php`) |
### Daemon mode
| Variable | Required | Description |
|---|---|---|
| `AXOMIND_USE_HERMES` | ❌ | `true` = Hermes handles AI, daemon = WS only. `false` = daemon uses Ollama (default: `false`) |
| `AXOMIND_OLLAMA_URL` | ✅ (Ollama mode) | Ollama API endpoint (must end with `/v1/chat/completions`) |
| `AXOMIND_OLLAMA_MODEL` | ✅ (Ollama mode) | Ollama model tag (exact tag from `ollama list`, include quantization suffix) |
| `AXOMIND_OLLAMA_TIMEOUT` | ❌ | Ollama request timeout in seconds (default: 120) |
| `AXOMIND_WS_URL` | ✅ (daemon) | WebSocket URL for real-time events (e.g. `ws://host:8080/`) |
| `AXOMIND_DAEMON_SYSTEM_PROMPT` | ❌ | Override the default daemon system prompt (Ollama mode only) |
### Session management (Ollama mode only)
| Variable | Default | Description |
|---|---|---|
| `AXOMIND_CONV_CACHE_DIR` | `/tmp/axomind_conv_cache` | Directory for conversation cache files (local filesystem) |
| `AXOMIND_SESSION_TIMEOUT` | `300` | Inactivity timeout before closing a session (seconds) |
| `AXOMIND_MAX_SESSION_MESSAGES` | `30` | Max messages sent to Ollama context (sliding window). Cache keeps full history |
| `AXOMIND_PERIODIC_SAVE_INTERVAL` | `5` | Save session to cache every N messages (crash protection) |
⚠️ `AXOMIND_ROUTE_PREFIX` is **mandatory** — without it, user API calls (login, messaging, invitations) get a 404 because the URL is built as `{USER_BASE_URL}?rt={ROUTE_PREFIX}{route}`. The prefix is the server-side shared secret that prevents unauthorized route access.
## Hermes configuration
In `~/.hermes/config.yaml`:
```yaml
mcp_servers:
axomind:
command: "python3"
args: ["-m", "axomind_mcp.serveur.server"]
env:
# Bot API
AXOMIND_BASE_URL: "http://xx.xx.xx.xx/app/bot_api.php"
AXOMIND_BOT_ID: "<your_bot_id>"
AXOMIND_BOT_KEY: "your_key_access"
# User API
AXOMIND_KEY_PASS: "your_keypass"
AXOMIND_USER_EMAIL: "your_bot_email"
AXOMIND_USER_PASSWORD: "your_bot_password"
AXOMIND_USER_TYPE_CLIENT: "axomind_desktop"
AXOMIND_ROUTE_PREFIX: "<your_route_prefix>"
# Daemon mode
AXOMIND_OLLAMA_URL: "http://your-ollama-host:11434/v1/chat/completions"
AXOMIND_OLLAMA_MODEL: "your_model_tag"
AXOMIND_OLLAMA_TIMEOUT: "120"
AXOMIND_WS_URL: "ws://your-server:8080/"
# Python
PYTHONPATH: "/path/to/axomind-mcp/src"
workdir: "/path/to/axomind-mcp"
```
⚠️ All `env` values must be strings (YAML parses `72` as int → pydantic rejects it).
⚠️ `PYTHONPATH` is required — `workdir` sets the cwd but not the Python import path.
⚠️ `AXOMIND_ROUTE_PREFIX` is required for all user API tools (login, messaging, invitations). Without it, the daemon and user tools get 404 errors.
To add a missing env var to an existing Hermes config:
```bash
hermes config set mcp_servers.axomind.env.AXOMIND_ROUTE_PREFIX "<your_route_prefix>"
```
Restart Hermes → tools are discovered automatically with the `mcp_axomind_` prefix.
## Tests
```bash
PYTHONPATH=src python -m pytest tests/ -v
```
178 tests — mock httpx, no network calls to the Axomind server.
## Architecture
```
src/axomind_mcp/
├── __init__.py
├── _common.py — FastMCP instance, env config, _post() / _post_user() helpers, node defaults
├── _planning.py — 8 tools planning/activity (bot API)
├── _ollama_tools.py — Auto-generated Ollama tool definitions from @mcp.tool() introspection
├── _doctrines.py — Daemon system prompt
├── daemon.py — Autonomous daemon (Hermes or Ollama mode)
├── messaging/ — Messaging tools + session management
│ ├── __init__.py
│ ├── _messenger.py — 4 tools messenger (bot API)
│ ├── _user.py — 9 tools user (user API, WS listener auto-start)
│ ├── session_context.py — SessionContext dataclass (in-memory conversation state)
│ ├── session_cache.py — File-based local conversation cache (backup + restore)
│ └── session_manager.py — Session lifecycle: active sessions, timers, closing, context building
├── serveur/
│ ├── __init__.py
│ ├── server.py — Entry point stdio, registration
│ └── _ws_listener.py — WebSocket listener (hybrid Hermes + daemon)
├── mindmap/
│ ├── __init__.py
│ ├── _mindmap.py — 10 tools mindmap (bot API)
│ ├── node_operations.py — Shared algo: update/delete/patch nodes, cycle detection, style propagation
│ └── config_layout_mindmap.py — Node expansion, validation, auto-positioning
└── tools/
├── __init__.py
├── _file_reader.py — File reading by extension → Quill Delta
├── md_to_quill_delta.py — Markdown → Quill Delta converter
└── _tree.py — 3 tools tree (local + bot API)
```
## Security
- The MCP does not touch the database or contain any business logic
- Credentials come from environment variables (never hardcoded)
- The Axomind server cannot tell it's a MCP — it sees normal bot_api requests
- Tree tools (local filesystem scan) are excluded from the daemon — no attack surface on production
- The `.env` file path is set via `AXOMIND_ENV_FILE` — not discoverable from the public repo
## License
Proprietary — see [LICENSE](LICENSE). Copyright © 2025 VEZZANI Sébastien. All rights reserved.
---
<div align="center">
<a href="https://sebastien-vezzani.xyz/" target="_blank"><img src="https://img.shields.io/badge/Portfolio-3423A6?style=for-the-badge&logo=firefox-browser&logoColor=white" alt="Link to Portfolio"/></a>
</div>
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available