Headless agents and token refresh
How to run an unattended agent against the Signal or GPS MCP connector without tripping over token rotation.
The MCP connectors sign people in with OAuth 2.1. That works well in Claude and ChatGPT, which look after the tokens for you. An agent you run yourself has to look after them, and a few design choices make that easy or painful. This page covers the ones that matter.
The rules the connector follows#
| Signal and GPS | |
|---|---|
| Access token | Lasts 1 hour. Sent as Authorization: Bearer on every call. |
| Refresh token | Rotates every time it's used. Each refresh returns a new access token and a new refresh token. |
| Idle limit | A refresh token lasts 30 days without use. Each use restarts the 30 days. |
| Lost response | A retry with the just-rotated refresh token, from the same client within 30 seconds, gets a fresh pair. |
| Reuse | Any other reuse of an old refresh token is treated as theft. GPS revokes every token in the connection. |
| Sessions | The connector is stateless. It sends no session ID, so one access token works for any number of calls and sessions until it expires. |
The mistakes that hurt#
Refreshing on every session. You don't need to. An access token is good for an hour, across as many sessions as you like. Refresh only when it's about to expire, or when a call answers 401.
Keeping tokens in environment variables. A process reads its environment once, at start. After a refresh the new token isn't visible to anything already running, so you end up restarting your gateway to pass it on, and that kills sessions in flight.
Sharing one refresh token between processes. If two processes both refresh, the second one presents a token that has already been rotated. Outside the 30-second window that revokes the connection, and the person has to sign in again.
A setup that works#
Put one small token broker between your agents and the connector. It's the only thing that ever holds the refresh token.
- One owner. The broker stores the refresh token in a file or secrets store it alone writes to.
- Refresh on a timer or on demand. It refreshes a few minutes before the hour is up, under a lock, so only one refresh ever runs at a time.
- Agents ask at call time. Each agent reads the current access token from the broker (a local file, a socket or a local HTTP endpoint) just before a call. Nothing is baked into an environment variable.
- Retry once on
401. If a call answers401, ask the broker for the current token and try again.
With this in place a refresh changes one stored value. No config is rewritten, nothing restarts, and sessions in flight carry on.
// The broker: the only code that touches the refresh token
let tokens = await load(); // { access, refresh, expiresAt }
let refreshing = null;
export async function accessToken() {
if (Date.now() < tokens.expiresAt - 5 * 60_000) return tokens.access;
refreshing ??= refresh().finally(() => { refreshing = null; });
return (await refreshing).access;
}
async function refresh() {
const res = await fetch("https://crm.empiraa.app/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: tokens.refresh, client_id: CLIENT_ID }),
});
const t = await res.json();
tokens = { access: t.access_token, refresh: t.refresh_token, expiresAt: Date.now() + t.expires_in * 1000 };
await save(tokens); // write before anyone uses the new pair
return tokens;
}
For GPS, use https://plan.empiraa.app/oauth/token.
If your agent framework manages MCP sign-in#
Some frameworks sign in to remote MCP servers themselves and keep the tokens in their own store. Use that where you can, instead of passing tokens in through your own config. OpenClaw's openclaw mcp login is one example: see OpenClaw bots.
When an API key is the better tool#
If the job runs unattended and does a set thing, such as posting figures or syncing records, use the REST API. An API key doesn't rotate, and it can be scoped to exactly what the job needs. The connector is the better fit when the agent acts for a person and needs the wider tool set.
Why there's no long-lived token#
The connector doesn't issue tokens that never expire, and it doesn't accept API keys. A connection works as a person, with everything they can see and do in Signal or GPS. A token that never changed would be worth stealing, and a stolen copy would be hard to spot.
Rotation is what catches a copy: the first time both the copy and the original are used, the connection is revoked. With a broker in place, rotation costs you nothing. An agent that refreshes at least once every 30 days stays connected without anyone signing in again.

