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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions