Content
FullADDMAX-mcp
> Multi-agent orchestration MCP server



[]()
[]()
[]()
FullADDMAX-mcp turns a single AI agent into a team. It exposes four battle-tested multi-agent workflows over the [Model Context Protocol](https://modelcontextprotocol.io/) (stdio), so any MCP client (Claude Desktop, Cursor, Trae, Continue.dev, ...) can dispatch sub-agents, run parallel research, shard processing, and collaborate with handoffs out of the box.
<p align="center">
<img src="docs/panel.svg" alt="FullADDMAX-mcp panel — 3 cards: Server / LLM / Agent Tools" width="100%">
</p>
---
## ✨ Features
#### 🛠 Tools
- **4 mega tools · 31 ops** — `admin` (9) · `agent` (7) · `config` (10) · `knowledge` (5)
- **MCP stdio transport** — directly loaded by Claude Desktop / Cursor / Trae / Continue.dev / Codex
#### 🔌 LLM
- **Autodetect priority chain** — `FULLADDMAX_*` → `OPENAI_*` → host injection (Claude / Cursor / Copilot) → local (Ollama / vLLM / LM Studio)
- **OpenAI compatible endpoint** — any `/v1/chat/completions`, no provider binding
- **Zero-config offline stub** — when `FULLADDMAX_AGENT_OFFLINE=1`, 7 agent ops go deterministic Markdown framework, no LLM, no network
#### 🛡 Reliability
- **Timeout + retry + rate limit token bucket** — prevent LLM rate limiting andglobal + per-session)
- **Token usage tracking** — automatic recording of prompt/completion tokens + cost estimation
- **Persistent session context** — Memory + SQLite backend, state can be passed across calls
- **🔍 Configurable logging** — level / format / file / rotation all via CLI flag or env var, zero extra dependencies
#### 📦 Dev
- **Zero extra runtime dependencies** — only depends on `mcp` + `httpx`
- **🎛 One-click dashboard** — `fulladdmax-mcp panel` generates a single-file SVG dashboard (3 core cards, pure SVG no emoji)
- **7 agent workflows** — orchestrator · parallel · map_reduce · swarm + auto_workflow · delegate · hive_run
---
## 🎛 Dashboard
`fulladdmax-mcp` comes with a **pure SVG dashboard** (no emoji / image dependencies), which can be called at any time to view the current server status: version, uptime, LLM configuration (model / base_url / api_key / timeout / retries), and 31 registered op distributions. A single command generates a single-file SVG that can be directly committed to README.
### 3 Core Cards
| Card | Fields | Meaning |
|------|--------|--------|
| **Server** | version · uptime · mcp servers | / version number / up| **LLM model · base_url · api_key · timeout · retries | smart 3-state: real key / host / out-of-the |
| **Agent | total ops: 31 | number of ops for the 4 mega tools (admin 9 / agent 7 / config 10 / knowledge 5) |
### Smart
The `api_key` field, which defaults to **no alarm**:
| Situation | api_key cell | Color | Subtitle |
|------|----------------||
| Configured with a real key | `sk****` (obfuscated White | — |
| Config empty + detected host | `inherited from <host>` | Green | `host-LLM: <host>` |
| Configured with empty + bare running | `(off-the-shelf)` | Gray | `set FULLADDMAX_API_KEY to enable agent ops` |
> When no LLM is configured, the server can still run `knowledge` / `config` / `admin`, and displays a soft "off-the-shelf" label. After configuring the LLM, all 4 mega tools are available (31 ops).
### Usage
```bash
# Generate a single-file SVG
fulladdmax-mcp panel --out docs/panel.svg
# Real-time preview (default refreshes every 5 seconds)
fulladdmax-mcp panel --serve --port 8765 --refresh 5
# → Open http://127.0.0.1:8765/panel
# Static preview page (pure display, zero buttons)
open docs/preview.html # macOS
start docs\preview.html # Windows
```
### 🔍 Configurable Logging
All 5 dimensions go through CLI flags or env vars, with **CLI taking priority**, env vars second, and default values as a fallback.
| Dimension | CLI flag | env var | Default |
|------|----------|---------|------|
| Level | `--log-level` | `FULLADDMAX_LOG_LEVEL` | `INFO` |
| Format | `--log-format text\|json` | `FULLADDMAX_LOG_FORMAT` | `text` |
| File | `--log-file PATH` | `FULLADDMAX_LOG_FILE` | stderr |
| Rotate (bytes) | `--log-rotate-max-bytes N` | `FULLADDMAX_LOG_ROTATE_MAX_BYTES` | 0 (no rotation) |
| Rotate (backups) | `--log-rotate-backups N` | `FULLADDMAX_LOG_ROTATE_BACKUPS` | 3 |
**Common production combinations**:
```bash
# Development: human-readable, open DEBUG
fulladdmax-mcp --log-level DEBUG
# Production: JSON output for ELK / Loki / Datadog parsing
fulladdmax-mcp --log-format json
# Long-term running: write to file + auto-rotation (10MB × 5 backups)
fulladdmax-mcp --log-file /var/log/fulladdmax-mcp.log \
--log-rotate-max-bytes 10485760 \
--log-rotate-backups 5
# Containerized deployment: inject with env vars, Docker/K8s friendly
FULLADDMAX_LOG_FORMAT=jsonMAX_LOG_LEVEL=INFO fulladdmax-mcp
```
**JSON output example** (one record per line, can be directly jq):
```json
{"level":"INFO","logger":"fulladdmax-mcp.llm","message":"LLM configured","timestamp":"2026-28T14:00:42+0800","base_url":"https://api.openai.com/v1","model":"gpt-4o-mini"}
{"level":"WARNING","fulladdmax-mcp.rate_limit","message":"rate","timestamp":"2026-28T14:00:43+0800","current":105,"limit":100}
{"level":"ERROR","logger":"fulladdmax.llm","message":"chat_with_tools reached max_steps","timestamp":"2026-06-28T14:00:44+0800","max_steps":10,"exc_info":"Traceback..."}
```
**Sub-loggers all go through the same handler** (no duplicate output): 12 modulesgetLogger(__name__)` to get loggers (`max_mcp.llm` / `fulladdmax_mcp.dispatcher` / ...), which converge to the root's single handler via `propagate=True`. `configure_logging()` is idempotent, calling it N times still results in a single handler.
**Team development reference**: a complete list of env vars is in [.env.example](.env.example) — copy to `.env` (already gitignored) and the values.5 logging dimensions + 4 LLM configuration schemes + OFFLINE switch + host injection instructions.
**Other modules directly call `init_logging()`**:
```python
# Library usage (most common)
from fulladdmax_mcp.logging_config import, get_logger
log = init_logging() # automatically loads cwd .env
init_logging(env_file="/etc/myapp/env") # explicitly specify
log = init_logging(env_file="none", level="DEBUG") # skip + debug
init_logging(env_fileenv.local", ".env"]) # multiple files (setdefault semantics)
```
Priority: **CLI arg > shell env > .env file > built-in default**. `.env` uses `os.environ.setdefault` injection, never overriding more specific values. See `init_logging()` docstring for details.
### What it looks like
Directly generating SVG with `fulladdcp panel --out docs/panel.svg` (1280×380 pixels, native SVG elements = `rect` / `polygon` /` / `circle`, **no emoji**, **no external resources**):

### Where comes from
The `panel` collects data through-process mega tool calls** — which also serve as a for the mega tool chain:
| Card | mega |
|------|---------------|
Server** | `="ping")`, uptime) **LLM** | `admin(operation="ping")`, base_url_key, timeout, retries) |
| **Agent Tools `admin(operation="_tools")` (4 mega tools × number of |
> Any mega tool errors are collected as `un status + health indication turns red, but **will not** cause the SVG to.
### Smart host LLM detection
If `FULLADDMAX_API_KEY` defaults to empty, the panel will **automatically scan environment variables** to determine if the server is being called by a host AI. Detection rules (any prefix match is considered a hit):
| Host AI | Recognized env prefix |
|---------|-----------------|
| Claude Desktop / Claude Code | `CLAUDE_` / `ANTHROPIC_` / `CLAUDE_CODE` |
| Cursor | `CURSOR_` |
| Codex CLI | `CODEX_` / `OPENAI_CODEX_` |
| Continue.dev | `CONTINUE_` |
| GitHub Copilot | `COPILOT_` / `GITHUB_COPILOT_` |
| Cline | `CLINE_` |
| Aider | `AIDER_` |
| Zed | `ZED_` / `ZED_AGENT_` |
> Trae IDE itself injects `CLAUDE_CODE_*` env vars when spawning an MCP server, so `fulladdmax-mcp panel` running in Trae will directly display "inherited from Claude Desktop" — you don't need to export anything.
---
## 🤖 Agent Workflows
The `agent` mega tool exposes **7 ops**, covering four scenarios: independent subtask parallelism / multi-angle analysis / pipeline processing / autonomous splitting. The the three most commonly used delegate — Heuristic parallelism
When a task contains **3+ independent subtasks** (e.g., "research A, B, C markets"), letting AI proactively call `delegate` is N times faster than running sequentially — the framework will **automatically dispatch** N workers to work simultaneously.
```python
# Heuristic splitting (by punctuation + and/then)
agent(operation="delegate",
params_json='{"task": "research Beijing, Shanghai, Shenzhen markets"}')
# → Internally: 3 workers in parallel (max_parallel=5)
# Explicitly provide subtasks
agent(operation="delegate",
params_json='{"task": "compare 3 databases",
"children": ["Postgres", "MySQL", "SQLite"]}')
# Forced splitting / single worker
agent(operation="delegate",
params_json='{"task": "...", "split": "always"|"never"}')
```
| Parameter | Default | Meaning |
|------|------|------|
| `task` | — | main task (required) |
| `children` | auto-heuristic | predefined subtask list () |
| `split` | `"auto"` | `"auto"` / `"always"` / `"never"` |
| `max_depth` | `2` | recursive depth limit (sub-agents can continue `delegate`) |
| `max_parallel` | `5` | maximum concurrent workers per layer |
**Heuristic delimiters**: CJK `,。、。;。` + EN `,` `.` `?` `!` `;` + ` and ` ` then ` ` & ` + newline.
### Offline stub
Set `FULLADDMAX_AGENT_OFFLINE=1` to make **all 7 agent operations use the deterministic stub path**——no LLM calls, no network, no GPU, pure Python text generation.
```bash
export FULLADDMAX_AGENT_OFFLINE=1
```
| op | Behavior without LLM |
|----|------------|
| `orchestrator_run` | 1 planner framework + N worker frameworks + 1 synthesizer framework |
| `parallel_agents_run` | N worker frameworks (one segment per task) |
| `map_reduce_run` | map stage: N item frameworks + reduce stage: merged list |
| `swarm_run` | initial agent framework + handoff chain template |
| `auto_workflow` | **completely without LLM** (pure heuristic routing) |
| `delegate` | N sub-agent frameworks (heuristic split) |
| `hive_run` | 6 department frameworks × waves + feedback from Justice department |
**Three-state behavior**:
| State | Behavior |
|------|------|
| 1. `FULLADDMAX_AGENT_OFFLINE=1` | **force use stub** (independent of LLM state) |
| 2. No env + no LLM | return lazy-hint text (tutorial-style) |
| 3. LLM configured | use true LLM path |
→ **True "plug-and-play"**: use it out of the box, no LLM required, no configuration needed, no network connection required.
### hive_run — Six Ministries Hive
`delegate` is suitable for "independent subtasks in parallel"; when the task is **"a complex issue that needs to be viewed from N angles simultaneously"**, call `hive_run`:
| Department | Angle |
|------|------|
| **Personnel** | stakeholders, roles, decision points |
| **Revenue** | cost, ROI, budget |
| **Protocol** | compliance, ethics, UX, standards |
| **Defense** | risk, edge cases, extensibility |
| **Justice** | red team, criticism, find weakest assumptions |
| **Engineering** | specific plan, milestones, architecture |
```python
# 6 departments in parallel
agent(operation="hive_run",
params_json='{"task": "design a global payment system"}')
# → wave 1: 6 ministers in parallel → feedback from Justice department
# → wave 2: 6 ministers take feedback and re-output
# → 12 sub-agents complete in < 1 second
# 3 waves / custom departments / budget
agent(operation="hive_run",
params_json='{"task": "...",
"waves": 3,
"departments": ["marketing", "legal", "eng"],
"max_subagents": 500}')
```
**hive_run vs delegate**:
| | `delegate` | `hive_run` |
|---|---|---|
| split method | heuristic split by `,` `,` `and` etc. | fixed N departments, each department views one angle |
| applicable task | independent subtasks (research A, B, C) | one issue with multiple angles (design + risk + cost + ...) |
| feedback loop | no | **yes** (Justice department criticism → other departments in wave 2) |
| recursion depth | `max_depth=2` | `max_depth=null` (default unlimited; configurable) |
| concurrency limit | `max_parallel=5` | **no limit** (all 6 departments run simultaneously) |
| waves limit | N/A | **20 hard limit** (exceeding throws `ValueError`, not silently truncated) |
**Three-layer protection** (by trigger order): `waves ≤ 20` → `max_depth` → `max_subagents = 200`. "No limit" means **design does not prevent AI from autonomous expansion**, but engineering must leave 3 safety nets for abnormal paths——this is responsible "freedom".
---
## 📦 Installation
```bash
# Clone
git clone https://github.com/addxiaoyi/FullADDMAX-mcp.git
cd FullADDMAX-mcp
# Use pip (recommended development mode)
pip install -e ".[dev]"
# Or use uv
uv pip install -e ".[dev]"
```
Optional dependencies `dev` include `pytest` / `pytest-asyncio` / `respx` / `ruff`, required for running tests and linting.
---
## 🔧 Configure LLM
Supports any OpenAI-compatible `/v1/chat/completions` endpoint.
### Method A: Environment variables (recommended for servers)
| Variable | Default | Description |
|------|------|------|
| `FULLADDMAX_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible base URL |
| `FULLADDMAX_API_KEY` | _(empty)_ | API key |
| `FULLADDMAX_MODEL` | `gpt-4o-mini` | model name |
| `FULLADDMAX_TEMPERATURE` | `0.7` | sampling temperature (0-2) |
| `FULLADDMAX_MAX_TOKENS` | `2048` | maximum tokens in a single response |
| `FULLADDMAX_TIMEOUT` | `60` | single request timeout (seconds) |
| `FULLADDMAX_MAX_RETRIES` | `2` | retry count for 5xx/network errors |
Also supports `OPENAI_API_KEY` as a fallback for `FULLADDMAX_API_KEY`.
### Method B: Configure at runtime via the MCP tool
Call `configure_llm(base_url, api_key, model, ...)` once, and all workflows will use this configuration.
---
## 🧩 Client Integration
### ⚡ One-click configuration (recommended)
The `fulladdmax-install` command is automatically included when installing the server. It can detect installed IDEs on your machine and write the correct configuration.
```bash
pip install -e .
# 1) List detected IDEs
fulladdmax-install --list
# 2) Install to Claude Desktop + Cursor + Codex (skips uninstalled ones)
fulladdmax-install \
--base-url https://api.openai.com/v1 \
--api-key sk-... \
--model gpt-4o-mini
# 3) Install to a specific IDE
fulladdmax-install --ide claude --api-key sk-...
# 4) HTTP mode (points to an already started HTTP server)
fulladdmax-install --ide cursor --url http://127.0.0.1:8000/mcp
# 5) Preview without writing files
fulladdmax-install --ide cursor --api-key sk-... --dry-run
# 6) Uninstall
fulladdmax-install --ide cursor --uninstall
```
Supported IDEs: `claude`, `cursor`, `trae`, `continue`, `codex` (comma-separated multiple selection). The `--api-key` parameter can also be omitted, relying on the `FULLADDMAX_API_KEY` environment variable. When uninstalling, `--api-key` etc. can be omitted.
Example output:
```
claude installed C:\Users\l\AppData\Roaming\Claude\claude_desktop_config.json (container_key=mcpServers)
cursor installed C:\Users\l\.cursor\mcp.json (container_key=mcpServers)
codex skipped C:\Users\l\.codex\config.toml (no change)
```
---
### Manual config (optional)
> All commands assume you have replaced `BASE_URL` / `API_KEY` / `MODEL` with your own values; `fulladdmax` is the display name of this server in the configuration file (can be changed to `My-Agents`, `maxmcp`, etc.).
---
### Claude Desktop
Configuration file path:
| OS | Path |
|----|------|
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Linux | `~/.config/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
**macOS / Linux (bash / zsh):**
```bash
CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
[ -f "$CONFIG" ] || CONFIG="$HOME/.config/Claude/claude_desktop_config.json"
mkdir -p "$(dirname "$CONFIG")"
# Use jq to merge into existing config, or create a new one
TMP=$(mktemp)
jq '.mcpServers += {
"fulladdmax": {
"command": "fulladdmax-mcp",
"env": {
"FULLADDMAX_BASE_URL": "https://api.openai.com/v1",
"FULLADDMAX_API_KEY": "sk-...",
"FULLADDMAX_MODEL": "gpt-4o-mini"
}
}
}' "$CONFIG" -o "$TMP" 2>/dev/null || cat > "$TMP" <<'JSON'
{
"mcpServers": {
"fulladdmax": {
"command": "fulladdmax-mcp",
"env": {
"FULLADDMAX_BASE_URL": "https://api.openai.com/v1",
"FULLADDMAX_API_KEY": "sk-...",
"FULLADDMAX_MODEL": "gpt-4o-mini"
}
}
}
}
JSON
mv "$TMP" "$CONFIG"
echo "✅ Claude Desktop config written to $CONFIG"
open -a "Claude" # macOS: automatically restart Claude
```
**Windows (PowerShell 5+):**
```powershell
$config = "$env:APPDATA\Claude\claude_desktop_config.json"
New-Item -ItemType Directory -Force -Path (Split-Path $config) | Out-Null
if (Test-Path $config) {
$cfg = Get-Content $config -Raw | ConvertFrom-Json
} else {
$cfg = [pscustomobject]@{ mcpServers = [pscustomobject]@{} }
}
$cfg.mcpServers | Add-Member -NotePropertyName fulladdmax -NotePropertyValue ([pscustomobject]@{
command = "fulladdmax-mcp"
env = [pscustomobject]@{
FULLADDMAX_BASE_URL = "https://api.openai.com/v1"
FULLADDMAX_API_KEY = "sk-..."
FULLADDMAX_MODEL = "gpt-4o-mini"
}
}) -Force
$cfg | ConvertTo-Json -Depth 10 | Set-Content $config -Encoding UTF8
Write-Host "✅ Claude Desktop config written to $config"
# Start-Process "Claude" # if you want to restart automatically
```
> **HTTP mode**: if the server is started remotely (`fulladdmax-mcp --transport streamable-http`), replace `command`+`env` with `"url": "http://host:port/mcp"`, and the remote server must have been configured with the `configure_llm` tool.
---
### Cursor
Configuration file path:
| OS | Path |
|----|------|
| All platforms | `~/.cursor/mcp.json` |
**macOS / Linux:**
```bash
CONFIG="$HOME/.cursor/mcp.json"
mkdir -p "$(dirname "$CONFIG")"
cat > "$CONFIG" <<'JSON'
{
"mcpServers": {
"fulladdmax": {
"command": "fulladdmax-mcp",
"env": {
"FULLADDMAX_BASE_URL": "https://api.openai.com/v1",
"FULLADDMAX_API_KEY": "sk-...",
"FULLADDMAX_MODEL": "gpt-4o-mini"
}
}
}
}
JSON
echo "✅ Cursor MCP config written to $CONFIG"
```
**Windows (PowerShell):**
```powershell
$config = "$env:USERPROFILE\.cursor\mcp.json"
New-Item -ItemType Directory -Force -Path (Split-Path $config) | Out-Null
@'
{
"mcpServers": {
"fulladdmax": {
"command": "fulladdmax-mcp",
"env": {
"FULLADDMAX_BASE_URL": "https://api.openai.com/v1",
"FULLADDMAX_API_KEY": "sk-...",
"FULLADDMAX_MODEL": "gpt-4o-mini"
}
}
}
}
'@ | Set-Content $config -Encoding UTF8
Write-Host "✅ Cursor MCP config written to $config"
```
You can also use Cursor's built-in CLI (v0.40+):
```bash
# Add stdio mode
cursor --add-mcp '{
"name": "fulladdmax",
"command": "fulladdmax-mcp",
"env": {
"FULLADDMAX_BASE_URL": "https://api.openai.com/v1",
"FULLADDMAX_API_KEY": "sk-...",
"FULLADDMAX_MODEL": "gpt-4o-mini"
}
}'
# Or add HTTP mode (requires prior server startup)
cursor --add-mcp '{"name":"fulladdmax","url":"http://127.0.0.1:8000/mcp"}'
```
### Trae
Trae stores MCP configuration in `mcp.json` (in the same format as Cursor), located at:
| OS | Path |
|----|------|
| All platforms | `~/.trae/mcp.json` (some versions may use `~/Library/Application Support/Trae/User/mcp.json`) |
**macOS / Linux:**
```bash
CONFIG="$HOME/.trae/mcp.json"
[ -f "$CONFIG" ] || CONFIG="$HOME/Library/Application Support/Trae/User/mcp.json"
mkdir -p "$(dirname "$CONFIG")"
cat > "$CONFIG" <<'JSON'
{
"mcpServers": {
"fulladdmax": {
"command": "fulladdmax-mcp",
"env": {
"FULLADDMAX_BASE_URL": "https://api.openai.com/v1",
"FULLADDMAX_API_KEY": "sk-...",
"FULLADDMAX_MODEL": "gpt-4o-mini"
}
}
}
}
JSON
echo "✅ Trae MCP config written to $CONFIG"
```
**Windows (PowerShell):**
```powershell
$config = "$env:USERPROFILE\.trae\mcp.json"
New-Item -ItemType Directory -Force -Path (Split-Path $config) | Out-Null
@'
{
"mcpServers": {
"fulladdmax": {
"command": "fulladdmax-mcp",
"env": {
"FULLADDMAX_BASE_URL": "https://api.openai.com/v1",
"FULLADDMAX_API_KEY": "sk-...",
"FULLADDMAX_MODEL": "gpt-4o-mini"
}
}
}
}
'@ | Set-Content $config -Encoding UTF8
Write-Host "✅ Trae MCP config written to $config"
```
Alternatively, you can configure Trae MCP in the Trae UI: Settings → MCP → Add → Paste the above JSON.
---
### Continue.dev
Configuration file: `~/.continue/config.json` (v0.9+ also supports `~/.continue/config.yaml`).
**JSON format:**
```json
{
"experimental": {
"modelContextProtocolServers": [
{
"name": "fulladdmax",
"command": "fulladdmax-mcp",
"env": {
"FULLADDMAX_BASE_URL": "https://api.openai.com/v1",
"FULLADDMAX_API_KEY": "sk-...",
"FULLADDMAX_MODEL": "gpt-4o-mini"
}
}
]
}
}
```
**YAML format (v0.9+):**
```yaml
mcpServers:
- name: fulladdmax
command: fulladdmax-mcp
env:
FULLADDMAX_BASE_URL: https://api.openai.com/v1
FULLADDMAX_API_KEY: sk-...
FULLADDMAX_MODEL: gpt-4o-mini
```
**macOS / Linux one-liner:**
```bash
CONFIG="$HOME/.continue/config.yaml"
mkdir -p "$(dirname "$CONFIG")"
cat > "$CONFIG" <<'YAML'
mcpServers:
- name: fulladdmax
command: fulladdmax-mcp
env:
FULLADDMAX_BASE_URL: https://api.openai.com/v1
FULLADDMAX_API_KEY: sk-...
FULLADDMAX_MODEL: gpt-4o-mini
YAML
echo "✅ Continue config written to $CONFIG"
```
---
### Codex CLI
Codex configuration file: `~/.codex/config.toml` (TOML format).
```toml
[[mcp_servers]]
name = "fulladdmax"
command = "fulladdmax-mcp"
env = { FULLADDMAX_BASE_URL = "https://api.openai.com/v1", FULLADDMAX_API_KEY = "sk-...", FULLADDMAX_MODEL = "gpt-4o-mini" }
```
**macOS / Linux one-liner:**
```bash
CONFIG="$HOME/.codex/config.toml"
mkdir -p "$(dirname "$CONFIG")"
# Append (do not overwrite)
cat >> "$CONFIG" <<'TOML'
[[mcp_servers]]
name = "fulladdmax"
command = "fulladdmax-mcp"
env = { FULLADDMAX_BASE_URL = "https://api.openai.com/v1", FULLADDMAX_API_KEY = "sk-...", FULLADDMAX_MODEL = "gpt-4o-mini" }
TOML
echo "✅ Codex config appended to $CONFIG"
```
**Windows (PowerShell):**
```powershell
$config = "$env:USERPROFILE\.codex\config.toml"
New-Item -ItemType Directory -Force -Path (Split-Path $config) | Out-Null
Add-Content -Path $config -Value @'
[[mcp_servers]]
name = "fulladdmax"
command = "fulladdmax-mcp"
env = { FULLADDMAX_BASE_URL = "https://api.openai.com/v1", FULLADDMAX_API_KEY = "sk-...", FULLADDMAX_MODEL = "gpt-4o-mini" }
'@
Write-Host "✅ Codex config appended to $config"
```
---
### Verify the install
After configuration, **restart your IDE**, and have the model (or yourself) call `ping`:
> "Please call the ping tool of fulladdmax"
The model should return:
```
FullADDMAX-mcp v0.2.0 OK
base_url : https://api.openai.com/v1
model : gpt-4o-mini
api_key : sk-...**** (first 4 characters + ****, complete key not leaked)
timeout : 60.0s
retries : 2
```
If the ping returns `api_key: (unset)`, it means the environment variables were not passed correctly (normal for HTTP mode, but check the `env` block in the configuration file for stdio mode).
---
## 🧰 Function Calling
The `fulladdmax-mcp` agent can call **your registered Python tools** during workflow execution. The LLM initiates the call using the OpenAI-compatible `tools`/`tool_calls` protocol, and the framework handles scheduling, error capture, and termination.
### 1. Register a tool
```python
# your_app.py — execute before fulladdmax-mcp starts
import fulladdmax_mcp.tools as ft
@ft.register_tool
async def get_weather(city: str) -> str:
"""Look up current weather for a city."""
return f"Weather in {city}: 72°F, sunny"
@ft.register_tool
async def search_docs(query: str, top_k: int = 5) -> str:
"""Search the local documentation index."""
return f"<{top_k} results for '{query}'>"
```
Alternatively, use `register_tool(fn, name=..., description=..., parameters=...)` to provide an OpenAI-style JSON schema.
### 2. Expose the tool to the agent
Each workflow tool accepts `tools: list[str] | None` as a parameter:
| Value | Meaning |
|----|------|
| `None` (default) | All registered tools (automatically exclude the orchestrator itself) |
| `["get_weather"]` | Only tools in the whitelist |
| `[]` | Disable function-calling (keep in pure conversation mode) |
> When calling MCP, the parameter name is `tools`, and the type is `string[]` (each element is a tool name).
### 3. Call the tool in the MCP client
Let the model (Cursor / Claude Desktop / Trae):
> "Use the `orchestrator_run` of fulladdmax with `tools=["get_weather"]`"
The model will:
1. Call `orchestrator_run(task="...", tools=["get_weather"])`
2. The planner breaks down the task into 3 sub-tasks
3. Each worker sees the tool list and can call `get_weather(city=...)` to get real data
4. The synthesizer summarizes the final answer
### 4. Self-recursion protection
To prevent LLM from recursively calling `orchestrator_run` in workers (causing infinite recursion), the framework excludes the following 6 tool names by default:
```
ping, configure_llm, orchestrator_run, parallel_agents_run,
map_reduce_run, swarm_run
```
This whitelist is defined in `fulladdmax_mcp.tools.DEFAULT_EXCLUDE`.
### 5. View available tools in the MCP client
Call `list_agent_tools`:
```
- get_weather — Look up current weather for a city.
- search_docs — Search the local documentation index.
OpenAI specs (excluded: ...):
```json
[
{"type": "function", "function": {"name": "get_weather", "description": "...", "parameters": {...}}},
...
]
```
### 6. Protocol details
Execution flow of `LLMClient.chat_with_tools`:
```
loop (max 6 steps):
1. POST /v1/chat/completions with messages + tools + tool_choice=auto
2. If response.message.tool_calls is not empty:
a. Append the assistant message (with tool_calls) to the conversation
b. Call the executor for each tool call
c. Append the result as a role=tool message to the conversation
3. If response.message has no tool_calls (or max_steps reached) → exit
```
Error handling: any exceptions thrown by the executor are caught and returned to the LLM in the format `"ERROR: ExceptionName: msg"` (without interrupting the workflow).
---
## 🛠️ Tool Reference
> ## ⚠️ BREAKING CHANGE in v0.6.0
>
> v0.6.0 integrated 28 individual tools into **4 mega tools**:
> - The client sees only 4 tool names (LLM prompts have a shorter tool list, from 28 → 4)
> - Each mega tool accepts `operation` + `params_json` + `session_id` as parameters
> - All business parameters are passed in the `params_json` JSON string
>
> **Complete replacement** — the old 28 tool names have been removed (no deprecated aliases). Please migrate using the migration guide at the end.
### 4 mega tools
| Mega tool | Purpose | Number of operations |
|-----------|------|----------------|
| `agent` | Multi-agent workflows (orchestrator / parallel / map_reduce / swarm) | 4 |
| `knowledge` | Obsidian vault bidirectional read/write | 5 |
| `config` | Runtime configuration & registry changes (write operations) | 10 |
| `admin` | Read-only / status queries | 9 |
| **Total** | | **28** |
### Calling pattern
Each mega tool has the same top-level parameters:
```python
# Calling format
agent(
operation: str, # Business method name (see below)
params_json: str = "", # Business parameters (JSON string, {} = none)
session_id: str = "", # Top-level session ID (for context / rate limit / usage isolation)
) -> str # Markdown / JSON report
```
### 28 operation list
#### `agent(operation, params_json, session_id="")` — Multi-agent workflows
| operation | params | Purpose |
|-----------|--------|------|
| `orchestrator_run` | `task` / `num_workers?` / `timeout?` / `tools?` | Orchestrator-Workers: planner breaks down tasks → N workers in parallel → synthesizer summarizes |
| `parallel_agents_run` | `tasks` / `max_concurrent?` / `timeout?` / `tools?` | Parallel sub-agents: up to 10 concurrent, each failure recorded separately without interrupting the overall process |
| `map_reduce_run` | `items` / `map_prompt?` / `reduce_prompt?` / `max_concurrent?` / `timeout?` / `tools?` | Map-Reduce: map stage parallel slices, reduce stage merges |
| `swarm_run` | `initial_agent` / `task` / `max_handoffs?` / `timeout?` / `tools?` / `agents_json?` | Swarm: 4 built-in agents + custom profiles, JSON handoffs |
#### `knowledge(operation, params_json, session_id="")` — Obsidian vault
| operation | params | Purpose |
|-----------|--------|------|
| `obsidian_list_notes` | `vault_path` / `folder?` / `limit?` | List `.md` notes in the vault |
| `obsidian_read_note` | `vault_path` / `path` | Read a note (frontmatter + body) |
| `obsidian_search_notes` | `vault_path` / `keyword` / `folder?` / `case_sensitive?` / `limit?` | Keyword search |
| `obsidian_write_note` | `vault_path` / `path` / `body` / `frontmatter_json?` / `overwrite?` | Create/overwrite a note |
| `obsidian_append_note` | `vault_path` / `path` / `content` | Append content (preserve frontmatter) |
#### `config(operation, params_json, session_id="")` — Write Operations
| operation | params | Purpose |
|-----------|--------|----------|
| `configure_llm` | `base_url` / `api_key` / `model?` / `temperature?` / `max_tokens?` / `timeout?` / `max_retries?` | Configure LLM endpoint |
| `configure_context_store` | `backend?` / `sqlite_path?` / `ttl_seconds?` | Switch memory/sqlite backend |
| `configure_rate_limit` | `global_rpm?` / `global_tpm?` / `per_session_rpm?` / `per_session_tpm?` / `default_estimated_tokens?` | Configure token bucket rate limiting |
| `configure_pricing_override` | `model` / `prompt_per_million` / `completion_per_million` | Override model pricing |
| `register_swarm_agent` | `name` / `system` / `description?` / `overwrite?` | Register Swarm agent |
| `unregister_swarm_agent` | `name` | Unregister Swarm agent |
| `unregister_agent_tool` | `name` | Unregister agent function-calling tool |
| `reset_rate_limit` | _None_ | Reset rate limiting |
| `reset_usage_stats` | _None_ | Clear usage records |
| `purge_expired_sessions` | `ttl_seconds?` | GC expired sessions |
#### `admin(operation, params_json, session_id="")` — Read-only / Status
| operation | params | Purpose |
|-----------|--------|----------|
| `ping` | _None_ | Health check |
| `list_sessions` | _None_ | List all sessions |
| `get_session` | `session_id` | Read a session's full payload |
| `delete_session` | `session_id` | Delete a session |
| `list_agent_tools` | _None_ | List registered agent tools |
| `list_swarm_agents` | _None_ | List registered Swarm agents |
| `get_rate_limit_status` | _None_ | Rate limiting status snapshot |
| `get_usage_stats` | `session_id?` / `model?` / `since_ts?` | Aggregate token usage + cost |
| `list_usage_records` | `session_id?` / `model?` / `since_ts?` / `limit?` | List recent N usage records |
### Error Format
```
ERROR: bad_op: operation is required / unknown operation 'X'. available: [...]
ERROR: bad_json: line 3 column 5: Expecting property name enclosed in double quotes
ERROR: bad_param: missing required field 'task' for operation 'orchestrator_run'
ERROR: bad_type: field 'num_workers' expected int, got str
ERROR: handler: <ExceptionClass>: <message>
```
All sensitive fields like `api_key` / `token` / `password` / `secret` are automatically masked to the first 4 characters + `****` in error messages.
### Client Call Examples
**Claude Desktop / Cursor / Trae Natural Language Call:**
> "Run an `agent` mega tool with `orchestrator_run`, task is 'Design a REST API for a todo app', split into 3 sub-tasks"
The model will call:
```json
agent(
operation: "orchestrator_run",
params_json: '{"task":"Design a REST API for a todo app","num_workers":3}',
session_id: ""
)
```
**Direct HTTP / JSON-RPC Call:**
```bash
curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{
"name":"agent",
"arguments":{
"operation":"orchestrator_run",
"params_json":"{\"task\":\"hello\"}"
}
}
}'
```
### Internal Workflow Details
> 4 workflows (`agent.orchestrator_run` / `parallel_agents_run` / `map_reduce_run` / `swarm_run`) all accept `tools: list[str] | None` parameter (default `None` = use all registered tools, `[]` = disable function-calling).
> 4 workflows all accept `session_id: str = ""` parameter (default `""` = create new session, pass value = bind to existing session, persist across requests).
> 4 workflows **do not** bypass rate limiting — any LLM calls will acquire token bucket before making HTTP requests. Exceeding limits returns `ERROR: RateLimitError: ...`.
> `agent.swarm_run` also accepts `agents_json: str` parameter (default `""` = use module-level registry, JSON array = override agent set for this call).
> All `knowledge.obsidian_*` operations accept `vault_path: str` parameter (absolute path to vault root), same server can serve multiple vaults in one session.
#### `agent.orchestrator_run({"task": str, "num_workers": int=3, "timeout": float=300})`
1. Planner agent splits `task` into `num_workers` (1-10) independent sub-tasks (JSON array)
2. Worker agent executes each sub-task in parallel
3. Synthesizer agent aggregates all results
#### `agent.parallel_agents_run({"tasks": list[str], "max_concurrent": int=10, "timeout": float=300})`
`tasks` is a list of 1-10 independent prompt strings, executed concurrently; output is a Markdown report, one `## Task #N` section per task.
#### `agent.map_reduce_run({"items": list[str], "map_prompt": str="", "reduce_prompt": str="", "max_concurrent": int=10, "timeout": float=600})`
- `map_prompt` contains placeholder `{item}` → filled with each item
- `reduce_prompt` contains placeholder `{results}` → filled with merged map output
- Leave empty to use default templates
#### `agent.swarm_run({"initial_agent": str, "task": str, "max_handoffs": int=8, "timeout": float=300})`
- `initial_agent` ∈ {`researcher`, `coder`, `critic`, `writer`}
- Each agent must reply with JSON `{"next": <name|DONE>, "message": <string>}`
- Ends when `max_handoffs` or `next="DONE"` is reached
---
## 🌐 Transports
FullADDMAX-mcp switches transports via CLI.
### stdio (Default) — Claude Desktop / Cursor / Trae
```bash
fulladdmax-mcp # Equivalent to --transport stdio
```
stdio mode directly launches MCP client as a subprocess, communicating over stdin/stdout with JSON-RPC. Configure `mcpServers` with `command` set to `fulladdmax-mcp`, `env` for LLM credentials (see below).
### Streamable-HTTP (Recommended for HTTP Clients / Remote Deployment / Multi-Client Sharing)
`streamable-http` is the recommended production-grade HTTP transport for MCP 1.x (POST requests carry JSON-RPC, GET for SSE streaming responses).
```bash
# Start HTTP server (default 127.0.0.1:8000, mount path /mcp)
fulladdmax-mcp --transport streamable-http
# Customize host/port
fulladdmax-mcp --transport http --host 0.0.0.0 --port 9000
# Customize mount path
fulladdmax-mcp --transport streamable-http --mount-path /fulladdmax
```
Once started, HTTP clients connect to:
```
http://127.0.0.1:8000/mcp
```
Also supports `python -m fulladdmax_mcp --transport streamable-http` with the same parameters.
#### Self-Test with `curl`
```bash
# 1) Initialize session
curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
# 2) Call ping tool (with Mcp-Session-Id from previous response)
curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: <session-id>" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ping","arguments":{}}}'
```
#### Claude Desktop via HTTP
```json
{
"mcpServers": {
"fulladdmax-http": {
"url": "http://127.0.0.1:8000/mcp"
}
}
}
```
> Note: In HTTP mode, LLM credentials cannot be injected via `env` field (server process does not read env), use `configure_llm` tool to configure at runtime.
### SSE (Legacy Client Compatibility)
```bash
fulladdmax-mcp --transport sse --port 8000
```
SSE is preserved for legacy MCP clients. New deployments use `streamable-http`.
### Transport Comparison
| Transport | Purpose | Config Field |
|-----------|----------|---------|
| `stdio` (Default) | Local MCP clients (Claude Desktop / Cursor / Trae) | `command` + `env` |
| `streamable-http` | Remote / Multi-client sharing / Reverse proxy | `url` |
| `sse` | Legacy client compatibility | `url` |
### Security Notes
- By default, only binds to `127.0.0.1`, accepting local connections only; FastMCP has built-in DNS-rebinding protection.
- When exposing to the public internet, use a reverse proxy (nginx / Caddy) + TLS and add authentication.
- In HTTP mode, server process does not read `FULLADDMAX_API_KEY` env (credentials configured via `configure_llm`), but request logs may expose task text; consider log redaction.
---
## 🚀 Quickstart
Start MCP server:
```bash
fulladdmax-mcp
```
Configure in Claude Desktop, then model can directly call:
> **Use `orchestrator_run` to split "Design a REST API for a todo app" into 3 sub-tasks and execute in parallel**
The model will automatically call `configure_llm` (if not configured), then `orchestrator_run`, and present results.
### Run Examples Directly
```bash
export FULLADDMAX_BASE_URL=https://api.openai.com/v1
export FULLADDMAX_API_KEY=sk-...
export FULLADDMAX_MODEL=gpt-4o-mini
python examples/orchestrator_demo.py
python examples/parallel_demo.py
python examples/mapreduce_demo.py
python examples/swarm_demo.py
```
---
## 🧪 Development
```bash
# Run tests (using respx to mock httpx, no real LLM key needed)
pytest -q
# Code style
ruff check src tests
# Start MCP Inspector for debugging
mcp dev src/fulladdmax_mcp/server.py
```
---
## 🗺️ Roadmap
- [x] HTTP / Streamable-HTTP transport (v0.2.0)
- [x] Function calling / agent-callable tools (v0.3.0)
- [x] Obsidian vault bidirectional integration (v0.3.0)
- [x] Custom Swarm agent profile registration API (v0.3.0)
- [x] Persistent context (SQLite + Memory, v0.4.0)
- [x] Token usage statistics & cost control (v0.5.0)
- [x] Rate limiting token bucket (global + per-session, v0.5.0)
---
## 🗒️ Obsidian Integration / Vault Integration
5 `obsidian_*` tools provide bidirectional read/write access to [Obsidian](https://obsidian.md/) vaults. `vault_path` is the absolute path to the vault root, passed as a parameter to each tool — same server can serve multiple vaults in one session.
| Tool | Behavior |
|------|------|
| `obsidian_list_notes(vault_path, folder="", limit=500)` | List all `.md` notes in vault (or subdirectory) |
| `obsidian_read_note(vault_path, path)` | Read a note, return frontmatter + body |
| `obsidian_search_notes(vault_path, keyword, folder="", case_sensitive=False, limit=50)` | Search by keyword (default case-insensitive), return path + snippet |
| `obsidian_write_note(vault_path, path, body, frontmatter_json="", overwrite=False)` | Create/overwrite note (frontmatter via JSON string) |
| `obsidian_append_note(vault_path, path, content)` | Append content (preserve frontmatter) |
### Frontmatter Support
Handwritten YAML parser (**zero dependencies**), supporting common Obsidian frontmatter usage:
- Scalars (strings / numbers / booleans / null)
- Block lists `- a / - b` and flow lists `[a, b, "c d"]`
- Nested mappings (2-space indent)
- Block scalars `|` (multi-line strings)
- Single/double quoted strings
- Comments (`#` whole line and inline)
- Unicode (Chinese, etc.)
Unsupported: complex YAML 1.2 features (multiple references, custom tags) — these will report `VaultError`.
### Path Safety
- Absolute paths (`/etc/passwd`, `C:\Windows`) — rejected
- Path traversal (`../foo`, `foo/../../bar`) — rejected
- File size limit 5 MB
### Usage Examples
**MCP Client Invocation (Cursor / Claude Desktop / Trae):**
> "Use fulladdmax's `obsidian_search_notes` to find all notes in `D:\MyVault` that mention 'FullADDMAX'"
> "Use `obsidian_read_note` to read the full content of `D:\MyVault\Projects\roadmap.md`"
> "Append today's work log to `D:\MyVault\Daily\2026-06-26.md`"
**Agent Automatic Usage (function calling):**
`obsidian_*` tools are also registered in the agent tool registry, so workers can **automatically** call them to retrieve notes and then generate answers based on the note content.
For example, let a worker write a competitor analysis report. The framework will let the worker:
1. `obsidian_search_notes("Competitor X")` to find relevant notes
2. `obsidian_read_note(...)` to read each note
3. Integrate the content into the final answer
### Complete Example
```python
from fulladdmax_mcp.obsidian import (
list_notes_tool, read_note_tool, search_notes_tool,
append_note_tool, write_note_tool,
)
vault = "D:/MyVault"
# List all notes
print(list_notes_tool(vault))
# Search
print(search_notes_tool(vault, "FullADDMAX"))
# Read
print(read_note_tool(vault, "Projects/roadmap.md"))
# Write (with frontmatter)
print(write_note_tool(
vault, "Daily/2026-06-26.md",
body="Started obsidian integration today",
frontmatter_json='{"tags": ["work"], "status": "draft"}',
))
# Append
print(append_note_tool(
vault, "Daily/2026-06-26.md",
"## Incremental Notes\n- Ran through list / read / search / write / append 5 tools",
))
```
Output Example:
```
Found 12 note(s):
- Daily/2026-06-25.md
- Daily/2026-06-26.md
- Projects/roadmap.md
- ...
Found 3 match(es) for 'FullADDMAX':
- **Projects/roadmap.md** — …# Project Roadmap ## In Progress - FullADDMAX Integration …
- **Daily/2026-06-25.md** — …## Notes - FullADDMAX v0.3.0 Released …
# Projects/roadmap.md
## Frontmatter
```yaml
status: active
tags: [mcp, agent]
```
## Body
# Project Roadmap
...
```
---
## 🐝 Custom Swarm Agent / Dynamic Agent Profiles
Swarm has 4 built-in agent profiles (`researcher` / `coder` / `critic` / `writer`). `register_swarm_agent` allows MCP clients (or Python scripts) to **dynamically register/override** any number of agents. All subsequent `swarm_run` calls will see the registered agents, persisting across requests (as long as the server is running).
### Three Ways to Provide Agents
| Method | Usage | Persistence |
|------|------|--------|
| **Built-in** (default) | 4 built-in agents are automatically seeded, zero configuration | Yes |
| **`register_swarm_agent`** | Dynamic registration / override; can be called at any time after server startup | Yes (until unregister) |
| **`swarm_run(agents_json=...)`** | Pass a JSON array of agents, only effective for this call | No |
### MCP Client Usage
**1. List current agents:**
> "Invoke fulladdmax's `list_swarm_agents`"
Returns a Markdown report + JSON block (machine-readable):
```
Registered swarm agents (4):
- **coder** — Implements and reviews code; explains trade-offs.
- **critic** — Stress-tests the proposal and surfaces risks.
- **researcher** — Gathers information, surfaces options, proposes hypotheses.
- **writer** — Synthesizes the final user-facing response.
```
**2. Register a custom agent:**
> "Register a custom agent `legal` using `register_swarm_agent`, with system prompt 'You are a legal reviewer. Be precise about liability. Always reply with JSON {next, message}.'"
→ `"registered: legal (total: 5 agent(s))"`
**3. Run swarm with custom agent:**
> "Run `swarm_run` with task 'Write a product launch compliance review report', initial agent `researcher`, handoff to `legal`, then handoff to `writer`"
The model may interact like this:
```
researcher → legal "Here is the product spec, please review for legal issues."
legal → writer "Compliance check: no major issues, see notes."
writer → DONE "Final report: ..."
```
**4. Pass agents temporarily:**
> "Run `swarm_run` with task 'Analyze competitors', initial agent `analyst`, agents JSON is '[{...}, {...}]'"
JSON format:
```json
[
{"name": "analyst", "system": "You are a market analyst. Reply with JSON.", "description": "Market research."},
{"name": "strategist", "system": "You are a strategist. Reply with JSON.", "description": "Strategic synthesis."}
]
```
→ Only sees these two agents for this call; registry remains unchanged.
### Python Usage
```python
from fulladdmax_mcp import swarm
# Register a custom agent (overriding the default researcher)
swarm.register_swarm_agent(
name="researcher",
system=(
"You are a senior research analyst. Cross-check claims across "
"multiple sources. Always reply with JSON {next, message}."
),
description="Senior cross-validated researcher.",
overwrite=True,
)
# List registered agents
for a in swarm.list_swarm_agents():
print(f" {a.name}: {a.description}")
# Run a swarm
import asyncio
out = asyncio.run(swarm.run("researcher", "What is the current state of MCP?"))
print(out)
```
### Security Guarantees
- `register` is protected by `RLock`, ensuring multi-threaded concurrency safety
- Duplicate names raise an error ( `SwarmAgentAlreadyExistsError` ) by default; must use `overwrite=True` to override
- Empty names or system prompts are rejected
- Built-in agents can be deleted ( `unregister_swarm_agent("writer")` ) — unless the process is restarted, they will not be automatically recovered
- `swarm_run` throws `EmptyInputError` immediately if `initial_agent` is not in the agent set
---
## 💾 Persistent Context / Persistent Session State
Workflows (orchestrator / parallel / map_reduce / swarm) write intermediate results to a **module-level** context store during execution (planner subtasks, worker outputs, handoff chains, etc.), which subsequent steps read from. **By default**, it uses in-process memory, which is lost when the process restarts. **Persistent backend** uses SQLite — data is stored in a single file, persisting across processes and restarts.
### 5 New MCP Tools
| Tool | Behavior |
|------|------|
| `configure_context_store(backend, sqlite_path, ttl_seconds)` | Switch to `memory` or `sqlite`, set TTL (default 7 days) |
| `list_sessions()` | List all sessions: Markdown table + JSON block |
| `get_session(session_id)` | Read a session's complete payload (JSON) |
| `delete_session(session_id)` | Delete a session (cascadingly delete all keys) |
| `purge_expired_sessions(ttl_seconds=0)` | GC: delete sessions with `last_access` earlier than TTL (default using store's TTL) |
Plus 4 workflow tools that take `session_id: str = ""` parameter (default creates a new session, passing a value = binds to an existing session, persisting across requests).
### Two Backends
| Backend | Persistence | Cross-process | Suitable Scenarios |
|---------|--------|--------|---------|
| `MemoryContextStore` (default) | ❌ | ❌ | Single process, unit tests, temporary runs |
| `SqliteContextStore` | ✅ | ✅ | Long-running servers, cross-restart, want to inspect historical sessions |
### Client Usage
**1. Switch to SQLite:**
> "Invoke `configure_context_store(backend='sqlite', sqlite_path='/tmp/fam-ctx.db')`"
→ `"Configured SqliteContextStore at /tmp/fam-ctx.db (ttl=604800.0s)"`
**2. Run a workflow with a specified session:**
> "Run `orchestrator_run(task='Analyze Q4 sales data', session_id='quarterly-2026q4')`"
The session will automatically write:
- `task`, `subtasks` (planner-decomposed)
- `worker_results` (each worker's output)
- `final` (synthesizer's final answer)
**3. Inspect a session at any time (even after server restart):**
> "Invoke `get_session('quarterly-2026q4')`"
```json
{
"task": "Analyze Q4 sales data",
"subtasks": ["Extract key metrics", "Compare to Q3", "Generate charts"],
"worker_results": [
"Q4 revenue ¥1234 million, up 12% YoY",
"Q3 comparison: Q4 is 8% higher than Q3",
"Chart code: import matplotlib..."
],
"final": "Q4 sales analysis report: ..."
}
```
**4. Continuously work on a session:**
> "Further elaborate on the `final` field of the `quarterly-2026q4` session"
The agent retrieves the session and continues working from there.
**5. Periodic GC:**
> "Invoke `purge_expired_sessions()` to delete sessions that have not been accessed for 30 days"
→ `"purged: 7 session(s)"`
### Python Script Usage
```python
import asyncio
from fulladdmax_mcp import context as ctx
from fulladdmax_mcp import orchestrator
from fulladdmax_mcp.context_store import SqliteContextStore
# Switch to SQLite (persistent across restarts)
ctx.use_sqlite_store("/tmp/fam-ctx.db", ttl_seconds=30 * 86400)
# Direct read/write
ctx.use_sqlite_store # ...
sid = ctx.new_session()
ctx.put("user", "alice")
ctx.put("step", 1)
print(ctx.snapshot()) # {'user': 'alice', 'step': 1}
# Run a workflow, automatically writing to the current session
out = asyncio.run(orchestrator.run("Analyze Q4 data", num_workers=2))
# Directly read store API
store = ctx.store() # SqliteContextStore
print(store.list_sessions())
print(store.snapshot(sid))
# Restart: reopen the same SQL file, data still exists
```
### Data Model
Each session is a row in SQLite `sessions(session_id, created_at, last_access)`, and key/value pairs are stored in `entries(session_id, key, value)` — `value` is a JSON string. WAL mode + foreign key cascade — deleting a session automatically deletes its entries.
### Important Guarantees
- **Configuration switching closes the previous store** (avoiding SQLite file descriptor leaks)
- **TTL checks only consider `last_access`** — each `put` / `merge` automatically bumps; `get` does not bump by default (configurable `touch_on_read=True`)
- **Thread-safe** — MemoryContextStore uses `RLock`, SqliteContextStore uses `check_same_thread=False` + in-process RLock
- **JSON-compatible** — any JSON-serializable value (str / int / list / dict / bool / None). Non-JSON values are automatically converted to strings using `put(key, value, default=str)`
- **Cross-process** — multiple processes are safe with the same SQL file (SQLite built-in lock). **Do not** modify the same session simultaneously across multiple processes.
### Detailed Semantics of Running Workflows with session_id
| `session_id` value | Behavior |
|------|------|
| `""` (default) | Create a new session, session ID visible in MCP tool output (bound to current context module) |
| `"quarterly-2026q4"` | Bind to an existing session; **non-existent sessions are automatically created** (binding is idempotent) |
| `""` but `bind()` is called externally | Use an externally bound session |
`bind()` is idempotent — given a non-existent ID, it creates a new session; given an existing ID, it simply binds to it. This allows clients to skip a separate `create_session` call.
---
## 💰 Token Usage and Rate Limiting / Usage Tracking & Rate Limiting
**Token usage** and **rate limiting** are the last two items on the roadmap, to be completed in v0.5.0. They are **fully automatic** — any LLM call will first acquire a token bucket and then record usage. Clients can also query/configure using 7 new MCP tools.
### 7 New MCP Tools
| Tool | Behavior |
|------|------|
| `configure_rate_limit(global_rpm, global_tpm, per_session_rpm, per_session_tpm, est)` | Configure 4 token bucket limits. 0 = unlimited. `est` = default estimated tokens when client doesn't provide `max_tokens` |
| `reset_rate_limit()` | Reset rate limit to unlimited |
| `get_rate_limit_status()` | Current configuration + token bucket status (remaining tokens) |
| `get_usage_stats(session_id, model, since_ts)` | Aggregate tokens + cost, grouped by model / session |
| `list_usage_records(session_id, model, since_ts, limit)` | List recent N records (in reverse time order) |
| `reset_usage_stats()` | Clear all usage records (retain pricing table) |
| `configure_pricing_override(model, prompt_per_million, completion_per_million)` | Override/add model pricing |
### Two-Level Token Bucket
| Bucket | Effect |
|----|------|
| **global** (rpm + tpm) | Shared by the entire server — Protects upstream providers |
| **per-session** (rpm + tpm) | Each session_id is independent — Prevents a single session from occupying the global budget |
Bucket behavior:
- **Capacity = max(1, limit/10)** — Allows short-term bursts
- **refill = limit/60** (tokens/sec) — Steady-state rate
- **Exceeding the limit immediately raises `RateLimitError`** (server returns `ERROR: ...`) — **No** queuing
- **HTTP requests are not sent** — Rate limiting failure is intercepted at the _request() entrance
### Built-in Pricing Table
| Model | Prompt / 1M | Completion / 1M |
|-------|-------------|-----------------|
| gpt-4o | $2.50 | $10.00 |
| gpt-4o-mini | $0.15 | $0.60 |
| gpt-4-turbo | $10.00 | $30.00 |
| gpt-3.5-turbo | $0.50 | $1.50 |
| o1 | $15.00 | $60.00 |
| o1-mini | $3.00 | $12.00 |
Supports date suffixes (e.g., `gpt-4o-2024-05-13` automatically归 to `gpt-4o`). Unknown model costs are recorded as 0, **do not** block calls.
### Client Usage
**1. Configure rate limiting:**
> "Call `configure_rate_limit(global_rpm=60, global_tpm=120000, per_session_rpm=10, per_session_tpm=20000)`"
→ `"Configured rate limit: global 60r/120000t per-min, per-session 10r/20000t per-min (est=1024)"`
Or use a compact string (CLI / script friendly):
```python
from fulladdmax_mcp import rate_limit
rate_limit.configure_from_string("global=60r/120k|session=10r/20k|est=2048")
```
**2. Run workflow to trigger rate limiting:**
> "Use `orchestrator_run(task='Translate 100 documents')`, session_id='translation'"
If 6 LLM requests are made within 1 second, **the 7th request will return** `ERROR: RateLimitError: global RPM limit 60 reached (retry after 0.5s, scope=global_rpm)`.
**3. Check usage:**
> "Call `get_usage_stats(session_id='translation', since_ts=timestamp of today 0:00)`"
```json
{
"records": 47,
"prompt_tokens": 125430,
"completion_tokens": 38021,
"total_tokens": 163451,
"cost_usd": 0.342118,
"by_model": {
"gpt-4o-mini": {"records": 47, "prompt_tokens": 125430, ..., "cost_usd": 0.342118}
},
"by_session": {
"translation": {"records": 47, ...}
}
}
```
**4. Check rate limiting status:**
> "Call `get_rate_limit_status()`"
Returns the current capacity / refill_per_second / available token count for the 4 buckets.
**5. Change pricing (enterprise contract / local model):**
> "Call `configure_pricing_override(model='my-llama-3', prompt_per_million=0.10, completion_per_million=0.10)`"
→ `"Pricing for 'my-llama-3': $0.1/1M prompt, $0.1/1M completion"`
**6. Periodic reset:**
> "Call `reset_usage_stats()` to clear old records"
### Python Script Usage
```python
import asyncio
from fulladdmax_mcp import rate_limit, usage
from fulladdmax_mcp import orchestrator
# Configure rate limiting
rate_limit.configure(global_rpm=120, global_tpm=200_000, per_session_rpm=30)
# Run workflow (automatically acquire bucket + automatically record usage)
out = asyncio.run(orchestrator.run("Analyze Q4", num_workers=2))
# Directly check store
summary = usage.store().summary()
print(f"total: {summary.total_tokens} tokens, ${summary.cost_usd:.4f}")
# Check by model
for model, s in summary.by_model.items():
print(f" {model}: {s.records} calls, ${s.cost_usd:.4f}")
```
### Important Guarantees
- **Rate limiting intercepts HTTP calls** — Exceeding limits **never** sends requests to LLM providers (saves money + avoids limits)
- **Usage recording failures do not affect LLM responses** — `_record_usage` has internal try/except handling
- **Responses without usage blocks can still work** — Local LLM servers (Ollama, vLLM) do not crash when usage blocks are not returned
- **Pricing table coverage does not re-calculate historical costs** — Existing UsageRecords retain original costs
- **Per-session rate limiting is independent** — Alice's limit being reached does not affect Bob
- **TTL GC** — `evict_idle_sessions()` automatically cleans up idle per-session buckets after 1 hour
### Failure Modes
| Error | Behavior |
|------|------|
| `RateLimitError` (scope=global_rpm) | Exceeding burst requests within 1 second |
| `RateLimitError` (scope=global_tpm) | Cumulative token count exceeds burst |
| `RateLimitError` (scope=per_session_*) | A session's individual limit is exceeded |
| `get_usage_stats` returns 0 records | Usage store is memory default + restarted (memory is not persisted). Use `configure_store` for persistence (see Persistence Context section) |
## 🤝 Comparison with Other Projects
| Project | Positioning | Differences |
|------|------|------|
| [lastmile-ai/mcp-agent](https://github.com/lastmile-ai/mcp-agent) | General MCP Agent framework | FullADDMAX-mcp focuses on orchestration, lighter, stateless, pure tool |
| [Ask149/orchestrator](https://github.com/Ask149/orchestrator) | Multi-agent parallel CLI | FullADDMAX-mcp is an MCP server form, loaded by any LLM client |
| [task-orchestrator](https://github.com/) (a29601) | Persistent workflow graph | FullADDMAX-mcp uses OpenAI-compatible LLM, no dedicated backend required |
| PraisonAI / BeeAI / fast-agent | Complete Agent platform | FullADDMAX-mcp is a single-file MCP server |
## 📄 License
MIT © addxiaoyi
## 🔄 Migration Guide (v0.5 → v0.6)
v0.6.0 integrates 28 independent tools into 4 mega tools (`agent` / `knowledge` / `config` / `admin`). This is a **breaking change** — the old 28 tool names have been completely removed, **no** deprecated aliases.
### 1. Parameter Changes
| Old (v0.5) | New (v0.6) |
|-----------|-----------|
| 28 independent tool names | 4 mega tool names |
| Business parameters as function parameters | Business parameters packaged into `params_json` JSON string |
| `session_id` only on 4 workflow tools | `session_id` on top-level signatures of all 4 mega tools |
### 2. Complete Mapping
| Old Tool (v0.5) | New Mega Tool + Operation (v0.6) |
|----------------|----------------------------------|
| `ping` | `admin(operation="ping", params_json="")` |
| `configure_llm` | `config(operation="configure_llm", params_json='{...}')` |
| `list_agent_tools` | `admin(operation="list_agent_tools", params_json="")` |
| `unregister_agent_tool` | `config(operation="unregister_agent_tool", params_json='{"name": "..."}')` |
| `obsidian_list_notes` | `knowledge(operation="obsidian_list_notes", params_json='{"vault_path": "..."}')` |
| `obsidian_read_note` | `knowledge(operation="obsidian_read_note", params_json='{"vault_path": "...", "path": "..."}')` |
| `obsidian_search_notes` | `knowledge(operation="obsidian_search_notes", params_json='{...}')` |
| `obsidian_write_note` | `knowledge(operation="obsidian_write_note", params_json='{...}')` |
| `obsidian_append_note` | `knowledge(operation="obsidian_append_note", params_json='{...}')` |
| `configure_context_store` | `config(operation="configure_context_store", params_json='{...}')` |
| `list_sessions` | `admin(operation="list_sessions", params_json="")` |
| `get_session` | `admin(operation="get_session", params_json='{"session_id": "..."}')` |
| `delete_session` | `admin(operation="delete_session", params_json='{"session_id": "..."}')` |
| `purge_expired_sessions` | `config(operation="purge_expired_sessions", params_json='{...}')` |
| `configure_rate_limit` | `config(operation="configure_rate_limit", params_json='{...}')` |
| `reset_rate_limit` | `config(operation="reset_rate_limit", params_json="")` |
| `get_rate_limit_status` | `admin(operation="get_rate_limit_status", params_json="")` |
| `get_usage_stats` | `admin(operation="get_usage_stats", params_json='{...}')` |
| `list_usage_records` | `admin(operation="list_usage_records", params_json='{...}')` |
| `reset_usage_stats` | `config(operation="reset_usage_stats", params_json="")` |
| `configure_pricing_override` | `config(operation="configure_pricing_override", params_json='{...}')` |
| `register_swarm_agent` | `config(operation="register_swarm_agent", params_json='{...}')` |
| `unregister_swarm_agent` | `config(operation="unregister_swarm_agent", params_json='{"name": "..."}')` |
| `list_swarm_agents` | `admin(operation="list_swarm_agents", params_json="")` |
| `orchestrator_run` | `agent(operation="orchestrator_run", params_json='{"task": "..."}')` |
| `parallel_agents_run` | `agent(operation="parallel_agents_run", params_json='{"tasks": [...]}')` |
| `map_reduce_run` | `agent(operation="map_reduce_run", params_json='{"items": [...]}')` |
| `swarm_run` | `agent(operation="swarm_run", params_json='{"initial_agent": "...", "task": "..."}')` |
### 3. Migration Example
**v0.5 (Old):**
```python
await mcp_client.call_tool("orchestrator_run", {
"task": "Design REST API for a todo app",
"num_workers": 3,
"tools": ["get_weather"],
})
```
**v0.6 (New):**
```python
await mcp_client.call_tool("agent", {
"operation": "orchestrator_run",
"params_json": json.dumps({
"task": "Design REST API for a todo app",
"num_workers": 3,
"tools": ["get_weather"],
}),
"session_id": "my-session-001",
})
```
**v0.5 (Old):**
```python
await mcp_client.call_tool("configure_llm", {
"base_url": "https://api.openai.com/v1",
"api_key": "sk-...",
"model": "gpt-4o-mini",
})
```
**v0.6 (New):**
```python
await mcp_client.call_tool("config", {
"operation": "configure_llm",
"params_json": json.dumps({
"base_url": "https://api.openai.com/v1",
"api_key": "sk-...",
"model": "gpt-4o-mini",
}),
"session_id": "",
})
```
### 4. Python SDK / White-box Testing
If your code directly imports `from fulladdmax_mcp.server import ping, configure_llm, ...` (white-box testing / internal SDK), **no changes are needed** — v0.6 still exports the 28 internal functions from the `server` module.
```python
# This works in both v0.5 and v0.6
from fulladdmax_mcp.server import ping, configure_llm, orchestrator_run
print(ping())
out = await orchestrator_run("design REST API for todo", num_workers=3)
```
### 5. LLM Prompts / Client Configuration
If your previous LLM prompts directly listed 28 tool names (using prompt engineering), now only list 4:
```
Available tools:
- agent(operation, params_json, session_id)
- knowledge(operation, params_json, session_id)
- config(operation, params_json, session_id)
- admin(operation, params_json, session_id)
```
Each mega tool's docstring will automatically tell LLM its available `operation` and `params_json` format.
### 6. Error Handling
Old: `ERROR: <ExceptionClass>: <msg>` (from handler)
New:
- `ERROR: bad_op: <reason>` (operation error)
- `ERROR: bad_json: <reason>` (JSON parsing error)
- `ERROR: bad_param: <reason>` (missing required/ incorrect choice)
- `ERROR: bad_type: <reason>` (type mismatch)
- `ERROR: handler: <reason>` (internal handler exception)
- All sensitive fields such as `api_key` / `token` / `secret` are **automatically masked** to `first4chars****` in error responses
### 7. One-Click Migration Script (Pseudocode)
```python
# Automatically migrate all old tool calls to mega tool format
def migrate_call(tool_name, args):
area = {
"orchestrator_run": ("agent", "orchestrator_run"),
"parallel_agents_run": ("agent", "parallel_agents_run"),
"map_reduce_run": ("agent", "map_reduce_run"),
"swarm_run": ("agent", "swarm_run"),
"configure_llm": ("config", "configure_llm"),
# ... remaining 24
}[tool_name]
mega_name, op = area
return {
"name": mega_name,
"arguments": {
"operation": op,
"params_json": json.dumps(args),
"session_id": args.pop("session_id", ""),
},
}
```
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