TLDR: A QuickBooks MCP server caches its access token at boot. Access tokens expire in ~an hour. Long sessions die silently. The fix: refresh per call, not per session.
The Setup
I was wiring up a cancer education business's QuickBooks Online books to Claude Code via the Intuit-official MCP server — github.com/intuit/quickbooks-online-mcp-server, 144 tools, full CRUD across 29 entities.
The thing runs as a local stdio process. You configure it with a .env full of QUICKBOOKS_* vars — Client ID, Client Secret, Refresh Token, Realm ID — then add it to Claude Code like any other MCP tool.
Simple enough. But before I even got the client's production keys unlocked, I spotted a design problem in the server itself.
The Wall I Saw Coming
An MCP server is not a script. It's a long-lived process that stays alive for the duration of a Claude Code session — which realistically means hours.
QuickBooks OAuth access tokens live for roughly an hour.
So if the server fetches an access token at startup and caches it for the session… you do the math. Every QBO tool call an hour in starts failing.
Silently, in the worst case. No loud error — just stale credentials, 401s, and a session that looks like it's working until it isn't.
There was a second bug underneath that one too: if QUICKBOOKS_REFRESH_TOKEN was empty or missing from .env, the server would loop indefinitely trying to authorize. No graceful exit. Just a hang.
What I Tried First
Honestly? I looked at whether the Zoom pattern applied here.
I've got a Zoom S2S (Server-to-Server) integration in another project. Same scenario: process-level OAuth, short-lived tokens. But Zoom's S2S gives you no refresh token — you just re-mint the access token on demand. So the right move there is an in-memory cache at ~50 minutes and re-mint when it's near expiry. Expiration-aware, not per-call.
QuickBooks is different. QBO does give you a refresh token. And because the server is a persistent process, the right pattern flips: don't trust the boot-time access token at all. Refresh it on every call.
The key insight: it's not that caching is wrong. It's that your caching strategy has to match both the token lifecycle and the process lifetime. Zoom: short-lived process-equivalent, no refresh → cache aggressively. QBO: hours-long process, refresh token available → refresh per call and stop worrying about it.
The Fix That Worked
PR #41 on the Intuit-official repo. Two changes:
- Per-call token refresh — the server now uses the refresh token to get a fresh access token before each API call, instead of holding onto the boot-time one.
- OAuth loop guard — if the refresh token env var is empty, fail fast with a clear error rather than looping.
Both were small. Neither was glamorous. But the second one in particular is a classic async error-handling trap — an empty credential string that evaluates truthy in the wrong place and sends you into an infinite auth retry.
Why This Matters to Me
The thing I keep running into with MCP servers is that people build them like CLI scripts — run fast, exit, done. But they don't exit. They live for the whole session.
That changes everything about how you handle credentials, retries, and state. A token cache that's GENIUS for a 10-second script is a silent time bomb in a process that's been running for 90 minutes.
Before you ship a tool integration that uses short-lived credentials: ask yourself how long the process lives, not just how long the token does. Those are two different questions, and confusing them is where sessions go quietly dark.