evermcp

v0.1.0
local-first gateway
19 tests · MIT

01 problem

You sign in once. Twenty minutes later the agent stops mid-task and asks for a password. Not because the tool broke — because the OAuth token behind it expired. MCP sessions sit on top of access tokens that expire on a schedule (often within the hour), refresh flows that fail silently, and session IDs a server is free to invalidate at any moment. Every MCP client inherits this, so every MCP-based product inherits it too. The standard fix today is a human retyping credentials.

evermcp is a small local process that takes that job away from the human.

02 how it works

One process sits between your clients and your servers. It owns every credential and every upstream session, so it can fix what the client never sees.

Client connects to evermcp, evermcp connects to MCP servers claude desktop cursor any MCP client one session id, never changes evermcp 30s sweep single-flight refresh vault · AES-256-GCM 127.0.0.1 only tokens rotate re-auth here mcp servers linear · notion · … oauth 2.1 · api keys
The client never sees a 401, a 404, or a login screen — all of that lives on the right side of the diagram, where evermcp handles it.
what happensproven by
A 30-second sweep rotates every OAuth token at 75% of its lifetime — before anything expires.test 07 · sweep refreshes at 75% of TTL, before the token expires
A 401 triggers exactly one refresh and one retry. If it still fails, the error reaches you.test 05 · 401 on a still-valid-looking token triggers one refresh and one retry
A 404 — the server invalidated the session — replays the original initialize and sends the call once more.test 08 · upstream 404 resurrects the session and replays the call once
Refresh is single-flight and shared: every client goes through one process, so parallel sessions can't invalidate each other's refresh tokens.test 06 · parallel requests after expiry share a single refresh

Tool calls that fail server-side — 5xx, timeouts — are surfaced and never replayed; a retried non-idempotent call could do real damage (test 10). Servers that use plain API keys instead of OAuth are supported too: the key is stored encrypted and sent verbatim, nothing to rotate.

03 proof

Run it yourself. This is the actual output, unedited:

$ npm test
# tests 19
# pass 19
# fail 0

$ npm run demo:expiry
2023-11-14 22:13  first call                         ok  refreshes=0 reinits=0
2023-11-15 22:13  24h later, access token expired    ok  refreshes=1 reinits=0
2023-11-16 22:13  48h, upstream session also gone    ok  refreshes=2 reinits=1
2023-11-17 22:13  day 3                              ok  refreshes=3 reinits=1
2023-11-18 22:13  day 4                              ok  refreshes=4 reinits=1
2023-11-19 22:13  day 5                              ok  refreshes=5 reinits=1
2023-11-20 22:13  day 6                              ok  refreshes=6 reinits=1
2023-11-21 22:13  day 7                              ok  refreshes=7 reinits=1
2023-11-22 22:13  refresh token revoked upstream     error -32001: re-authentication required for demo: invalid_grant  refreshes=7 reinits=1

user prompts before the revocation: 0

The 2023 timestamps are the test clock's fixed epoch — the demo simulates a week in about a second. The provider on the right is a local fake that follows the OAuth and MCP specs, not a real server; real-provider testing is the top item under limits.

Each of the 19 tests is a behavior:

  1. 01discovery falls back to default endpoints when the server publishes no metadata
  2. 02full PKCE sign-in: register, authorize, exchange; wrong verifier and reused code are rejected
  3. 03session survives a forced 24h expiry with zero intervention
  4. 04survives a week of daily expiries, rotating the refresh token each time
  5. 05401 on a still-valid-looking token triggers one refresh and one retry
  6. 06parallel requests after expiry share a single refresh
  7. 07sweep refreshes at 75% of TTL, before the token expires
  8. 08upstream 404 resurrects the session and replays the call once
  9. 09revoked refresh token escalates to needs_reauth and stops hammering the server
  10. 10a 500 on a tool call is surfaced and never replayed
  11. 11vault encrypts per record and enforces version checks
  12. 12static api-key credentials are used verbatim and never refreshed
  13. 13a rejected static key surfaces 401 without retrying
  14. 14vault round-trips encrypted records through a file
  15. 15register keeps rotated tokens instead of reloading stale ones from servers.json
  16. 16dashboard is served; /api/state reports live status; refresh rotates the token
  17. 17kill switch blocks the data plane until resumed
  18. 18a stdio client survives two forced 24h expiries and a killed upstream session with zero intervention
  19. 19gateway keeps one stable client session across an upstream kill and enforces the perimeter

04 install

Three commands and one config line. The source isn't public yet, so this starts from having the repository on disk.

cd evermcp
npm install -g .
evermcp

evermcp prints its data-plane address and a dashboard URL with the admin token in the fragment. Everything lives in ~/.evermcp/.

Open the dashboard URL. Press a, enter an alias and the server's MCP URL, and sign in at the provider in the browser window that opens. That is the last time you type that password.

// Claude Desktop / Cursor — the whole configuration:
{ "mcpServers": { "linear": { "command": "evermcp-stdio", "args": ["linear"] } } }
curl -X POST http://127.0.0.1:7421/mcp/linear \
  -H "Authorization: Bearer $(cat ~/.evermcp/token)" \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'

API-key servers skip the sign-in: pick api key in the form, name the header, paste the value.

05 the dashboard

No build step, no dependencies — one HTML file served by the daemon itself.

The evermcp dashboard: one server named demo, live, with access-token countdown, rotation time, request latency, and an event log.
The real dashboard, captured the day this page was built: one server live, its countdowns running, the event log underneath.

Each row shows a state glyph, the access-token countdown, the re-auth countdown, when the token last rotated, and the last request's latency. The glyphs are the same ones the tests use:

statemeaning
livetoken fresh, nothing to do
refresh duerotation imminent, handled automatically
re-auth neededprovider revoked the grant — one login, on purpose
disabledyou pressed x; the data plane returns 503 until you press it again
keyaction
j kselect server
aadd a server
lsign in again
rrotate token now
xdisable / resume
?help

Before you've signed in to anything, the dashboard says: “No servers yet. Press a, enter an alias and the server's MCP URL, then sign in at the provider. The server appears here and stays signed in.” That sentence was written before this page was.

06 limits

07 faq

Why a gateway instead of fixing each client?

Because the fix belongs in one place that all clients already talk to. Getting every MCP client author to implement the same refresh-and-resurrect logic is a coordination problem; a local process with a config line is not.

What happens if the daemon dies?

Clients get connection errors until it restarts — the same experience as a network blip. On restart it reloads encrypted tokens from disk and re-establishes sessions. No re-login. Verified manually across a daemon restart.

Where do the tokens live?

~/.evermcp/vault.json, AES-256-GCM per record, record ID bound as authenticated data, key in a separate file. The dashboard and API never return token values — only expiry times.

Does it replay my tool calls?

Only when it's provably safe: a 401 or 404 means the server rejected the call before processing it. Timeouts and 5xx are never replayed, because a retried non-idempotent call could do real damage.

What does it see?

All traffic between your clients and the servers you registered. It binds to 127.0.0.1, rejects requests carrying an Origin header (DNS-rebinding guard), and both the data plane and the dashboard require tokens.

What if two clients refresh at once?

They don't. One process owns every credential, and a per-credential mutex makes refresh single-flight. That race is exactly what breaks multi-session MCP setups today.