fix(mcp-oauth): fix token storage serialization and cached token refresh - #5399
fix(mcp-oauth): fix token storage serialization and cached token refresh#5399jvg123 wants to merge 1 commit into
Conversation
3f1ffa2 to
19b58ba
Compare
Two fixes for MCP OAuth in tools/mcp_oauth.py: 1. AnyUrl serialization: model_dump(mode='json') in set_tokens() and set_client_info() to handle Pydantic AnyUrl objects that aren't JSON-serializable by default. Without this, token storage fails on first OAuth connection with TypeError. 2. Cached token refresh (critical): get_tokens() now checks if the access token has expired by comparing file mtime + expires_in against current time. If expired, clears access_token but keeps refresh_token, so the MCP SDK uses the refresh flow instead of sending an expired token, getting 401, and falling through to full browser auth. Without this fix, any non-interactive deployment (gateway, Docker, cron) loses MCP connectivity after ~1 hour and cannot recover, even with a valid refresh token. Note: redirect URI port reuse is addressed separately in NousResearch#5345. Tests added for both fixes.
19b58ba to
8aa72ec
Compare
|
Thanks @jvg123. Closing — both fixes in this PR have been addressed on main:
Both contributors spotted the same serialization issue — credit preserved in the salvage PR. |
What does this PR do?
Fixes two bugs in
tools/mcp_oauth.pythat cause MCP OAuth to break in non-interactive environments (gateways, Docker containers, cron jobs). Together these bugs mean any deployment without a browser loses MCP connectivity after ~1 hour and cannot recover, even with a valid refresh token.Related Issue
Relates to #5344 (redirect URI port reuse addressed separately in #5345).
Type of Change
Changes Made
HermesTokenStorage.set_tokens()andset_client_info(): Changedmodel_dump(exclude_none=True)tomodel_dump(mode="json", exclude_none=True). PydanticAnyUrlfields in the MCP SDK's token/client models aren't serializable by the default JSON encoder, causingTypeError: Object of type AnyUrl is not JSON serializableon first OAuth connection.HermesTokenStorage.get_tokens(): When loading cached tokens from disk, checks whether the access token has expired by comparingfile_mtime + expires_inagainst the current time. If expired, clearsaccess_token(sets to empty string) but preservesrefresh_token. This causes the MCP SDK'sis_token_valid()to return False andcan_refresh_token()to return True, so it correctly uses the refresh flow instead of sending an expired token, receiving 401, and falling through to the full browser authorization flow.How to Test
auth: oauthin~/.hermes/config.yamltouch -t 202401010000 ~/.hermes/mcp-tokens/<server>.jsonpytest tests/tools/test_mcp_oauth.py -vNew test cases added:
TestAnyUrlSerialization::test_set_tokens_with_anyurl— verifiesmode="json"is passedTestAnyUrlSerialization::test_set_client_info_with_anyurl— verifiesmode="json"is passedTestExpiredTokenRefresh::test_expired_token_cleared— verifies expired access token is cleared, refresh token preservedTestExpiredTokenRefresh::test_fresh_token_not_cleared— verifies fresh tokens are returned as-is🤖 Generated with Claude Code assistance