diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx index 39b5a4abf..3cf286ce3 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -94,9 +94,11 @@ fastmcp whoami fastmcp logout ``` -`fastmcp login` always shows a verification URL and code. +`fastmcp login` first uses `HORIZON_API_KEY` or a valid stored key when one is available. +When login needs a new key, it shows a verification URL and code. It opens a browser when the terminal supports it. If the browser does not open, use the shown URL and code on another device. +To switch accounts, run `fastmcp logout` before you run `fastmcp login` again. Use `--host` to connect to another Horizon environment. The CLI saves the host and clears the stored key before a host change. diff --git a/docs/deployment/prefect-horizon.mdx b/docs/deployment/prefect-horizon.mdx index cab9795d1..beb80d34e 100644 --- a/docs/deployment/prefect-horizon.mdx +++ b/docs/deployment/prefect-horizon.mdx @@ -21,8 +21,10 @@ Sign in to Horizon from the FastMCP CLI with the device authorization flow. fastmcp login ``` -The command shows a verification URL and code before it opens the browser. +The command uses an environment key or a valid stored key when one is available. +When login needs a new key, it shows a verification URL and code before it opens the browser. If the browser cannot open, visit the shown URL and enter the code. +To switch accounts, run `fastmcp logout` before you run `fastmcp login` again. New users can register and create their first Horizon organization in the browser. Use `fastmcp login --host ` to save a different Horizon host. A host change clears the stored credential before login. diff --git a/fastmcp_slim/fastmcp/cli/deploy/command.py b/fastmcp_slim/fastmcp/cli/deploy/command.py index 6ac03a824..d0d3175d1 100644 --- a/fastmcp_slim/fastmcp/cli/deploy/command.py +++ b/fastmcp_slim/fastmcp/cli/deploy/command.py @@ -216,6 +216,7 @@ async def login( credential = await resolve_credential( credentials, authorize=device_authorization, + expected_api_origin=configuration.api_origin, ) try: @@ -234,6 +235,7 @@ async def login( credential = await resolve_credential( credentials, authorize=device_authorization, + expected_api_origin=configuration.api_origin, ) try: user = await _get_user( @@ -277,7 +279,14 @@ async def whoami( ) except HorizonUnauthorizedError as error: if credential is not None and credential.source == "stored": - credentials.clear() + try: + credentials.clear() + except StateFileError as cleanup_error: + _fail_for_expected_error( + "whoami", + cleanup_error, + json_output=json_output, + ) _fail_for_expected_error("whoami", error, json_output=json_output) except ( AuthenticationRequiredError, diff --git a/tests/cli/deploy/test_command.py b/tests/cli/deploy/test_command.py index d7f696aca..4cf7499c9 100644 --- a/tests/cli/deploy/test_command.py +++ b/tests/cli/deploy/test_command.py @@ -13,6 +13,7 @@ import fastmcp.cli.deploy.command as command_module from fastmcp.cli.deploy.command import login, logout, whoami from fastmcp.cli.deploy.credentials import CredentialStore from fastmcp.cli.deploy.horizon_client import HorizonClient +from fastmcp.cli.deploy.state import StateFileError class HorizonAuthAPI: @@ -259,6 +260,26 @@ async def test_login_never_persists_an_environment_key( ) +async def test_json_whoami_reports_a_failed_rejected_key_cleanup( + use_horizon_api: Callable[[HorizonAuthAPI], None], + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + use_horizon_api(HorizonAuthAPI(invalid_api_key="fmcp_stale_key")) + CredentialStore().save("fmcp_stale_key") + + def fail_clear(store: CredentialStore) -> None: + raise StateFileError("cleanup failed") + + monkeypatch.setattr(CredentialStore, "clear", fail_clear) + + with pytest.raises(SystemExit, match="1"): + await whoami(json_output=True) + + result = json.loads(capsys.readouterr().out) + assert result["error"]["category"] == "state_error" + + async def test_json_whoami_does_not_start_device_authorization( capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, diff --git a/tests/cli/deploy/test_output.py b/tests/cli/deploy/test_output.py index 79b45d875..f760c2f5c 100644 --- a/tests/cli/deploy/test_output.py +++ b/tests/cli/deploy/test_output.py @@ -51,7 +51,7 @@ def test_tty_device_challenge_uses_the_sign_in_layout( emit_device_challenge(authorization(), json_output=False) output = capsys.readouterr().out - assert "╭" in output + assert "│" in output assert "Deploy FastMCP on Horizon" in output assert "✓ Device authorization started" in output assert "https://horizon.prefect.io/oauth/device?user_code=ABCD-EFGH" in output @@ -82,7 +82,7 @@ def test_tty_identity_uses_an_account_panel( emit_identity("whoami", user(), json_output=False) output = capsys.readouterr().out - assert "╭" in output + assert "│" in output assert "Horizon Account" in output assert "Ada" in output assert "ada@example.com" in output @@ -124,7 +124,7 @@ def test_tty_logout_uses_the_horizon_header( output = capsys.readouterr().out assert "Logged out of Horizon" in output - assert "╭" in output + assert "│" in output def test_json_logout_has_stable_fields(