Skip to content

Commit d62dcb6

Browse files
authored
Feature/remote authentication (#36)
* Implement oauth2.1 with mcp python skd oauth prvider * add authorization and orgID via env variables * bump version to 0.2.9
1 parent 711f251 commit d62dcb6

File tree

16 files changed

+1104
-96
lines changed

16 files changed

+1104
-96
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ description = "SingleStore MCP server"
55
readme = "README.md"
66
requires-python = ">=3.11"
77
dependencies = [
8+
"fastapi>=0.115.12",
89
"mcp[cli]>=1.8.1",
910
"nbformat>=5.10.4",
1011
"singlestoredb>=1.12.0",
File renamed without changes.

src/auth.py renamed to src/auth/auth.py

Lines changed: 81 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
import urllib.parse
1111
import requests
1212
from pathlib import Path
13-
from typing import Optional, Dict, Any, Tuple
13+
from typing import Optional, Dict, Any, Tuple, TYPE_CHECKING
1414
from datetime import datetime
1515
from pathlib import Path
16-
from src.constants import CLIENT_ID, OAUTH_HOST, AUTH_TIMEOUT_SECONDS, ROOT_DIR
17-
from src.config.app_config import app_config
16+
from src.config.config import CLIENT_ID, OAUTH_HOST, AUTH_TIMEOUT_SECONDS, ROOT_DIR
17+
from src.config.app_config import app_config, AuthMethod
18+
19+
if TYPE_CHECKING:
20+
from auth_provider import SingleStoreOAuthProvider
1821

1922
# Scopes that are always required
2023
ALWAYS_PRESENT_SCOPES = ["openid", "offline", "offline_access"]
@@ -218,8 +221,9 @@ def refresh_token(token_set: TokenSet, client_id: str = CLIENT_ID) -> Optional[T
218221
# Create new token set
219222
new_token_set = TokenSet(token_data)
220223

221-
# Save updated credentials
222-
save_credentials(new_token_set)
224+
# Only save to file in stdio mode
225+
if app_config.server_mode == "stdio":
226+
save_credentials(new_token_set)
223227

224228
return new_token_set
225229

@@ -354,58 +358,104 @@ def __init__(self, *args, **kwargs):
354358
# Create token set
355359
token_set = TokenSet(token_response)
356360

357-
# Save credentials
358-
save_credentials(token_set)
361+
# Only save credentials to file in stdio mode
362+
if app_config.server_mode == "stdio":
363+
save_credentials(token_set)
359364

360365
return True, token_set
361366

362367
except Exception as e:
363368
print(f"Authentication failed: {e}")
364369
return False, None
365370

366-
def get_authentication_token(client_id: Optional[str] = None) -> Optional[str]:
371+
def get_authentication_token(client_id: Optional[str] = None, http_auth_header: Optional[str] = None) -> Optional[str]:
367372
"""
368-
Get authentication token from environment or credentials file.
369-
If no valid token is available, prompt for authentication.
373+
Get authentication token from various sources based on server mode.
374+
For HTTP mode, prioritizes auth header, then app_config, then browser auth.
375+
For stdio mode, uses app_config, credentials file, then browser auth.
370376
371377
Args:
372378
client_id: Optional client ID to use for authentication
379+
http_auth_header: Optional HTTP Authorization header (for HTTP mode)
373380
374381
Returns:
375382
JWT token or API key if available, None otherwise
376383
"""
377-
# First check for API key in environment
384+
server_mode = app_config.server_mode # "stdio" or "http"
385+
386+
print(f"Server mode: {server_mode}")
387+
print(f"Client ID: {client_id}")
388+
print(f"HTTP Authorization header: {http_auth_header}")
389+
print(f"App config auth token: {app_config.get_auth_token()}")
390+
391+
# For HTTP mode, first check the Authorization header
392+
if server_mode == "http" and http_auth_header:
393+
print("Using token from HTTP Authorization header")
394+
if http_auth_header.startswith("Bearer "):
395+
token = http_auth_header[7:]
396+
print("Using token from HTTP Authorization header")
397+
app_config.set_auth_token(token, AuthMethod.JWT_TOKEN)
398+
return token
399+
400+
# Next, check for existing token in app_config
378401
api_key = app_config.get_auth_token()
379-
print(f"API key from environment: {api_key}")
402+
auth_method = app_config.get_auth_method()
403+
380404
if api_key:
405+
print(f"Using existing authentication token (type: {auth_method.name})")
381406
return api_key
382407

383-
# Then check for saved credentials
384-
credentials = load_credentials()
385-
if credentials and "token_set" in credentials:
386-
token_set = TokenSet(credentials["token_set"])
387-
388-
# If token is expired, try to refresh it
389-
if token_set.is_expired() and token_set.refresh_token:
390-
print("Access token expired, refreshing...")
391-
refreshed_token_set = refresh_token(token_set, client_id or CLIENT_ID)
392-
if refreshed_token_set:
393-
token_set = refreshed_token_set
394-
else:
395-
print("Token refresh failed, proceeding to re-authentication")
396-
397-
# If we have a valid token, use it
398-
if not token_set.is_expired() and token_set.access_token:
399-
print("Using saved OAuth token.")
400-
return token_set.access_token
408+
# For stdio mode, check saved credentials file
409+
if server_mode == "stdio":
410+
credentials = load_credentials()
411+
if credentials and "token_set" in credentials:
412+
token_set = TokenSet(credentials["token_set"])
413+
414+
# If token is expired, try to refresh it
415+
if token_set.is_expired() and token_set.refresh_token:
416+
print("Access token expired, refreshing...")
417+
refreshed_token_set = refresh_token(token_set, client_id or CLIENT_ID)
418+
if refreshed_token_set:
419+
token_set = refreshed_token_set
420+
# Update app config with the refreshed token
421+
app_config.set_auth_token(token_set.access_token, AuthMethod.OAUTH)
422+
else:
423+
print("Token refresh failed, proceeding to re-authentication")
424+
425+
# If we have a valid token, use it
426+
if not token_set.is_expired() and token_set.access_token:
427+
print("Using saved OAuth token.")
428+
app_config.set_auth_token(token_set.access_token, AuthMethod.OAUTH)
429+
return token_set.access_token
401430

402-
# If no valid credentials, authenticate
431+
# If no valid credentials found, launch browser authentication
403432
print("No API key or valid authentication token found.")
404433
success, token_set = authenticate(client_id)
405434

406435
if success and token_set and token_set.access_token:
407436
print("Authentication successful!")
437+
app_config.set_auth_token(token_set.access_token, AuthMethod.OAUTH)
438+
439+
# Only save to credentials file in stdio mode
440+
# In HTTP mode, we just keep it in memory (app_config)
441+
if server_mode == "stdio" and token_set:
442+
save_credentials(token_set)
443+
408444
return token_set.access_token
409445
else:
410446
print("Authentication failed. Please try again or provide an API key.")
411447
return None
448+
449+
def get_oauth_provider() -> Optional["SingleStoreOAuthProvider"]:
450+
"""
451+
Get the singleton instance of the OAuth provider.
452+
453+
Returns:
454+
The OAuth provider instance
455+
"""
456+
try:
457+
from src.auth.oauth_routes import oauth_provider
458+
return oauth_provider
459+
except ImportError:
460+
# Handle case where oauth_routes hasn't been initialized yet
461+
return None

0 commit comments

Comments
 (0)