Metadata-Version: 2.4
Name: t4l-server
Version: 0.4.0
Summary: Self-hosted REST and MCP server for T4L Trainer agent workflows.
Project-URL: Homepage, https://github.com/BigSlikTobi/t4l-server
Project-URL: Repository, https://github.com/BigSlikTobi/t4l-server
Project-URL: Issues, https://github.com/BigSlikTobi/t4l-server/issues
Author: T4L Trainer
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Requires-Python: >=3.11
Provides-Extra: dev
Requires-Dist: build>=1.3; extra == 'dev'
Requires-Dist: mypy>=1.18; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.14; extra == 'dev'
Requires-Dist: twine>=6.2; extra == 'dev'
Description-Content-Type: text/markdown

# 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

```bash
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:

```text
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:

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

```text
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:

```json
{
  "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.

## Development

```bash
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/*
```
