mcp2cli 3.3.0__tar.gz → 3.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcp2cli-3.3.0 → mcp2cli-3.3.1}/PKG-INFO +1 -1
- {mcp2cli-3.3.0 → mcp2cli-3.3.1}/pyproject.toml +1 -1
- {mcp2cli-3.3.0 → mcp2cli-3.3.1}/src/mcp2cli/__init__.py +58 -12
- {mcp2cli-3.3.0 → mcp2cli-3.3.1}/README.md +0 -0
- {mcp2cli-3.3.0 → mcp2cli-3.3.1}/src/mcp2cli/__main__.py +0 -0
- {mcp2cli-3.3.0 → mcp2cli-3.3.1}/src/mcp2cli/py.typed +0 -0
|
@@ -810,15 +810,53 @@ def build_oauth_provider(
|
|
|
810
810
|
and the CLI hangs on the callback.
|
|
811
811
|
|
|
812
812
|
We patch both by restoring ``token_expiry_time`` from a sidecar
|
|
813
|
-
we persist in :class:`FileTokenStorage`, and
|
|
814
|
-
|
|
815
|
-
the subsequent re-auth
|
|
813
|
+
we persist in :class:`FileTokenStorage`, and — only when the token
|
|
814
|
+
endpoint *definitively* rejects the client or grant — by wiping the
|
|
815
|
+
cached ``client_info`` from disk and memory so the subsequent re-auth
|
|
816
|
+
performs fresh Dynamic Client Registration.
|
|
817
|
+
|
|
818
|
+
Issue #59: the wipe must NOT fire on a transient refresh failure (a
|
|
819
|
+
brief 5xx, clock skew, a momentarily-unhappy token endpoint). Erasing
|
|
820
|
+
``client.json``/``tokens.json`` on a transient blip forces a full
|
|
821
|
+
interactive ``authorization_code`` consent on the next call, which
|
|
822
|
+
permanently bricks any headless/scheduled run that has no browser. We
|
|
823
|
+
therefore preserve the persisted OAuth state on anything that isn't a
|
|
824
|
+
clean ``invalid_client``/``invalid_grant`` so a later retry can refresh
|
|
825
|
+
again on its own.
|
|
816
826
|
"""
|
|
817
827
|
|
|
818
828
|
async def _initialize(self) -> None:
|
|
819
829
|
await super()._initialize()
|
|
820
830
|
_restore_token_expiry_from_sidecar(self.context)
|
|
821
831
|
|
|
832
|
+
@staticmethod
|
|
833
|
+
async def _refresh_failure_is_definitive(response) -> bool:
|
|
834
|
+
"""Return True only when a failed refresh means the cached client
|
|
835
|
+
or grant is genuinely dead and must be discarded to recover.
|
|
836
|
+
|
|
837
|
+
Distinguishes a definitive ``invalid_client`` / ``invalid_grant``
|
|
838
|
+
rejection (RFC 6749 §5.2) from a transient transport/server error.
|
|
839
|
+
Returns False — i.e. "keep the persisted state" — for a 5xx, a
|
|
840
|
+
malformed body, or any other failure a later retry might survive
|
|
841
|
+
(issue #59).
|
|
842
|
+
"""
|
|
843
|
+
status = getattr(response, "status_code", None)
|
|
844
|
+
# Per RFC 6749 §5.2 a 401 from the token endpoint means client
|
|
845
|
+
# authentication failed (invalid_client) even when the body
|
|
846
|
+
# carries no machine-readable error code — definitive.
|
|
847
|
+
if status == 401:
|
|
848
|
+
return True
|
|
849
|
+
# Anything that isn't an OAuth 400/401 error response (notably a
|
|
850
|
+
# 5xx) is transient: keep the cache so the next run can retry.
|
|
851
|
+
if status != 400:
|
|
852
|
+
return False
|
|
853
|
+
try:
|
|
854
|
+
error = json.loads(await response.aread()).get("error")
|
|
855
|
+
except Exception:
|
|
856
|
+
# Unparseable body on a 400 — be conservative and preserve.
|
|
857
|
+
return False
|
|
858
|
+
return error in ("invalid_client", "invalid_grant", "unauthorized_client")
|
|
859
|
+
|
|
822
860
|
async def _handle_refresh_response(self, response) -> bool:
|
|
823
861
|
# Issue #58: RFC 6749 §5.1 permits a refresh response to omit
|
|
824
862
|
# refresh_token, in which case the previously issued one stays
|
|
@@ -832,15 +870,23 @@ def build_oauth_provider(
|
|
|
832
870
|
)
|
|
833
871
|
ok = await super()._handle_refresh_response(response)
|
|
834
872
|
if not ok:
|
|
835
|
-
# Refresh failed.
|
|
836
|
-
#
|
|
837
|
-
#
|
|
838
|
-
#
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
873
|
+
# Refresh failed. Only when the token endpoint *definitively*
|
|
874
|
+
# rejects us (invalid_client / invalid_grant) do we wipe the
|
|
875
|
+
# cached DCR client_id + tokens so the subsequent 401 fallback
|
|
876
|
+
# performs a fresh registration instead of
|
|
877
|
+
# /authorize?client_id=<stale> → opaque 500 (issue #54).
|
|
878
|
+
#
|
|
879
|
+
# A transient failure (5xx, blip, clock skew) must NOT erase
|
|
880
|
+
# the cache — doing so would force an interactive consent that
|
|
881
|
+
# no headless/scheduled run can complete (issue #59). Leaving
|
|
882
|
+
# client.json/tokens.json in place lets the next run simply
|
|
883
|
+
# retry the refresh.
|
|
884
|
+
if await self._refresh_failure_is_definitive(response):
|
|
885
|
+
self.context.client_info = None
|
|
886
|
+
storage = self.context.storage
|
|
887
|
+
if isinstance(storage, FileTokenStorage):
|
|
888
|
+
storage.clear_client_info()
|
|
889
|
+
storage.clear_tokens()
|
|
844
890
|
return ok
|
|
845
891
|
# Carry the prior refresh token forward when the server did not
|
|
846
892
|
# issue a new one, then re-persist so it survives a restart.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|