cd /news/ai-agents/let-the-model-write-not-compute-less… · home › topics › ai-agents › article
[ARTICLE · art-156209] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Let the model write, not compute: lessons from an MCP coach

A developer built GymRap, a personal strength-coaching system in which a Cloudflare Worker computes all scores and training targets in tested code while ChatGPT connects as a remote MCP server and writes only the coaching text, which the server validates against a schema before storing. The Worker exposes 13 tools behind OAuth 2.1 with PKCE and per-call scope checks, and a cron job guarantees the daily report with a numbers-only fallback after ChatGPT's safety layer blocked report saves in six scheduled runs over three days. The developer's conclusion: "if a step can be refused by a system you do not control, it cannot be the step that makes the job succeed.

by read4 min views2 publishedOct 23, 2026

In GymRap, my personal strength coach, the language model never produces a number. A Cloudflare Worker computes every score and target in tested code; ChatGPT connects to it as a remote MCP server, reads a structured brief and writes only the coaching text, which the server validates against a schema before storing. The server, not the model, guarantees the daily report: when the model's write does not arrive, a numbers-only report still goes out, and an uncertain email is never sent twice.

GymRap reads what my watch and phone record through Health Connect: 23 record types, uploaded by a small Kotlin bridge only for the days that changed. Everything that turns those records into a decision runs on the server: a readiness score against my own 30-day baselines, sleep debt, double-progression training targets and weekly sets per muscle group.

I drew that line first, and every later choice follows from it. A model is good at reading a structured situation and explaining it; it is not a calculator I can test. So the backend makes no LLM API calls at all. The model's job is interpretation, and the repository's 137 backend tests at the published commit cover the part that has to be right.

This also kept the running cost at zero beyond the ChatGPT subscription I already had: the model does its work inside ChatGPT, through tools the server exposes.

The Worker is a remote MCP server with 13 tools. Each tool declares whether it only reads, and every call passes through one guard that checks the OAuth scope the call needs: gymrap:read for reads, gymrap:write for the two save tools. A call without the right scope gets a structured insufficient_scope answer instead of an exception.

Authorization is OAuth 2.1 with PKCE, limited to one owner account. The consent page states what the connecting app may do, and the email recipient is fixed on the server: a connected app can ask for a report to be sent, but it can never choose where it goes. For a tool that can send email on my behalf, that boundary matters more than any prompt.

The model saves its coaching text with save_coach_report (and save_post_workout_report after a gym session). Both take a report key, a hash of the context the text was written from, and the coaching JSON.

Three rules make the save safe:

The tool descriptions say all of this in plain words, because the model reads them. "Idempotent per report_key: repeating the identical call is safe and changes nothing" is an instruction to the model as much as documentation.

The first version assumed the model's save would arrive. It did not always. In six scheduled runs over three days, ChatGPT's safety layer blocked the report save. From inside the chat it looked like the tool had been called; the server's call log, which records each tool call's name, outcome and size but never its arguments, showed that the write never arrived.

I stopped treating the model as the thing that completes the job. The server now guarantees the daily report. A cron job checks every ten minutes and, if no coaching text has arrived by the deadline, sends a numbers-only report: all the measurements and targets, without the commentary. If the coaching text turns up later, the server attaches it to that day's web report instead of sending a second email. The coaching is now one sentence away in chat, and the morning email no longer depends on a write I cannot see.

The lesson I took from it: if a step can be refused by a system you do not control, it cannot be the step that makes the job succeed.

Every emailed report moves through a ledger: generated, sending, then sent; or failed, or uncertain. Only a new, queued or definitely failed report can be claimed for sending, and a send whose lease runs out without an outcome becomes uncertain, which is surfaced to me and never retried automatically.

That is a deliberate trade. A report that does not arrive is an annoyance I will notice. The same report arriving twice teaches me to distrust the system. When I cannot know whether Gmail accepted a message, the system chooses the outcome I can recover from.

GymRap is a single-user system by design, and its readiness score is my own transparent formula, not a medical measure. The patterns above are the part I would carry into any agent that acts for someone.

These links point to the published commit 8f35eba of my GymRap AI Coach repository. The case study describes the whole system.

Originally published on onurerguden.dev.

── more in #ai-agents 4 stories · sorted by recency
── more on @gymrap 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/let-the-model-write-…] indexed:0 read:4min 2026-10-23 · —