Writing — MacroStack

The website is just a view

MacroStack is the macro tracker I use every day. Every capability is an MCP tool first; the web app is one client among several. Design notes on tool budgets, context isolation, and OAuth for agents.

August 2026 · 7 min read · Gonzalo Pelenur

Every day I log my meals by talking to Claude. “490 lentils” logs 490 grams of lentils. “Add more cheese” re-logs a food from earlier with a new amount. A photo of a nutrition label becomes a new food in the database. At the gym, “8 more” logs a set that inherits the exercise, weight, and unit from the previous one.

The app behind this is MacroStack: a calorie, macro, micronutrient, workout, and body-weight tracker for my household, built in a sprint on Next.js, Convex, and Vercel. It has a perfectly nice website but the website is not the product.

The product is a rule: everything the website can do is also reachable as an MCP tool. The website is a view onto the data, never the only way in. MacroStack's MCP server exposes 43 tools over remote HTTP, and it works from claude.ai, Claude Code, Claude Desktop, and ChatGPT. When your primary interface is a model, a set of design questions shows up that normal product work never asks. These are my notes on them.

A tool list is an interface with a budget

Tool-selection accuracy degrades as the tool list grows — a model choosing between 43 tools is meaningfully sharper than one choosing between 90. So the tool count is managed as an explicit design constraint: new capabilities get folded into arguments of existing tools rather than becoming the Nth entry in the list.

This is the opposite of how API surfaces usually evolve, where adding an endpoint is free and deleting one is a negotiation. For an agent, every tool you add taxes every decision the model makes, including the ones that have nothing to do with it. A tool list is closer to a screen than a directory: everything on it competes for attention.

Context is also what you don't show

MacroStack serves multiple households. Isolation works in two layers.

The usability layer: schemas and instructions are generated per household. The model operating for my household is never told that other profiles exist — not “you may not access them,” just nothing. A model can't leak what it never saw, and it also can't waste a turn asking about it.

The enforcement layer: every request to the database carries a short-lived signed token (an ES256 JWT) scoping it to the household. Asking about someone outside it errors identically to asking about someone who doesn't exist.

The opinion underneath is that what the model sees is a designed artifact — prompt-level hiding is UX, cryptographic scoping is security. You need both, and confusing one for the other is how agent products end up with polite prompts doing a firewall's job.

Compute what a model would re-derive

Ask a model to estimate maintenance calories twice, a week apart, and you'll get 2,550 one week and 2,610 the next — from a body that hasn't changed.

So MacroStack computes maintenance server-side (Mifflin-St Jeor, with an activity multiplier that deliberately covers only life outside the gym — training is added as its own term, because stock TDEE tables double-count it) and instructs the model to quote the figure together with its workings, never to re-derive it. Same idea for body weight: tools return a seven-point trend, not this morning's reading, because this morning's reading is mostly noise.

The rule: anything that must be stable across conversations belongs in code, not in inference. Models are for understanding the request, not for being the source of truth.

Memory that cannot miss

MacroStack's model-facing memory is 40 notes of at most 400 characters, keyed, updated by overwriting the key. There is no retrieval tool and no forget tool. The set is kept small enough that every note is simply appended to the diary, weekly, and workout outputs.

That's a deliberately unfashionable design. Retrieval that can't miss beats retrieval that's clever, at any scale where you can afford it. And a hard cap forces the discipline that actually matters: every fact with a home goes to its home — body weight goes to the weight log, targets go to the targets table. Notes hold only what has no column: decisions, protocols, standing constraints. Memory systems often fail because they were asked to store what should have been schema, not because they can't recall.

Auth is where MCP-first gets real

“Works from four different AI clients” is an auth problem before it's anything else. MacroStack has three doors in: a token-in-path URL for clients that only accept a URL — where a wrong token returns 404, not 401, so the URL doesn't advertise that an MCP server exists at all; a bearer token for clients with proper header support; and a full OAuth 2.1 authorization server — discovery metadata (RFC 8414/9728), dynamic client registration, PKCE — for the claude.ai connector.

The good thing is that the authorization server keeps zero token tables. Client IDs, authorization codes, and access tokens are all HMAC-signed payloads — the server can verify anything it issued without storing any of it.

Guardrails

The domain logic is where designing-for-a-model gets concrete:

All of it exists because the primary user is a model, and a model handed ambiguous tools does the wrong thing confidently. The fix is rarely a better prompt; it's a tool that can't be misused.