Skip to content

Commit 00703d5

Browse files
committed
add auth commands
1 parent 1beacc0 commit 00703d5

File tree

8 files changed

+288
-0
lines changed

8 files changed

+288
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""MCP Agent Cloud authentication commands."""
2+
3+
from .login import login
4+
from .logout import logout
5+
from .whoami import whoami
6+
7+
__all__ = ["login", "logout", "whoami"]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""MCP Agent Cloud login command."""
2+
3+
from .main import login
4+
5+
__all__ = ["login"]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Constants for the MCP Agent CLI login command."""
2+
3+
# Default values
4+
# TODO: Change to oauth2
5+
DEFAULT_API_AUTH_PATH = "auth/signin?callbackUrl=%2Fapikeys%3Fcreate%3DMCP_AGENT_CLI"
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import asyncio
2+
from typing import Optional
3+
4+
import typer
5+
from rich.prompt import Confirm, Prompt
6+
7+
from mcp_agent_cloud.auth import (
8+
UserCredentials,
9+
load_credentials,
10+
save_credentials,
11+
)
12+
from mcp_agent_cloud.config import settings
13+
from mcp_agent_cloud.core.api_client import APIClient
14+
from mcp_agent_cloud.exceptions import CLIError
15+
from mcp_agent_cloud.ux import (
16+
print_info,
17+
print_success,
18+
print_warning,
19+
)
20+
21+
from .constants import DEFAULT_API_AUTH_PATH
22+
23+
24+
def _load_user_credentials(api_key: str) -> UserCredentials:
25+
"""Load credentials with user profile data fetched from API.
26+
27+
Args:
28+
api_key: The API key
29+
30+
Returns:
31+
UserCredentials object with profile data if available
32+
"""
33+
34+
async def fetch_profile() -> UserCredentials:
35+
"""Fetch user profile from the API."""
36+
client = APIClient(settings.API_BASE_URL, api_key)
37+
38+
response = await client.post("user/get_profile", {})
39+
user_data = response.json()
40+
41+
user_profile = user_data.get("user", {})
42+
43+
return UserCredentials(
44+
api_key=api_key,
45+
username=user_profile.get("name"),
46+
email=user_profile.get("email"),
47+
)
48+
49+
try:
50+
return asyncio.run(fetch_profile())
51+
except Exception as e:
52+
print_warning(f"Could not fetch user profile: {str(e)}")
53+
# Fallback to minimal credentials
54+
return UserCredentials(api_key=api_key)
55+
56+
57+
def login(
58+
api_key: Optional[str] = typer.Option(
59+
None,
60+
"--api-key",
61+
help="Optionally set an existing API key to use for authentication, bypassing manual login.",
62+
envvar="MCP_API_KEY",
63+
),
64+
no_open: bool = typer.Option(
65+
False,
66+
"--no-open",
67+
help="Don't automatically open browser for authentication.",
68+
),
69+
) -> str:
70+
"""Authenticate to MCP Agent Cloud API.
71+
72+
Direct to the api keys page for obtaining credentials, routing through login.
73+
74+
Args:
75+
api_key: Optionally set an existing API key to use for authentication, bypassing manual login.
76+
no_open: Don't automatically open browser for authentication.
77+
78+
Returns:
79+
API key string. Prints success message if login is successful.
80+
"""
81+
82+
existing_credentials = load_credentials()
83+
if existing_credentials and not existing_credentials.is_token_expired:
84+
if not Confirm.ask("You are already logged in. Do you want to login again?"):
85+
print_info("Using existing credentials.")
86+
return existing_credentials.api_key
87+
88+
if api_key:
89+
print_info("Using provided API key for authentication (MCP_API_KEY).")
90+
if not _is_valid_api_key(api_key):
91+
raise CLIError("Invalid API key provided.")
92+
93+
credentials = _load_user_credentials(api_key)
94+
95+
save_credentials(credentials)
96+
print_success("API key set.")
97+
if credentials.username:
98+
print_info(f"Logged in as: {credentials.username}")
99+
return api_key
100+
101+
base_url = settings.API_BASE_URL
102+
103+
return _handle_browser_auth(base_url, no_open)
104+
105+
106+
def _handle_browser_auth(base_url: str, no_open: bool) -> str:
107+
"""Handle browser-based authentication flow.
108+
109+
Args:
110+
base_url: API base URL
111+
no_open: Whether to skip automatic browser opening
112+
113+
Returns:
114+
API key string
115+
"""
116+
auth_url = f"{base_url}/{DEFAULT_API_AUTH_PATH}"
117+
118+
# TODO: This flow should be updated to OAuth2. Probably need to spin up local server to handle
119+
# the oauth2 callback url.
120+
if not no_open:
121+
print_info("Opening MCP Agent Cloud API login in browser...")
122+
print_info(
123+
f"If the browser doesn't automatically open, you can manually visit: {auth_url}"
124+
)
125+
typer.launch(auth_url)
126+
else:
127+
print_info(f"Please visit: {auth_url}")
128+
129+
return _handle_manual_key_input()
130+
131+
132+
def _handle_manual_key_input() -> str:
133+
"""Handle manual API key input.
134+
135+
Returns:
136+
API key string
137+
"""
138+
input_api_key = Prompt.ask("Please enter your API key :key:")
139+
140+
if not input_api_key:
141+
print_warning("No API key provided.")
142+
raise CLIError("Failed to set valid API key")
143+
144+
if not _is_valid_api_key(input_api_key):
145+
print_warning("Invalid API key provided.")
146+
raise CLIError("Failed to set valid API key")
147+
148+
credentials = _load_user_credentials(input_api_key)
149+
150+
save_credentials(credentials)
151+
print_success("API key set.")
152+
if credentials.username:
153+
print_info(f"Logged in as: {credentials.username}")
154+
155+
return input_api_key
156+
157+
158+
def _is_valid_api_key(api_key: str) -> bool:
159+
"""Validate the API key.
160+
161+
Args:
162+
api_key: The API key to validate.
163+
164+
Returns:
165+
bool: True if the API key is valid, False otherwise.
166+
"""
167+
return api_key.startswith("lm_mcp_api_")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""MCP Agent Cloud logout command."""
2+
3+
from .main import logout
4+
5+
__all__ = ["logout"]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""MCP Agent Cloud logout command implementation."""
2+
3+
import typer
4+
from rich.prompt import Confirm
5+
6+
from mcp_agent_cloud.auth import clear_credentials, load_credentials
7+
from mcp_agent_cloud.ux import print_info, print_success
8+
9+
10+
def logout() -> None:
11+
"""Clear credentials.
12+
13+
Removes stored authentication information.
14+
"""
15+
credentials = load_credentials()
16+
17+
if not credentials:
18+
print_info("Not currently logged in.")
19+
return
20+
21+
# Show who is being logged out
22+
user_info = "current user"
23+
if credentials.username:
24+
user_info = f"user '{credentials.username}'"
25+
elif credentials.email:
26+
user_info = f"user '{credentials.email}'"
27+
28+
# Confirm logout action
29+
if not Confirm.ask(f"Are you sure you want to logout {user_info}?"):
30+
print_info("Logout cancelled.")
31+
return
32+
33+
# Clear credentials
34+
if clear_credentials():
35+
print_success("Successfully logged out.")
36+
else:
37+
print_info("No credentials were found to clear.")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""MCP Agent Cloud whoami command."""
2+
3+
from .main import whoami
4+
5+
__all__ = ["whoami"]
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""MCP Agent Cloud whoami command implementation."""
2+
3+
from typing import Optional
4+
5+
import typer
6+
from rich.console import Console
7+
from rich.panel import Panel
8+
from rich.table import Table
9+
10+
from mcp_agent_cloud.auth import load_credentials
11+
from mcp_agent_cloud.exceptions import CLIError
12+
from mcp_agent_cloud.ux import print_error, print_info
13+
14+
15+
def whoami() -> None:
16+
"""Print current identity and org(s).
17+
18+
Shows the authenticated user information and organization memberships.
19+
"""
20+
credentials = load_credentials()
21+
22+
if not credentials:
23+
raise CLIError(
24+
"Not logged in. Use 'mcp-agent login' to authenticate.", exit_code=4
25+
)
26+
27+
if credentials.is_token_expired:
28+
raise CLIError(
29+
"Authentication token has expired. Use 'mcp-agent login' to re-authenticate.",
30+
exit_code=4,
31+
)
32+
33+
console = Console()
34+
35+
# Create user info table
36+
user_table = Table(show_header=False, box=None)
37+
user_table.add_column("Field", style="bold")
38+
user_table.add_column("Value")
39+
40+
# Add user information
41+
if credentials.username:
42+
user_table.add_row("Username", credentials.username)
43+
if credentials.email:
44+
user_table.add_row("Email", credentials.email)
45+
46+
# Add token expiry if available
47+
if credentials.token_expires_at:
48+
user_table.add_row(
49+
"Token Expires",
50+
credentials.token_expires_at.strftime("%Y-%m-%d %H:%M:%S UTC"),
51+
)
52+
else:
53+
user_table.add_row("Token Expires", "Never")
54+
55+
# Create user panel
56+
user_panel = Panel(user_table, title="User Information", title_align="left")
57+
console.print(user_panel)

0 commit comments

Comments
 (0)