Skip to content

fix: reconnect closed MCP sessions; fail fast on expired OAuth sign-in (0.9.4) - #16

Merged
mrjasonroy merged 2 commits into
mainfrom
mrjasonroy/geo-agent-geo-3
Oct 22, 2026
Merged

mrjasonroy merged 2 commits into
mainfrom
mrjasonroy/geo-agent-geo-3

Conversation

@mrjasonroy

Copy link
Copy Markdown
Member

Why

On 2026-10-22 the GEO Slack agent (slack-agents-geo, lib 0.9.3) failed two ways while agent-geo itself was healthy:

  1. Firecrawl scrape (MCP over HTTP): every call failed at once with anyio.ClosedResourceError. mcp_http opened its session once at startup and never replaced it after the connection dropped.
  2. A2A to agent-geo: the token refresh returned HTTP 400, then the request blocked 300 s waiting for an OAuth callback and surfaced as TimeoutError on every call.
    • Keycloak's event log says REFRESH_TOKEN_ERROR error="invalid_token" reason="Token is not active".
    • The user first signed in on 2026-08-21, and realm mcp-servers caps offline sessions at 60 days (offlineSessionMaxLifespanEnabled: true). The owner chose to keep that cap.
    • So the fix makes the expiry clean instead of hanging.

What changed

tools/mcp_http.py

  • The MCP connection now runs in its own task, so the transport's anyio task groups are entered and exited by the same task. A connection that ends drops the session.
  • The next call reconnects. Concurrent callers share one reconnect.
  • A call is retried once, and only when sending it failed on the closed stream (ClosedResourceError/BrokenResourceError). Such a request never reached the server.
  • In two cases the session is dropped but the call is not replayed:
    • CONNECTION_CLOSED after the request was sent.
    • Session terminated, meaning the server restarted and answered 404 for our session id.
  • list_tools runs at connect, so the SDK's output-schema cache never sends a request after a tool ran.

oauth/flow.py, oauth/errors.py, callers

  • A refresh refused with invalid_grant/invalid_token now:
    • deletes the dead stored token,
    • starts a background sign-in that posts the user a fresh Authenticate link at once and stores the new token when they finish,
    • raises SignInExpired (a subclass of ReauthRequired, so background pollers keep their existing notice).
  • A2A and mcp_http_oauth map SignInExpired to a sign_in_expired result: "Your sign-in to X has expired. I've just sent you a sign-in link. Please sign in, then ask again." The request does not wait 300 s and no TimeoutError is raised.
  • Other refresh failures (5xx etc.) keep the SDK's behaviour.

Release: version 0.9.4, CHANGELOG [0.9.4], docs/oauth.md, regenerated llms-full.txt.

Testing

  • tests/test_mcp_client.py: send-failure reconnect and single retry, retry at most once, no replay after send, concurrent reconnect, reconnect failure, and connect/close lifecycle in one task.
  • tests/test_oauth_sign_in_expired.py, tests/a2a/test_oauth.py: the refused-refresh paths.
  • Local runs, one process: 221 passed, 2 skipped across mcp, oauth, a2a, tool errors and agent loop. ruff check and ruff format are clean.
  • End to end against a real local FastMCP streamable-HTTP server:
    • Closing the live session's write stream: the next call reconnected, retried and succeeded.
    • Restarting the server: one call failed with Session terminated, and the next call reconnected and succeeded. Before this change, every call after either failure failed until restart.

🤖 Generated with Claude Code

mrjasonroy and others added 2 commits October 21, 2026 20:19
The session was opened once at startup and never replaced, so after the
connection dropped every tool call failed with ClosedResourceError until
restart. Run the connection in its own task, drop dead sessions, reconnect
on the next call, and retry once only when the send itself failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the IdP refuses the stored refresh token (invalid_grant/invalid_token,
e.g. Keycloak's 60-day offline-session cap), the SDK fell through to a full
authorization inside the request and blocked 300 s until TimeoutError.
Delete the dead token, post a sign-in link at once, finish sign-in in the
background, and return a clear sign_in_expired result. Release 0.9.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mrjasonroy
mrjasonroy merged commit 60cedd7 into main Oct 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant