Documentation · Self-host guide

From git clone
to first session.

Everything you need to run your own tutor on a small VPS, plug it into your favorite AI assistant, and keep progression, sessions, and learner memory under your control.

v0.4.0MIT licensedGo 1.25SQLite, no CGOgithub.com/ArnaudGuiovanna/tutor-mcpRead the user guide
On this page
Get going

Quick start

The fastest path: clone, build, run, connect. You'll have your tutor reachable from your AI assistant in under ten minutes.

i.
Clone & build
One go build, single binary, no CGO.
ii.
Set the secret
A base64 JWT_SECRET, plus BASE_URL for web clients.
iii.
Expose HTTPS
Caddy, Nginx, or Tailscale Funnel, you pick.
iv.
Plug your AI
Add the URL as a custom MCP connector.
Just want to test locally? Skip step iii and use Claude Code with http://localhost:3000/mcp. No domain, no TLS, no public IP needed. For cloud connectors, set BASE_URL to your HTTPS origin.
Requirements

What you need

Tutor MCP is intentionally lean. A 2 GB VPS handles a small classroom; your laptop handles personal use.

Runtime
Go 1.25 + SQLite (pure Go)
OS
Linux (macOS & Windows for dev)
Memory
512 MB min / 2 GB recommended
Disk
2 GB SSD, room for SQLite + backups
Public endpoint
HTTPS required for web AI clients
Capacity
~200 / node single-node SQLite, Postgres scales out
Web AI assistants (Claude.ai, ChatGPT, Le Chat) require a public HTTPS URL with a valid certificate. CLI clients (Claude Code, Cline, Continue) work fine over localhost.
How it works

Architecture in plain English

Tutor MCP sits between the assistant and the learner state. The assistant remains the conversational interface; Tutor MCP is the memory and decision runtime.

1

You talk to your AI

Ask for a goal, a review, a harder challenge, or a session close in natural language.

2

The AI calls MCP tools

It reads learner context, asks for the next activity, records outcomes, and saves session memory.

3

Tutor MCP updates state

The runtime updates mastery, retention, calibration, misconceptions, memory files, and audit traces.

SQLite

Algorithmic state: domains, interactions, mastery, retention, calibration, affect, autonomy, OAuth tokens, scheduler data.

Markdown memory

Narrative state: session summaries, concept notes, stable memory, pending observations, and long-term archives.

The important split: the LLM writes explanations and summaries, but Tutor MCP owns the state transitions and the next-activity decision.
Run the server

Install & build

Two paths to a running binary, pick the one that fits. The pre-built binary is the fastest; building from source is for contributors or anyone who wants to pin to a commit.

Install the latest binary

Single binary, no toolchain. The installer detects Linux or macOS plus amd64 or arm64, downloads the latest release asset, verifies it when SHA256SUMS is published, installs a tutor-mcp launcher, and creates a local instance folder.

By default it creates ~/tutor-mcp/.env, ~/tutor-mcp/data, ~/tutor-mcp/memory, and ~/tutor-mcp/backups. Existing .env files are preserved. runtime.db is created automatically on first start at DB_PATH.

$ curl -fsSL https://tutor-mcp.dev/install.sh | sh
==> Detected platform: linux/amd64
==> Creating config at ~/tutor-mcp/.env
==> Database path: ~/tutor-mcp/data/runtime.db
==> Memory root: ~/tutor-mcp/memory
==> Start with: tutor-mcp

Run tutor-mcp after install; the launcher loads ~/tutor-mcp/.env automatically. For public web clients, edit BASE_URL to your HTTPS origin. Override paths with TUTOR_MCP_HOME=/srv/tutor-mcp, TUTOR_MCP_INSTALL_DIR=/usr/local/bin, or TUTOR_MCP_PORT=3001. Full platform assets and SHA256SUMS remain available on the release page.

Pin the version in production when stability matters. The command above follows latest; use a tagged release URL such as v0.4.0 when you want deliberate upgrades.

Build from source

The whole tree is one Go module; no Docker, no Node, no Python. Useful if you want to contribute or pin to a specific commit.

$ git clone https://github.com/ArnaudGuiovanna/tutor-mcp.git
$ cd tutor-mcp
$ go build -o tutor-mcp
$ mkdir -p data backups memory
$ cat > .env <<EOF
JWT_SECRET=$(openssl rand -base64 32)
BASE_URL=http://localhost:3000
PORT=3000
DB_PATH=./data/runtime.db
BACKUP_DIR=./backups
TUTOR_MCP_MEMORY_ROOT=./memory
EOF
$ set -a
$ . ./.env
$ set +a
$ ./tutor-mcp
[info] tutor-mcp listening on :3000
[info] db opened at ./data/runtime.db

Environment & config

The binary reads its configuration from environment variables. JWT_SECRET is mandatory and must be base64. BASE_URL should be your public origin for web clients.

# .env or systemd EnvironmentFile
JWT_SECRET=$(openssl rand -base64 32)
BASE_URL=https://tutor.your-domain.com
DB_PATH=./data/runtime.db
BACKUP_DIR=./backups
BACKUP_RETENTION_DAYS=14
TUTOR_MCP_MEMORY_ENABLED=on
TUTOR_MCP_MEMORY_ROOT=./memory
TRUSTED_PROXY_CIDRS=127.0.0.1/32
MCP_RATE_LIMIT_PER_MIN=60
MCP_RATE_LIMIT_BURST=60
PORT=3000
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/… # optional

Reverse proxy & HTTPS

Web AI clients require HTTPS. Caddy gives you automatic Let's Encrypt with two lines:

# /etc/caddy/Caddyfile
tutor.your-domain.com {
  reverse_proxy localhost:3000
}

Reload Caddy (caddy reload) and your tutor is live at https://tutor.your-domain.com/mcp. That's the URL you'll paste into your AI provider.

Run as a service

A user systemd unit keeps things running and ties cleanly into the documented backup timer.

$ systemctl --user enable --now tutor-mcp
$ systemctl --user enable --now tutor-mcp-backup.timer
$ journalctl --user -u tutor-mcp -f
Scale out

Postgres & multi-node (optional)

SQLite is the default and handles up to ~200 active learners on a single node. v0.4.0 ships an opt-in Postgres backend that turns Tutor MCP into a stateless service you can run on multiple nodes behind a load balancer.

Single-node on Postgres

Point the binary at a Postgres database with DB_DRIVER=postgres and a DATABASE_URL. The schema is created and migrated on first start, guarded by a Postgres advisory lock so cold starts can't race. A checksum guard rejects boots against a drifted schema.

# .env additions for Postgres
DB_DRIVER=postgres
DATABASE_URL=postgres://tutor:secret@db.example.com:5432/tutor?sslmode=require
DB_MAX_CONNS=20

Multi-node behind a load balancer

For horizontal scale, run several instances against the same Postgres. Three switches make the fleet behave as one logical server:

  • JWT_SECRET identical on every node so OAuth tokens stay valid across the fleet.
  • SCHEDULER_MODE=distributed uses database leasing so each scheduled job fires exactly once, no matter how many nodes are running.
  • RATELIMIT_BACKEND=postgres moves rate limits and login-failure counters into the shared store for fleet-wide throttling.
# identical on every node
DB_DRIVER=postgres
DATABASE_URL=postgres://tutor:secret@db.example.com:5432/tutor?sslmode=require
JWT_SECRET=<base64 — same on every node>
SCHEDULER_MODE=distributed
RATELIMIT_BACKEND=postgres
DB_MAX_CONNS=20
Row-level SELECT … FOR UPDATE on concept_states and SKIP LOCKED on the webhook queue protect against lost updates when multiple nodes write at the same time. Full operator notes live in OPERATIONS.md.
SQLite remains the default and nothing changes for existing installs. Switching to Postgres is a configuration choice, not a migration you're forced into.
Connect a provider

Plug into your AI

Once your server is reachable, registering it inside your assistant takes about a minute. The wording differs between providers; the moves don't.

Claude (claude.ai), Pro / Max / Team / Enterprise

  1. Open SettingsConnectors.
  2. Click the + next to Connectors.
  3. Fill in: Name = Tutor MCP, Server URL = https://your.domain/mcp.
  4. Click Add and complete the OAuth login.
Free tier doesn't expose connectors yet, you'll need a Pro plan minimum.
Claude Code (CLI)

Plug into Claude Code

If you're using Claude Code in your terminal, drop a .mcp.json file in your project root (or ~/.claude/mcp.json globally) and you're connected.

{
  "mcpServers": {
    "tutor-mcp": {
      "type": "http",
      "url": "http://localhost:3000/mcp"
    }
  }
}

Swap localhost:3000 for your.domain if your server isn't local.

Other clients

Local & alternative clients

The MCP protocol is open. Tutor MCP speaks plain HTTP MCP, so any client that supports custom MCP servers works, including those wired to local models.

  • Cline, VS Code extension, supports MCP and local LLMs.
  • Continue, IDE assistant with MCP support.
  • OpenWebUI, self-hosted ChatGPT-style frontend.
  • Custom client, any LLM that can call MCP tools (Ollama-backed, llama.cpp, vLLM…).
The cognitive engine doesn't care which model is on the other end, it only cares that something can call its tools.
Verify

Test your setup

Once your provider is connected, send this prompt in a fresh chat. The assistant should call two MCP tools and reply with a short pedagogical brief.

Turn-key check

Drop this into your chat

« Trigger Tutor MCP. Set me up to learn Go in three weeks. Lecture-led style. »
The assistant calls get_learner_context
The assistant calls get_next_activity (or sets up a domain first)
You see a coaching reply that names a concept and a phase

If any step fails, jump to Troubleshooting below.

Operate

Learner memory

Tutor MCP stores two complementary layers. SQLite keeps algorithmic state: domains, interactions, BKT/FSRS/IRT/Rasch-Elo values, calibration, affect, autonomy, OAuth tokens, and scheduler data. Markdown memory keeps the narrative layer the LLM can read and update.

Sessions

Timestamped summaries with affect, concepts touched, salient exchanges, and implementation intentions.

Concept notes

Current narrative state per concept, updated when a durable observation appears.

Stable memory

Learner preferences and durable facts, promoted only when confirmed or explicitly stated.

Archives

Monthly, quarterly, and annual consolidation written by the connected LLM through MCP.

The main tools are record_session_close, update_learner_memory, read_raw_session, and get_memory_state. The LLM authors the summaries; the server validates scopes and paths, writes files atomically, and exposes recent narrative context inside get_next_activity.

Privacy model: the provider still hosts its own chat transcript. Tutor MCP stores the learning state you ask it to preserve on your server. Back up both DB_PATH and TUTOR_MCP_MEMORY_ROOT.
Operate

Backup & restore

Your algorithmic learner model lives in SQLite, and the narrative learner memory lives under TUTOR_MCP_MEMORY_ROOT. Back up both. Two systemd units handle online SQLite backups; add an off-host copy for the database and the memory directory.

$ systemctl --user enable --now tutor-mcp-backup.timer
$ systemctl --user start tutor-mcp-backup.service

For the Markdown memory directory, use your normal file backup path:

$ rsync -a ./memory/ user@backup-host:/var/backups/tutor-mcp/memory/

To restore from a snapshot:

$ systemctl --user stop tutor-mcp
$ mv ./data/runtime.db ./data/runtime.db.broken-$(date -u +%FT%TZ)
$ rm -f ./data/runtime.db-shm ./data/runtime.db-wal
$ cp ./backups/runtime-2026-05-05T03-30-00Z.db ./data/runtime.db
$ systemctl --user start tutor-mcp
Test your restore quarterly.A backup you've never restored is a backup you don't have.
When things go sideways

Troubleshooting

The assistant never calls the tutor

Check the logs for missing pipeline decision entries:

$journalctl --user -u tutor-mcp -f | grep -E "pipeline decision|interaction recorded"

If no decisions are logged, the LLM isn't calling get_next_activity, re-trigger explicitly with: « Use Tutor MCP get_next_activity. »

OAuth handshake fails

Make sure your domain has a valid TLS certificate. curl -I https://your.domain/mcp should return 200 with no warning. AI providers reject self-signed certs.

Repeated phase fallback (NoFringe)

Empty candidate pool, you haven't defined a domain yet, or the goal is too narrow. Run tutor.init_domain with a goal description and three to five concept names.

Need more help? Open an issue on GitHub or check the full OPERATIONS.md runbook.