From 8b930d207e0506f3ce3e0a051e6f0c0b6f3009e5 Mon Sep 17 00:00:00 2001 From: Edward Park Date: Fri, 7 Aug 2026 23:31:32 -0700 Subject: [PATCH] feat: describe device authorization requests --- fastmcp_slim/fastmcp/cli/deploy/command.py | 13 +++++++++++++ tests/cli/deploy/test_command.py | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/fastmcp_slim/fastmcp/cli/deploy/command.py b/fastmcp_slim/fastmcp/cli/deploy/command.py index 630b12e88..6ac03a824 100644 --- a/fastmcp_slim/fastmcp/cli/deploy/command.py +++ b/fastmcp_slim/fastmcp/cli/deploy/command.py @@ -2,6 +2,7 @@ from __future__ import annotations +import platform import sys import webbrowser from typing import Annotated, NoReturn @@ -9,6 +10,7 @@ from typing import Annotated, NoReturn from cyclopts import Parameter from rich.status import Status +import fastmcp from fastmcp.cli.deploy.authentication import ( DeviceAuthorizationDeniedError, DeviceAuthorizationError, @@ -25,6 +27,7 @@ from fastmcp.cli.deploy.credentials import ( ) from fastmcp.cli.deploy.horizon_client import ( DeviceAuthorization, + DeviceMetadata, HorizonClient, HorizonResponseError, HorizonUnauthorizedError, @@ -64,6 +67,15 @@ def _can_open_browser() -> bool: return sys.stdin.isatty() and sys.stdout.isatty() +def _device_metadata() -> DeviceMetadata: + return DeviceMetadata( + device_name=platform.node() or None, + platform=platform.system().lower() or None, + architecture=platform.machine().lower() or None, + client_version=fastmcp.__version__, + ) + + def _fail( command: CommandName, category: ErrorCategory, @@ -193,6 +205,7 @@ async def login( async with HorizonClient(configuration.api_origin) as client: return await authorize_device( client, + metadata=_device_metadata(), on_challenge=show_challenge, open_browser=not json_output and _can_open_browser(), browser_opener=webbrowser.open, diff --git a/tests/cli/deploy/test_command.py b/tests/cli/deploy/test_command.py index f4c9e2949..d7f696aca 100644 --- a/tests/cli/deploy/test_command.py +++ b/tests/cli/deploy/test_command.py @@ -1,6 +1,7 @@ import json from collections.abc import Callable from unittest.mock import Mock +from urllib.parse import parse_qs import httpx2 import pytest @@ -111,6 +112,10 @@ async def test_json_login_writes_one_result_and_challenge_to_stderr( use_horizon_api(api) browser_open = Mock() monkeypatch.setattr(command_module.webbrowser, "open", browser_open) + monkeypatch.setattr(command_module.platform, "node", lambda: "Avery's laptop") + monkeypatch.setattr(command_module.platform, "system", lambda: "Darwin") + monkeypatch.setattr(command_module.platform, "machine", lambda: "arm64") + monkeypatch.setattr(command_module.fastmcp, "__version__", "4.0.0") await login(json_output=True) @@ -135,6 +140,18 @@ async def test_json_login_writes_one_result_and_challenge_to_stderr( "userCode": "ABCD-EFGH", } browser_open.assert_not_called() + authorization_request = next( + request + for request in api.requests + if request.url.path == "/api/v0/oauth/device/authorization" + ) + assert parse_qs(authorization_request.content.decode()) == { + "client_id": ["fastmcp-cli"], + "device_name": ["Avery's laptop"], + "platform": ["darwin"], + "architecture": ["arm64"], + "client_version": ["4.0.0"], + } state = json.loads(CredentialStore().path.read_text()) assert state == {"schemaVersion": 1, "apiKey": "fmcp_device_key"}