Skip to main content

Self-hosted REST and MCP server for T4L Trainer agent workflows.

Project description

T4L Server

Self-hosted REST and MCP server for exchanging T4L Trainer JSON artifacts between the iPhone app and a coaching agent.

The phone remains the source of truth for accepted training state. The server stores synced context and pending agent results in SQLite so local agents, home-server agents, and private VPS agents can work through the same protocol.

Install

pipx install t4l-server
t4l-server serve --data-dir ~/T4LServerData

The command prints the server URL, MCP URL, and API key. Enter the server URL and API key in the T4L Trainer Settings screen. Pass --log-level DEBUG for verbose request and error logging (logs go to stderr; the API key is only ever printed to stdout on startup).

Agent setup

Agents should read the public instruction repo first:

https://github.com/BigSlikTobi/t4l-agent-instructions

Those instructions explain how to start or verify this server, connect through MCP, wait for fresh phone context, and write app-consumed results safely.

Safety model

  • REST and MCP endpoints require the API key, compared in constant time.
  • Request bodies over 25 MiB are rejected (413) to bound memory use.
  • JSON artifacts are stored in t4l.sqlite (WAL mode; writes use an immediate transaction so concurrent upserts cannot interleave).
  • Image blobs are stored under blobs/.
  • Path traversal and arbitrary file access are rejected.
  • Stored payloads are structurally validated before persisting.

Architecture

Zero runtime dependencies by design — the server is built on the Python standard library (http.server) so it installs and runs anywhere pipx does. Keep it dependency-free.

  • constants.py: single source of truth for artifact kinds, route maps, limits.
  • store.py: SQLite artifact store (WAL, immediate-transaction upserts).
  • server.py: REST handler, auth, body limits, logging.
  • mcp.py: JSON-RPC MCP tools (read context / write results).
  • validation.py: structural payload validation shared by REST + MCP writes.
  • cli.py: serve entrypoint, logging, and the chat-user / chat-agent test clients (see below).

Chat broker

The server also relays a live chat between the athlete (in the app) and the coaching agent, replacing any third-party chat app. The conversation is an append-only message log (its own chat_messages table, keyed by a monotonic seq cursor — distinct from the latest-only artifact store). v1 is message-level over polling; token/word streaming and SSE (GET /v1/chat/stream) are reserved as future enhancements.

App side (REST, requires x-t4l-token):

Method Path Body / query Response
POST /v1/chat/messages {"content": "..."} (optional conversationId) the stored message
GET /v1/chat/messages?since=<seq> {"messages": [...], "latestSeq": <int>}

Agent side (MCP tools over the authenticated POST /mcp):

Tool Arguments structuredContent
get_pending_chat_messages {} {"messages": [...unanswered user turns...]}
write_chat_reply {"content": "...", "inReplyToSeq": <seq>} {"posted": {...assistant turn...}}

Posting a reply atomically marks the answered user turn(s) answered, so get_pending_chat_messages only ever returns work the agent still owes.

Test the round-trip without an app or an LLM

Two CLI simulators exercise the broker end to end in separate terminals:

# Terminal 1 — broker
t4l-server serve --data-dir ~/T4LChatTest --host 127.0.0.1 --port 8787 --api-key testkey

# Terminal 2 — agent simulator (auto-answers pending messages)
t4l-server chat-agent --server-url http://127.0.0.1:8787 --api-key testkey

# Terminal 3 — athlete/app simulator (interactive)
t4l-server chat-user --server-url http://127.0.0.1:8787 --api-key testkey
> How was my long run?
[user #1] How was my long run?
[assistant #2] You said: How was my long run?

chat-agent --reply "<text>" sends a fixed reply instead of echoing; chat-agent --once answers the current backlog and exits; chat-user --message "<text>" sends one message, waits for the reply, and exits.

Browser test page

The server also serves a self-contained chat page at GET /chat for testing in a browser instead of the chat-user CLI. With the broker and chat-agent running (above), open:

http://127.0.0.1:8787/chat?key=testkey

Type a message and the agent's reply appears within ~1–2s. The page is same-origin with the API (no CORS) and sends your API key as x-t4l-token; the ?key= query just pre-fills the key field. It is a debug aid — the real client is the app.

Planning context

Daily planning needs more than the latest day context — it needs what the athlete actually said in chat ("move my long run to Saturday", "knee still sore"). That intent used to live only in the chat_messages log, which the planner never read. Three MCP tools close the gap, and a fourth bundles everything the planner needs into one call.

The broker stays dumb: it relays recent chat verbatim and bundles the latest artifact of each kind — it never classifies a message as a "request". The interpreted fields (explicit athlete requests, open coaching questions) are authored by the agent itself during chat answering and stored as a standing coaching_notes artifact, then relayed back unchanged. This keeps the brain in the swappable agent, not in the pipe.

Tool Arguments structuredContent
get_recent_chat_messages {"limit": <int>} (optional, default 20, max 200) {"messages": [...recent turns, chronological...]}
get_coaching_notes {} the latest coaching_notes payload, or null
write_coaching_notes {"payload": {...}} {"stored": {...artifact summary...}}
get_planning_context {"recentChatLimit": <int>} (optional) one planning_context.v1 object (below)

get_planning_context returns a single object so daily planning needs one call, not eight:

{
  "schema": "planning_context.v1",
  "generatedAt": "2026-05-31T08:00:00+00:00",
  "dayContext": { },
  "recentLogs": [ ],
  "profile": { },
  "activeBlock": { },
  "nextDayPlan": { },
  "fuelGuidance": { },
  "nutritionAnalysis": { },
  "pendingRequests": [ ],
  "coachingNotes": { },
  "recentChat": [ ]
}

Each artifact slot is the latest payload or null; recentLogs is lifted from daily_snapshot.recentLogs. coaching_notes is free-form (the broker validates only that it is an object with an optional string schema); the agent owns its shape. write_coaching_notes replaces the latest notes, so read them first (via get_coaching_notes or get_planning_context.coachingNotes) and merge before writing.

Training plan payloads may use workout items for supersets and circuits. Use type: "superset" for exactly two alternating child exercises and type: "circuit" for three or more child exercises repeated by rounds; do not use circle. Simple flat workouts may continue to use exercises.

Development

python -m venv .venv
. .venv/bin/activate
python -m pip install -e ".[dev]"
ruff format --check .
ruff check .
mypy
pytest
python -m build
twine check dist/*

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

t4l_server-0.5.0.tar.gz (29.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

t4l_server-0.5.0-py3-none-any.whl (26.2 kB view details)

Uploaded Python 3

File details

Details for the file t4l_server-0.5.0.tar.gz.

File metadata

  • Download URL: t4l_server-0.5.0.tar.gz
  • Upload date:
  • Size: 29.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for t4l_server-0.5.0.tar.gz
Algorithm Hash digest
SHA256 eb87b2c09b1785227c0ae748afaf53f8b5e3d5e0cab016ca6b1c1b43c2ef7c9a
MD5 35e575f86719ff4bdb612dcbc5ab8c1e
BLAKE2b-256 07f5459b086045ed6e8cbdbfb48cb68168a868e720f3a0974cb8dd05c98abb18

See more details on using hashes here.

Provenance

The following attestation bundles were made for t4l_server-0.5.0.tar.gz:

Publisher: publish.yml on BigSlikTobi/t4l-server

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file t4l_server-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: t4l_server-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 26.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for t4l_server-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 eb6d8662e44b0f87473f438ce4f828aebcece2e98c140dd95375d04097835266
MD5 62bb7069cc7d446380ea07871fa7c54b
BLAKE2b-256 44d087d669316034e92b82da534a4236c0d64c6cc65d682000077edfcc1940c3

See more details on using hashes here.

Provenance

The following attestation bundles were made for t4l_server-0.5.0-py3-none-any.whl:

Publisher: publish.yml on BigSlikTobi/t4l-server

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page