Skip to content

Park a server whose credential was rejected instead of retrying it forever - #20

Merged
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/park-unauthorized-mcp-servers
Sep 22, 2026
Merged

M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/park-unauthorized-mcp-servers

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Contributor

What changed and why

An MCP server answering HTTP 401 was penalised on the backoff curve like any other failure. The curve caps at BACKOFF_MAX, so the server was re-dialled every five minutes for the life of the session — 18+ attempts in one reported session, each writing a warning, against an endpoint that had already said no.

Waiting does not turn a 401 into a 200. Only the user supplying a working credential does, and that arrives through the store, not through time.

The supervisor already has a terminal/park path, used for Error::MissingRuntime, whose comment states exactly this reasoning: "a penalty says 'wait, then try again', and there is nothing to wait for". A rejected credential is the same shape, so this routes it into that existing path rather than inventing a second mechanism.

-            Err(error) if error.is_missing_runtime() => {
+            Err(error) if error.is_missing_runtime() || error.is_unauthorized() => {

Error::is_unauthorized() already existed and is already tested (transport/http/test.rs, a_401_becomes_a_typed_unauthorized_error); it simply was never consulted here.

Public API / behavior changes

Behavior: a server whose credential is rejected now emits SupervisorEvent::Parked on the first attempt instead of ReconnectFailed on every attempt forever. It is skipped by subsequent ticks until the user disables and re-enables it.

Public API: None.

Deliberate consequence worth review: a server that answers 401 transiently — say, an expired token its own operator rotates server-side without the user touching anything — will now stay parked until toggled, where before it would eventually reconnect by itself. I judged that the right trade: the reported case (a credential that was never supplied) is the common one, and the toggle is discoverable because the status is visible. If you would rather 401 kept a bounded number of retries before parking, that is a one-line change to make here and I am happy to make it.

What this gets for free

These fall out of the existing park machinery rather than needing new code:

  • One warning per server per session — a_parked_server_is_reported_once_and_then_stays_quiet already covers this.
  • A way back — terminal is already cleared on disable, so toggling after setting a token reconnects; the same gesture that already clears a backoff penalty.
  • Visible state — ServerStatus::Unauthorized already flows to the host.

Validation

Run from crates/tinymcp's workspace root, in the order the repository's CLAUDE.md specifies:

Command Result
cargo fmt --all -- --check clean
cargo test --all-features 863 passed, 0 failed (679 + 150 + 18 + 12 + 4)
cargo clippy --all-targets --all-features -- -D warnings fails — pre-existing, not from this change (see below)

Clippy fails on error: unknown lint: clippy::unused_async_trait_impl. I verified this is inherited rather than mine by reverting both changed files to the pinned commit with git checkout -- and re-running: byte-identical error, same exit 101, with none of my changes present. My toolchain is clippy 0.1.96 (31fca3adb2 2026-06-26), which does not know that lint; a newer one does. Nothing in this PR touches a lint declaration.

Test

a_401_is_terminal_and_earns_no_backoff_penalty, modelled on the existing a_missing_runtime_is_terminal_and_earns_no_backoff_penalty beside it, with a loopback axum route that answers every request 401.

Written before the fix, and it failed on the assertion that names the defect:

assertion `left == right` failed: a backoff promises that waiting helps; a 401 needs a new credential
  left: 1
 right: 0

After the fix it passes. It asserts the server is parked not penalised, that it is still parked after BACKOFF_MAX * 2 (so a penalised server would certainly have been retried by then), and that disabling clears the verdict.

Related

  • openhuman issue: tinyhumansai/openhuman#6412
  • Delivering this to the desktop app additionally needs an openhuman gitlink bump for vendor/tinymcp; that is separate work and not part of this PR.

…rever

An MCP server answering 401 was penalised on the backoff curve like any other
failure. The curve caps at BACKOFF_MAX, so the server was re-dialled every five
minutes for the life of the session — 18+ attempts in one reported session,
each writing a warning, against an endpoint that had already said no
(openhuman#6412).

Waiting does not turn a 401 into a 200. Only the user supplying a working
credential does, and that arrives through the store, not through time. This is
the same shape as a missing runtime, which the supervisor already parks with
exactly that reasoning, so route a rejected credential into the same terminal
path rather than inventing a second one.

The existing park machinery then covers the rest of the report for free: a
parked server is reported once and stays quiet, so the log noise stops at one
line; `terminal` is already cleared when a server is disabled, so toggling it
off and on after setting a token is the way back, the same gesture that already
clears a backoff penalty; and `ServerStatus::Unauthorized` already reaches the
host, so the state is visible rather than merely silent.
@tinysweeper

tinysweeper Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Tiny Sweeper reviewed this change across 6 lane(s) and found 0 active actionable finding(s). Detailed lane evidence and any incomplete work are listed below.

State: Ready for maintainer review
Priority: none
Reviewed head: 0f5e7fc7f661
Updated: 1790036999 (Unix time)

Review snapshot

Change surface Files Review signal Count
Production 1 Active findings 0
Tests 1 Noted findings 0
Documentation 0 Resolved findings 0
Configuration 0 Pending checks/questions 0

Completeness: Complete
Test assessment: No supported feature-to-test mapping was available; this does not mean tests are absent or passed.

What changed

The review could not produce a supported behavioral summary; inspect the cited changed surface and lane details below.

Features

None identified with supported citations.

Tests

No supported feature-to-test mapping was produced. Test execution is not inferred.

Findings

No active actionable findings.

Before merge

None.

How this fits together

flowchart LR
  n0["Supervisor<br/>changed"]:::changed
  n1["new"]:::impacted
  n2["install"]:::impacted
  n3["insert_server"]:::impacted
  n4["supervisor"]:::impacted
  n5["connected_to"]:::impacted
  n2 -->|calls| n1
  n4 -->|uses| n0
  n4 -->|calls| n1
  n5 -->|calls| n1
  n5 -->|calls| n2
  n5 -->|calls| n3
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading
Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The change consistently treats unauthorized connection failures as terminal alongside missing-runtime failures and adds a regression test for avoiding repeated retries. It looks safe to merge. _The code index is behind this pull request (indexed at `7dd00d74e96b`), so retrieved context may be out of date._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The change parks unauthorized servers instead of retrying them indefinitely, and the regression test covers the terminal state and re-enable path. It looks safe to merge. _The code index is behind this pull request (indexed at `7dd00d74e96b`), so retrieved context may be out of date._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: _The code index is behind this pull request (indexed at `7dd00d74e96b`), so retrieved context may be out of date._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._

commits

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: Nothing sensitive found in what this pull request commits.

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: This change correctly routes HTTP 401 responses into the terminal/park path instead of the backoff curve, preventing an infinite retry storm against an endpoint that has indicated its credential is wrong. The implementation is minimal, well-tested, and follows the existing pattern for missing runtime errors. It is safe to merge. _The code index is behind this pull request (indexed at `7dd00d74e96b`), so retrieved context may be out of date._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._

e2e

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: No end-to-end harness in this repository: no e2e test files and no e2e workflow.
Evidence and run details
  • Models: flash, ladder/vectors, gpt-5.6-luna, deepseek/deepseek-v4-flash
  • Spend: $0.005183
  • Tokens: 101047 input · 4152 output · 26251 cached · 347 embedding
Head State Pass summary
0f5e7fc7f661 ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 1790036999)

tinysweeper 0.1.0

@coderabbitai

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: b5746acc-cdd7-4d7a-9ae7-d649ede0da8f


Comment @coderabbitai help to get the list of available commands.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

             $0.0052 · 101,047 in / 4,152 out · 26,251 cached (26%) · flash, ladder/vectors, gpt-5.6-luna, deepseek/deepseek-v4-flash · 347 embedded
critique:    $0.0018 · 30,992 in  / 1,120 out · 2,119 cached (7%)   · gpt-5.6-luna
security:    $0.0017 · 30,504 in  / 858 out   · 1,875 cached (6%)   · gpt-5.6-luna
tests:       $0.0007 · 26,807 in  / 728 out   · 18,872 cached (70%) · deepseek/deepseek-v4-flash
description: $0.0005 · 5,391 in   / 78 out    · 0 cached (0%)       · deepseek/deepseek-v4-flash

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant