Skip to content

Commit 87e5d7c

Browse files
committed
Fix: resolve conflict between Single User Mode and OAuth 2.1
- Ensure '--single-user' CLI flag takes precedence over 'MCP_ENABLE_OAUTH21=true' in .env - Early setting of MCP_SINGLE_USER_MODE to influence configuration loading - Remove 'user_google_email' from tool signatures and docstrings in single-user mode - Allow implicit authentication in single-user mode by relaxing email validation Original objective: Fix google-workspace-mcp server error 'OAuth 2.1 mode requires an authenticated user' when running in single-user mode. Resolve conflict where MCP_ENABLE_OAUTH21=true in .env blocks --single-user mode, and make user_google_email optional in tool schemas for single-user operation. Generated-by: Gemini
1 parent 2b72c45 commit 87e5d7c

4 files changed

Lines changed: 59 additions & 17 deletions

File tree

auth/google_auth.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -849,11 +849,16 @@ async def get_authenticated_google_service(
849849
f"[{tool_name}] Attempting to get authenticated {service_name} service. Email: '{user_google_email}', Session: '{session_id}'"
850850
)
851851

852-
# Validate email format
853-
if not user_google_email or "@" not in user_google_email:
852+
# Validate email format (unless single-user mode, where it can be None)
853+
is_single_user = os.getenv("MCP_SINGLE_USER_MODE") == "1"
854+
if not is_single_user and (not user_google_email or "@" not in user_google_email):
854855
error_msg = f"Authentication required for {tool_name}. No valid 'user_google_email' provided. Please provide a valid Google email address."
855856
logger.info(f"[{tool_name}] {error_msg}")
856857
raise GoogleAuthenticationError(error_msg)
858+
elif is_single_user and not user_google_email:
859+
logger.debug(
860+
f"[{tool_name}] Single-user mode: user_google_email is None, will try to find any credentials."
861+
)
857862

858863
credentials = await asyncio.to_thread(
859864
get_credentials,
@@ -871,6 +876,18 @@ async def get_authenticated_google_service(
871876
f"[{tool_name}] Valid email '{user_google_email}' provided, initiating auth flow."
872877
)
873878

879+
# In single user mode without an email, we need one to start auth flow
880+
if is_single_user and not user_google_email:
881+
# Try to get default email from env
882+
from core.config import USER_GOOGLE_EMAIL
883+
884+
if USER_GOOGLE_EMAIL:
885+
user_google_email = USER_GOOGLE_EMAIL
886+
else:
887+
error_msg = f"Authentication required for {tool_name} in Single User Mode. No credentials found and no 'USER_GOOGLE_EMAIL' environment variable set. Please set USER_GOOGLE_EMAIL or provide email in the tool call to initiate authentication."
888+
logger.error(f"[{tool_name}] {error_msg}")
889+
raise GoogleAuthenticationError(error_msg)
890+
874891
# Ensure OAuth callback is available
875892
from auth.oauth_callback_server import ensure_oauth_callback_available
876893

auth/oauth_config.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,17 @@ def __init__(self):
3636
self.client_secret = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET")
3737

3838
# OAuth 2.1 configuration
39+
# Disable OAuth 2.1 if Single User Mode is active
40+
self.single_user_mode = os.getenv("MCP_SINGLE_USER_MODE", "false").lower() in (
41+
"1",
42+
"true",
43+
"yes",
44+
"on",
45+
)
3946
self.oauth21_enabled = (
4047
os.getenv("MCP_ENABLE_OAUTH21", "false").lower() == "true"
41-
)
48+
) and not self.single_user_mode
49+
4250
self.pkce_required = self.oauth21_enabled # PKCE is mandatory in OAuth 2.1
4351
self.supported_code_challenge_methods = (
4452
["S256", "plain"] if not self.oauth21_enabled else ["S256"]

auth/service_decorator.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import inspect
22
import logging
3+
import os
34

45
import re
56
from functools import wraps
@@ -526,7 +527,10 @@ def decorator(func: Callable) -> Callable:
526527

527528
# Create a new signature for the wrapper that excludes the 'service' parameter.
528529
# In OAuth 2.1 mode, also exclude 'user_google_email' since it's automatically determined.
529-
if is_oauth21_enabled():
530+
# In Single User Mode, also exclude 'user_google_email' to make it optional in schema.
531+
is_single_user = os.getenv("MCP_SINGLE_USER_MODE") == "1"
532+
533+
if is_oauth21_enabled() or is_single_user:
530534
# Remove both 'service' and 'user_google_email' parameters
531535
filtered_params = [p for p in params[1:] if p.name != "user_google_email"]
532536
wrapper_sig = original_sig.replace(parameters=filtered_params)
@@ -549,6 +553,10 @@ async def wrapper(*args, **kwargs):
549553
user_google_email = _extract_oauth21_user_email(
550554
authenticated_user, func.__name__
551555
)
556+
elif is_single_user:
557+
# In single user mode, user_google_email is optional (hidden from schema)
558+
# We can try to get it from kwargs if passed (e.g. explicitly), or default to None
559+
user_google_email = kwargs.get("user_google_email")
552560
else:
553561
user_google_email = _extract_oauth20_user_email(
554562
args, kwargs, wrapper_sig
@@ -614,7 +622,7 @@ async def wrapper(*args, **kwargs):
614622

615623
try:
616624
# In OAuth 2.1 mode, we need to add user_google_email to kwargs since it was removed from signature
617-
if is_oauth21_enabled():
625+
if is_oauth21_enabled() or is_single_user:
618626
kwargs["user_google_email"] = user_google_email
619627

620628
# Prepend the fetched service object to the original arguments
@@ -629,9 +637,9 @@ async def wrapper(*args, **kwargs):
629637
wrapper.__signature__ = wrapper_sig
630638

631639
# Conditionally modify docstring to remove user_google_email parameter documentation
632-
if is_oauth21_enabled():
640+
if is_oauth21_enabled() or is_single_user:
633641
logger.debug(
634-
"OAuth 2.1 mode enabled, removing user_google_email from docstring"
642+
"OAuth 2.1 or Single User mode enabled, removing user_google_email from docstring"
635643
)
636644
if func.__doc__:
637645
wrapper.__doc__ = _remove_user_email_arg_from_docstring(func.__doc__)
@@ -669,7 +677,10 @@ def decorator(func: Callable) -> Callable:
669677

670678
# Remove injected service params from the wrapper signature; drop user_google_email only for OAuth 2.1.
671679
filtered_params = [p for p in params if p.name not in service_param_names]
672-
if is_oauth21_enabled():
680+
681+
is_single_user = os.getenv("MCP_SINGLE_USER_MODE") == "1"
682+
683+
if is_oauth21_enabled() or is_single_user:
673684
filtered_params = [
674685
p for p in filtered_params if p.name != "user_google_email"
675686
]
@@ -688,6 +699,8 @@ async def wrapper(*args, **kwargs):
688699
user_google_email = _extract_oauth21_user_email(
689700
authenticated_user, tool_name
690701
)
702+
elif is_single_user:
703+
user_google_email = kwargs.get("user_google_email")
691704
else:
692705
user_google_email = _extract_oauth20_user_email(
693706
args, kwargs, wrapper_sig
@@ -752,7 +765,7 @@ async def wrapper(*args, **kwargs):
752765
# Call the original function with refresh error handling
753766
try:
754767
# In OAuth 2.1 mode, we need to add user_google_email to kwargs since it was removed from signature
755-
if is_oauth21_enabled():
768+
if is_oauth21_enabled() or is_single_user:
756769
kwargs["user_google_email"] = user_google_email
757770

758771
return await func(*args, **kwargs)
@@ -767,9 +780,9 @@ async def wrapper(*args, **kwargs):
767780
wrapper.__signature__ = wrapper_sig
768781

769782
# Conditionally modify docstring to remove user_google_email parameter documentation
770-
if is_oauth21_enabled():
783+
if is_oauth21_enabled() or is_single_user:
771784
logger.debug(
772-
"OAuth 2.1 mode enabled, removing user_google_email from docstring"
785+
"OAuth 2.1 or Single User mode enabled, removing user_google_email from docstring"
773786
)
774787
if func.__doc__:
775788
wrapper.__doc__ = _remove_user_email_arg_from_docstring(func.__doc__)

main.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ def main():
120120
)
121121
args = parser.parse_args()
122122

123+
# Set global single-user mode flag early to influence config loading
124+
if args.single_user:
125+
if is_stateless_mode():
126+
safe_print("❌ Single-user mode is incompatible with stateless mode")
127+
safe_print(" Stateless mode requires OAuth 2.1 which is multi-user")
128+
sys.exit(1)
129+
os.environ["MCP_SINGLE_USER_MODE"] = "1"
130+
# Reload config to apply single user mode setting to OAuthConfig
131+
reload_oauth_config()
132+
123133
# Set port and base URI once for reuse throughout the function
124134
port = int(os.getenv("PORT", os.getenv("WORKSPACE_MCP_PORT", 8000)))
125135
base_uri = os.getenv("WORKSPACE_MCP_BASE_URI", "http://localhost")
@@ -264,13 +274,7 @@ def main():
264274
safe_print(f" 📝 Log Level: {logging.getLogger().getEffectiveLevel()}")
265275
safe_print("")
266276

267-
# Set global single-user mode flag
268277
if args.single_user:
269-
if is_stateless_mode():
270-
safe_print("❌ Single-user mode is incompatible with stateless mode")
271-
safe_print(" Stateless mode requires OAuth 2.1 which is multi-user")
272-
sys.exit(1)
273-
os.environ["MCP_SINGLE_USER_MODE"] = "1"
274278
safe_print("🔐 Single-user mode enabled")
275279
safe_print("")
276280

0 commit comments

Comments
 (0)