Skip to content

L1c: fula-mcp silent gateway-JWT auto-refresh (refresh-on-401, retry once) #73

Description

@ehsan6sha

Goal

Make a paired MCP connection work indefinitely without re-pairing, by silently auto-refreshing the short-lived scoped gateway JWT when it expires.

Model

The fula-mcp crate holds a long-lived connection refresh token (delivered in the capability bundle, populated by FxFiles in a later phase, L1d). When a gateway op is rejected with an auth error (the short JWT expired), the MCP:

  1. POSTs to pinning-webui {refresh_url} (= /api/mcp/tokens/refresh-connection) with { refresh_token: <token> },
  2. parses the fresh scoped JWT from the response field token (the L1a contract returns { token, jti, expiresAt }),
  3. swaps it into the in-memory bundle, and
  4. retries the operation exactly ONCE.

If the refresh itself 401/403s (the connection was REVOKED, not merely expired) the MCP gives up and surfaces the original error — that is the real disconnect.

Scope (crate crates/fula-mcp)

  • Bundle: optional refresh_token + refresh_url fields (snake_case, defaulted → backward-compatible); derive the refresh URL from storage_api_url if refresh_url absent; validate HTTPS-or-loopback.
  • Swappable JWT: jwt: RwLock<String> + set_jwt.
  • Refresh HTTP helper: refresh_connection_jwt(...) (5s timeout, mirrors the quota pre-check pattern); parse field token; distinguish revoked (401/403) vs transport vs missing-token. Never log secrets.
  • Refresh-on-auth-error retry in the store/read/list/tags ops via one shared with_refresh_retry helper: detect a gateway auth rejection (S3Error.code ∈ {Unauthorized, AccessDenied, InvalidToken} — the gateway's 401/403 auth codes), refresh, rebuild client, retry once.

Invariants

  • Backward-compatible: no refresh_token in the bundle → behavior byte-identical to today (no retry).
  • Single retry only — never loop.
  • Secrets (refresh_token + JWTs) never logged.

Runtime dependency

L1c's runtime needs the L1a gateway endpoint POST /api/mcp/tokens/refresh-connection (pinning-webui) live. The bundle fields are optional/defaulted, so this phase is standalone + testable; deploy ordering: merge/deploy L1a/L1b before L1c is used in anger.

PR targets feat/fula-mcp (not main).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions