workspace-mcp 1.0.4__tar.gz → 1.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/PKG-INFO +1 -1
  2. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/auth/google_auth.py +293 -137
  3. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/core/utils.py +6 -2
  4. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/main.py +19 -12
  5. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/pyproject.toml +1 -1
  6. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/workspace_mcp.egg-info/PKG-INFO +1 -1
  7. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/LICENSE +0 -0
  8. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/README.md +0 -0
  9. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/auth/__init__.py +0 -0
  10. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/auth/oauth_callback_server.py +0 -0
  11. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/auth/oauth_responses.py +0 -0
  12. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/auth/scopes.py +0 -0
  13. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/auth/service_decorator.py +0 -0
  14. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/core/__init__.py +0 -0
  15. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/core/comments.py +0 -0
  16. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/core/context.py +0 -0
  17. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/core/server.py +0 -0
  18. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gcalendar/__init__.py +0 -0
  19. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gcalendar/calendar_tools.py +0 -0
  20. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gchat/__init__.py +0 -0
  21. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gchat/chat_tools.py +0 -0
  22. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gdocs/__init__.py +0 -0
  23. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gdocs/docs_tools.py +0 -0
  24. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gdrive/__init__.py +0 -0
  25. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gdrive/drive_tools.py +0 -0
  26. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gforms/__init__.py +0 -0
  27. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gforms/forms_tools.py +0 -0
  28. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gmail/__init__.py +0 -0
  29. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gmail/gmail_tools.py +0 -0
  30. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gsheets/__init__.py +0 -0
  31. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gsheets/sheets_tools.py +0 -0
  32. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gslides/__init__.py +0 -0
  33. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/gslides/slides_tools.py +0 -0
  34. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/setup.cfg +0 -0
  35. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/tests/test_auth.py +0 -0
  36. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/workspace_mcp.egg-info/SOURCES.txt +0 -0
  37. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/workspace_mcp.egg-info/dependency_links.txt +0 -0
  38. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/workspace_mcp.egg-info/entry_points.txt +0 -0
  39. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/workspace_mcp.egg-info/requires.txt +0 -0
  40. {workspace_mcp-1.0.4 → workspace_mcp-1.0.6}/workspace_mcp.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workspace-mcp
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: Comprehensive, highly performant Google Workspace Streamable HTTP & SSE MCP Server for Calendar, Gmail, Docs, Sheets, Slides & Drive
5
5
  Author-email: Taylor Wilsdon <taylor@taylorwilsdon.com>
6
6
  License: MIT
@@ -19,27 +19,48 @@ from auth.scopes import OAUTH_STATE_TO_SESSION_ID_MAP, SCOPES
19
19
  logging.basicConfig(level=logging.INFO)
20
20
  logger = logging.getLogger(__name__)
21
21
 
22
+
22
23
  # Constants
23
- DEFAULT_CREDENTIALS_DIR = ".credentials"
24
+ def get_default_credentials_dir():
25
+ """Get the default credentials directory path, preferring user-specific locations."""
26
+ # Check for explicit environment variable override
27
+ if os.getenv("GOOGLE_MCP_CREDENTIALS_DIR"):
28
+ return os.getenv("GOOGLE_MCP_CREDENTIALS_DIR")
29
+
30
+ # Use user home directory for credentials storage
31
+ home_dir = os.path.expanduser("~")
32
+ if home_dir and home_dir != "~": # Valid home directory found
33
+ return os.path.join(home_dir, ".google_workspace_mcp", "credentials")
34
+
35
+ # Fallback to current working directory if home directory is not accessible
36
+ return os.path.join(os.getcwd(), ".credentials")
37
+
38
+
39
+ DEFAULT_CREDENTIALS_DIR = get_default_credentials_dir()
24
40
 
25
41
  # In-memory cache for session credentials, maps session_id to Credentials object
26
42
  # This is brittle and bad, but our options are limited with Claude in present state.
27
43
  # This should be more robust in a production system once OAuth2.1 is implemented in client.
28
44
  _SESSION_CREDENTIALS_CACHE: Dict[str, Credentials] = {}
29
45
  # Centralized Client Secrets Path Logic
30
- _client_secrets_env = os.getenv("GOOGLE_CLIENT_SECRET_PATH") or os.getenv("GOOGLE_CLIENT_SECRETS")
46
+ _client_secrets_env = os.getenv("GOOGLE_CLIENT_SECRET_PATH") or os.getenv(
47
+ "GOOGLE_CLIENT_SECRETS"
48
+ )
31
49
  if _client_secrets_env:
32
50
  CONFIG_CLIENT_SECRETS_PATH = _client_secrets_env
33
51
  else:
34
52
  # Assumes this file is in auth/ and client_secret.json is in the root
35
53
  CONFIG_CLIENT_SECRETS_PATH = os.path.join(
36
54
  os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
37
- 'client_secret.json'
55
+ "client_secret.json",
38
56
  )
39
57
 
40
58
  # --- Helper Functions ---
41
59
 
42
- def _find_any_credentials(base_dir: str = DEFAULT_CREDENTIALS_DIR) -> Optional[Credentials]:
60
+
61
+ def _find_any_credentials(
62
+ base_dir: str = DEFAULT_CREDENTIALS_DIR,
63
+ ) -> Optional[Credentials]:
43
64
  """
44
65
  Find and load any valid credentials from the credentials directory.
45
66
  Used in single-user mode to bypass session-to-OAuth mapping.
@@ -53,104 +74,135 @@ def _find_any_credentials(base_dir: str = DEFAULT_CREDENTIALS_DIR) -> Optional[C
53
74
 
54
75
  # Scan for any .json credential files
55
76
  for filename in os.listdir(base_dir):
56
- if filename.endswith('.json'):
77
+ if filename.endswith(".json"):
57
78
  filepath = os.path.join(base_dir, filename)
58
79
  try:
59
- with open(filepath, 'r') as f:
80
+ with open(filepath, "r") as f:
60
81
  creds_data = json.load(f)
61
82
  credentials = Credentials(
62
- token=creds_data.get('token'),
63
- refresh_token=creds_data.get('refresh_token'),
64
- token_uri=creds_data.get('token_uri'),
65
- client_id=creds_data.get('client_id'),
66
- client_secret=creds_data.get('client_secret'),
67
- scopes=creds_data.get('scopes')
83
+ token=creds_data.get("token"),
84
+ refresh_token=creds_data.get("refresh_token"),
85
+ token_uri=creds_data.get("token_uri"),
86
+ client_id=creds_data.get("client_id"),
87
+ client_secret=creds_data.get("client_secret"),
88
+ scopes=creds_data.get("scopes"),
68
89
  )
69
90
  logger.info(f"[single-user] Found credentials in {filepath}")
70
91
  return credentials
71
92
  except (IOError, json.JSONDecodeError, KeyError) as e:
72
- logger.warning(f"[single-user] Error loading credentials from {filepath}: {e}")
93
+ logger.warning(
94
+ f"[single-user] Error loading credentials from {filepath}: {e}"
95
+ )
73
96
  continue
74
97
 
75
98
  logger.info(f"[single-user] No valid credentials found in {base_dir}")
76
99
  return None
77
100
 
78
- def _get_user_credential_path(user_google_email: str, base_dir: str = DEFAULT_CREDENTIALS_DIR) -> str:
101
+
102
+ def _get_user_credential_path(
103
+ user_google_email: str, base_dir: str = DEFAULT_CREDENTIALS_DIR
104
+ ) -> str:
79
105
  """Constructs the path to a user's credential file."""
80
106
  if not os.path.exists(base_dir):
81
107
  os.makedirs(base_dir)
82
108
  logger.info(f"Created credentials directory: {base_dir}")
83
109
  return os.path.join(base_dir, f"{user_google_email}.json")
84
110
 
85
- def save_credentials_to_file(user_google_email: str, credentials: Credentials, base_dir: str = DEFAULT_CREDENTIALS_DIR):
111
+
112
+ def save_credentials_to_file(
113
+ user_google_email: str,
114
+ credentials: Credentials,
115
+ base_dir: str = DEFAULT_CREDENTIALS_DIR,
116
+ ):
86
117
  """Saves user credentials to a file."""
87
118
  creds_path = _get_user_credential_path(user_google_email, base_dir)
88
119
  creds_data = {
89
- 'token': credentials.token,
90
- 'refresh_token': credentials.refresh_token,
91
- 'token_uri': credentials.token_uri,
92
- 'client_id': credentials.client_id,
93
- 'client_secret': credentials.client_secret,
94
- 'scopes': credentials.scopes,
95
- 'expiry': credentials.expiry.isoformat() if credentials.expiry else None
120
+ "token": credentials.token,
121
+ "refresh_token": credentials.refresh_token,
122
+ "token_uri": credentials.token_uri,
123
+ "client_id": credentials.client_id,
124
+ "client_secret": credentials.client_secret,
125
+ "scopes": credentials.scopes,
126
+ "expiry": credentials.expiry.isoformat() if credentials.expiry else None,
96
127
  }
97
128
  try:
98
- with open(creds_path, 'w') as f:
129
+ with open(creds_path, "w") as f:
99
130
  json.dump(creds_data, f)
100
131
  logger.info(f"Credentials saved for user {user_google_email} to {creds_path}")
101
132
  except IOError as e:
102
- logger.error(f"Error saving credentials for user {user_google_email} to {creds_path}: {e}")
133
+ logger.error(
134
+ f"Error saving credentials for user {user_google_email} to {creds_path}: {e}"
135
+ )
103
136
  raise
104
137
 
138
+
105
139
  def save_credentials_to_session(session_id: str, credentials: Credentials):
106
140
  """Saves user credentials to the in-memory session cache."""
107
141
  _SESSION_CREDENTIALS_CACHE[session_id] = credentials
108
142
  logger.debug(f"Credentials saved to session cache for session_id: {session_id}")
109
143
 
110
- def load_credentials_from_file(user_google_email: str, base_dir: str = DEFAULT_CREDENTIALS_DIR) -> Optional[Credentials]:
144
+
145
+ def load_credentials_from_file(
146
+ user_google_email: str, base_dir: str = DEFAULT_CREDENTIALS_DIR
147
+ ) -> Optional[Credentials]:
111
148
  """Loads user credentials from a file."""
112
149
  creds_path = _get_user_credential_path(user_google_email, base_dir)
113
150
  if not os.path.exists(creds_path):
114
- logger.info(f"No credentials file found for user {user_google_email} at {creds_path}")
151
+ logger.info(
152
+ f"No credentials file found for user {user_google_email} at {creds_path}"
153
+ )
115
154
  return None
116
155
 
117
156
  try:
118
- with open(creds_path, 'r') as f:
157
+ with open(creds_path, "r") as f:
119
158
  creds_data = json.load(f)
120
159
 
121
160
  # Parse expiry if present
122
161
  expiry = None
123
- if creds_data.get('expiry'):
162
+ if creds_data.get("expiry"):
124
163
  try:
125
164
  from datetime import datetime
126
- expiry = datetime.fromisoformat(creds_data['expiry'])
165
+
166
+ expiry = datetime.fromisoformat(creds_data["expiry"])
127
167
  except (ValueError, TypeError) as e:
128
- logger.warning(f"Could not parse expiry time for {user_google_email}: {e}")
168
+ logger.warning(
169
+ f"Could not parse expiry time for {user_google_email}: {e}"
170
+ )
129
171
 
130
172
  credentials = Credentials(
131
- token=creds_data.get('token'),
132
- refresh_token=creds_data.get('refresh_token'),
133
- token_uri=creds_data.get('token_uri'),
134
- client_id=creds_data.get('client_id'),
135
- client_secret=creds_data.get('client_secret'),
136
- scopes=creds_data.get('scopes'),
137
- expiry=expiry
173
+ token=creds_data.get("token"),
174
+ refresh_token=creds_data.get("refresh_token"),
175
+ token_uri=creds_data.get("token_uri"),
176
+ client_id=creds_data.get("client_id"),
177
+ client_secret=creds_data.get("client_secret"),
178
+ scopes=creds_data.get("scopes"),
179
+ expiry=expiry,
180
+ )
181
+ logger.debug(
182
+ f"Credentials loaded for user {user_google_email} from {creds_path}"
138
183
  )
139
- logger.debug(f"Credentials loaded for user {user_google_email} from {creds_path}")
140
184
  return credentials
141
185
  except (IOError, json.JSONDecodeError, KeyError) as e:
142
- logger.error(f"Error loading or parsing credentials for user {user_google_email} from {creds_path}: {e}")
186
+ logger.error(
187
+ f"Error loading or parsing credentials for user {user_google_email} from {creds_path}: {e}"
188
+ )
143
189
  return None
144
190
 
191
+
145
192
  def load_credentials_from_session(session_id: str) -> Optional[Credentials]:
146
193
  """Loads user credentials from the in-memory session cache."""
147
194
  credentials = _SESSION_CREDENTIALS_CACHE.get(session_id)
148
195
  if credentials:
149
- logger.debug(f"Credentials loaded from session cache for session_id: {session_id}")
196
+ logger.debug(
197
+ f"Credentials loaded from session cache for session_id: {session_id}"
198
+ )
150
199
  else:
151
- logger.debug(f"No credentials found in session cache for session_id: {session_id}")
200
+ logger.debug(
201
+ f"No credentials found in session cache for session_id: {session_id}"
202
+ )
152
203
  return credentials
153
204
 
205
+
154
206
  def load_client_secrets_from_env() -> Optional[Dict[str, Any]]:
155
207
  """
156
208
  Loads the client secrets from environment variables.
@@ -175,7 +227,7 @@ def load_client_secrets_from_env() -> Optional[Dict[str, Any]]:
175
227
  "client_secret": client_secret,
176
228
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
177
229
  "token_uri": "https://oauth2.googleapis.com/token",
178
- "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs"
230
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
179
231
  }
180
232
 
181
233
  # Add redirect_uri if provided via environment variable
@@ -191,6 +243,7 @@ def load_client_secrets_from_env() -> Optional[Dict[str, Any]]:
191
243
  logger.debug("OAuth client credentials not found in environment variables")
192
244
  return None
193
245
 
246
+
194
247
  def load_client_secrets(client_secrets_path: str) -> Dict[str, Any]:
195
248
  """
196
249
  Loads the client secrets from environment variables (preferred) or from the client secrets file.
@@ -217,21 +270,29 @@ def load_client_secrets(client_secrets_path: str) -> Dict[str, Any]:
217
270
 
218
271
  # Fall back to loading from file
219
272
  try:
220
- with open(client_secrets_path, 'r') as f:
273
+ with open(client_secrets_path, "r") as f:
221
274
  client_config = json.load(f)
222
275
  # The file usually contains a top-level key like "web" or "installed"
223
276
  if "web" in client_config:
224
- logger.info(f"Loaded OAuth client credentials from file: {client_secrets_path}")
277
+ logger.info(
278
+ f"Loaded OAuth client credentials from file: {client_secrets_path}"
279
+ )
225
280
  return client_config["web"]
226
281
  elif "installed" in client_config:
227
- logger.info(f"Loaded OAuth client credentials from file: {client_secrets_path}")
282
+ logger.info(
283
+ f"Loaded OAuth client credentials from file: {client_secrets_path}"
284
+ )
228
285
  return client_config["installed"]
229
286
  else:
230
- logger.error(f"Client secrets file {client_secrets_path} has unexpected format.")
231
- raise ValueError("Invalid client secrets file format")
287
+ logger.error(
288
+ f"Client secrets file {client_secrets_path} has unexpected format."
289
+ )
290
+ raise ValueError("Invalid client secrets file format")
232
291
  except (IOError, json.JSONDecodeError) as e:
233
292
  logger.error(f"Error loading client secrets file {client_secrets_path}: {e}")
234
293
  raise
294
+
295
+
235
296
  def check_client_secrets() -> Optional[str]:
236
297
  """
237
298
  Checks for the presence of OAuth client secrets, either as environment
@@ -242,45 +303,53 @@ def check_client_secrets() -> Optional[str]:
242
303
  """
243
304
  env_config = load_client_secrets_from_env()
244
305
  if not env_config and not os.path.exists(CONFIG_CLIENT_SECRETS_PATH):
245
- logger.error(f"OAuth client credentials not found. No environment variables set and no file at {CONFIG_CLIENT_SECRETS_PATH}")
306
+ logger.error(
307
+ f"OAuth client credentials not found. No environment variables set and no file at {CONFIG_CLIENT_SECRETS_PATH}"
308
+ )
246
309
  return f"OAuth client credentials not found. Please set GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET environment variables or provide a client secrets file at {CONFIG_CLIENT_SECRETS_PATH}."
247
310
  return None
248
311
 
249
- def create_oauth_flow(scopes: List[str], redirect_uri: str, state: Optional[str] = None) -> Flow:
312
+
313
+ def create_oauth_flow(
314
+ scopes: List[str], redirect_uri: str, state: Optional[str] = None
315
+ ) -> Flow:
250
316
  """Creates an OAuth flow using environment variables or client secrets file."""
251
317
  # Try environment variables first
252
318
  env_config = load_client_secrets_from_env()
253
319
  if env_config:
254
320
  # Use client config directly
255
321
  flow = Flow.from_client_config(
256
- env_config,
257
- scopes=scopes,
258
- redirect_uri=redirect_uri,
259
- state=state
322
+ env_config, scopes=scopes, redirect_uri=redirect_uri, state=state
260
323
  )
261
324
  logger.debug("Created OAuth flow from environment variables")
262
325
  return flow
263
326
 
264
327
  # Fall back to file-based config
265
328
  if not os.path.exists(CONFIG_CLIENT_SECRETS_PATH):
266
- raise FileNotFoundError(f"OAuth client secrets file not found at {CONFIG_CLIENT_SECRETS_PATH} and no environment variables set")
329
+ raise FileNotFoundError(
330
+ f"OAuth client secrets file not found at {CONFIG_CLIENT_SECRETS_PATH} and no environment variables set"
331
+ )
267
332
 
268
333
  flow = Flow.from_client_secrets_file(
269
334
  CONFIG_CLIENT_SECRETS_PATH,
270
335
  scopes=scopes,
271
336
  redirect_uri=redirect_uri,
272
- state=state
337
+ state=state,
338
+ )
339
+ logger.debug(
340
+ f"Created OAuth flow from client secrets file: {CONFIG_CLIENT_SECRETS_PATH}"
273
341
  )
274
- logger.debug(f"Created OAuth flow from client secrets file: {CONFIG_CLIENT_SECRETS_PATH}")
275
342
  return flow
276
343
 
344
+
277
345
  # --- Core OAuth Logic ---
278
346
 
347
+
279
348
  async def start_auth_flow(
280
349
  mcp_session_id: Optional[str],
281
350
  user_google_email: Optional[str],
282
- service_name: str, # e.g., "Google Calendar", "Gmail" for user messages
283
- redirect_uri: str, # Added redirect_uri as a required parameter
351
+ service_name: str, # e.g., "Google Calendar", "Gmail" for user messages
352
+ redirect_uri: str, # Added redirect_uri as a required parameter
284
353
  ) -> str:
285
354
  """
286
355
  Initiates the Google OAuth flow and returns an actionable message for the user.
@@ -297,29 +366,47 @@ async def start_auth_flow(
297
366
  Raises:
298
367
  Exception: If the OAuth flow cannot be initiated.
299
368
  """
300
- initial_email_provided = bool(user_google_email and user_google_email.strip() and user_google_email.lower() != 'default')
301
- user_display_name = f"{service_name} for '{user_google_email}'" if initial_email_provided else service_name
369
+ initial_email_provided = bool(
370
+ user_google_email
371
+ and user_google_email.strip()
372
+ and user_google_email.lower() != "default"
373
+ )
374
+ user_display_name = (
375
+ f"{service_name} for '{user_google_email}'"
376
+ if initial_email_provided
377
+ else service_name
378
+ )
302
379
 
303
- logger.info(f"[start_auth_flow] Initiating auth for {user_display_name} (session: {mcp_session_id}) with global SCOPES.")
380
+ logger.info(
381
+ f"[start_auth_flow] Initiating auth for {user_display_name} (session: {mcp_session_id}) with global SCOPES."
382
+ )
304
383
 
305
384
  try:
306
- if 'OAUTHLIB_INSECURE_TRANSPORT' not in os.environ and ("localhost" in redirect_uri or "127.0.0.1" in redirect_uri): # Use passed redirect_uri
307
- logger.warning("OAUTHLIB_INSECURE_TRANSPORT not set. Setting it for localhost/local development.")
308
- os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
385
+ if "OAUTHLIB_INSECURE_TRANSPORT" not in os.environ and (
386
+ "localhost" in redirect_uri or "127.0.0.1" in redirect_uri
387
+ ): # Use passed redirect_uri
388
+ logger.warning(
389
+ "OAUTHLIB_INSECURE_TRANSPORT not set. Setting it for localhost/local development."
390
+ )
391
+ os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
309
392
 
310
393
  oauth_state = os.urandom(16).hex()
311
394
  if mcp_session_id:
312
395
  OAUTH_STATE_TO_SESSION_ID_MAP[oauth_state] = mcp_session_id
313
- logger.info(f"[start_auth_flow] Stored mcp_session_id '{mcp_session_id}' for oauth_state '{oauth_state}'.")
396
+ logger.info(
397
+ f"[start_auth_flow] Stored mcp_session_id '{mcp_session_id}' for oauth_state '{oauth_state}'."
398
+ )
314
399
 
315
400
  flow = create_oauth_flow(
316
- scopes=SCOPES, # Use global SCOPES
317
- redirect_uri=redirect_uri, # Use passed redirect_uri
318
- state=oauth_state
401
+ scopes=SCOPES, # Use global SCOPES
402
+ redirect_uri=redirect_uri, # Use passed redirect_uri
403
+ state=oauth_state,
319
404
  )
320
405
 
321
- auth_url, _ = flow.authorization_url(access_type='offline', prompt='consent')
322
- logger.info(f"Auth flow started for {user_display_name}. State: {oauth_state}. Advise user to visit: {auth_url}")
406
+ auth_url, _ = flow.authorization_url(access_type="offline", prompt="consent")
407
+ logger.info(
408
+ f"Auth flow started for {user_display_name}. State: {oauth_state}. Advise user to visit: {auth_url}"
409
+ )
323
410
 
324
411
  message_lines = [
325
412
  f"**ACTION REQUIRED: Google Authentication Needed for {user_display_name}**\n",
@@ -330,18 +417,28 @@ async def start_auth_flow(
330
417
  "**LLM, after presenting the link, instruct the user as follows:**",
331
418
  "1. Click the link and complete the authorization in their browser.",
332
419
  ]
333
- session_info_for_llm = f" (this will link to your current session {mcp_session_id})" if mcp_session_id else ""
420
+ session_info_for_llm = (
421
+ f" (this will link to your current session {mcp_session_id})"
422
+ if mcp_session_id
423
+ else ""
424
+ )
334
425
 
335
426
  if not initial_email_provided:
336
- message_lines.extend([
337
- f"2. After successful authorization{session_info_for_llm}, the browser page will display the authenticated email address.",
338
- " **LLM: Instruct the user to provide you with this email address.**",
339
- "3. Once you have the email, **retry their original command, ensuring you include this `user_google_email`.**"
340
- ])
427
+ message_lines.extend(
428
+ [
429
+ f"2. After successful authorization{session_info_for_llm}, the browser page will display the authenticated email address.",
430
+ " **LLM: Instruct the user to provide you with this email address.**",
431
+ "3. Once you have the email, **retry their original command, ensuring you include this `user_google_email`.**",
432
+ ]
433
+ )
341
434
  else:
342
- message_lines.append(f"2. After successful authorization{session_info_for_llm}, **retry their original command**.")
435
+ message_lines.append(
436
+ f"2. After successful authorization{session_info_for_llm}, **retry their original command**."
437
+ )
343
438
 
344
- message_lines.append(f"\nThe application will use the new credentials. If '{user_google_email}' was provided, it must match the authenticated account.")
439
+ message_lines.append(
440
+ f"\nThe application will use the new credentials. If '{user_google_email}' was provided, it must match the authenticated account."
441
+ )
345
442
  return "\n".join(message_lines)
346
443
 
347
444
  except FileNotFoundError as e:
@@ -350,16 +447,22 @@ async def start_auth_flow(
350
447
  raise Exception(error_text)
351
448
  except Exception as e:
352
449
  error_text = f"Could not initiate authentication for {user_display_name} due to an unexpected error: {str(e)}"
353
- logger.error(f"Failed to start the OAuth flow for {user_display_name}: {e}", exc_info=True)
450
+ logger.error(
451
+ f"Failed to start the OAuth flow for {user_display_name}: {e}",
452
+ exc_info=True,
453
+ )
354
454
  raise Exception(error_text)
355
455
 
456
+
356
457
  def handle_auth_callback(
357
458
  scopes: List[str],
358
459
  authorization_response: str,
359
460
  redirect_uri: str,
360
461
  credentials_base_dir: str = DEFAULT_CREDENTIALS_DIR,
361
462
  session_id: Optional[str] = None,
362
- client_secrets_path: Optional[str] = None # Deprecated: kept for backward compatibility
463
+ client_secrets_path: Optional[
464
+ str
465
+ ] = None, # Deprecated: kept for backward compatibility
363
466
  ) -> Tuple[str, Credentials]:
364
467
  """
365
468
  Handles the callback from Google, exchanges the code for credentials,
@@ -385,17 +488,18 @@ def handle_auth_callback(
385
488
  try:
386
489
  # Log deprecation warning if old parameter is used
387
490
  if client_secrets_path:
388
- logger.warning("The 'client_secrets_path' parameter is deprecated. Use GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET environment variables instead.")
491
+ logger.warning(
492
+ "The 'client_secrets_path' parameter is deprecated. Use GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET environment variables instead."
493
+ )
389
494
 
390
495
  # Allow HTTP for localhost in development
391
- if 'OAUTHLIB_INSECURE_TRANSPORT' not in os.environ:
392
- logger.warning("OAUTHLIB_INSECURE_TRANSPORT not set. Setting it for localhost development.")
393
- os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
496
+ if "OAUTHLIB_INSECURE_TRANSPORT" not in os.environ:
497
+ logger.warning(
498
+ "OAUTHLIB_INSECURE_TRANSPORT not set. Setting it for localhost development."
499
+ )
500
+ os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
394
501
 
395
- flow = create_oauth_flow(
396
- scopes=scopes,
397
- redirect_uri=redirect_uri
398
- )
502
+ flow = create_oauth_flow(scopes=scopes, redirect_uri=redirect_uri)
399
503
 
400
504
  # Exchange the authorization code for credentials
401
505
  # Note: fetch_token will use the redirect_uri configured in the flow
@@ -405,11 +509,11 @@ def handle_auth_callback(
405
509
 
406
510
  # Get user info to determine user_id (using email here)
407
511
  user_info = get_user_info(credentials)
408
- if not user_info or 'email' not in user_info:
409
- logger.error("Could not retrieve user email from Google.")
410
- raise ValueError("Failed to get user email for identification.")
512
+ if not user_info or "email" not in user_info:
513
+ logger.error("Could not retrieve user email from Google.")
514
+ raise ValueError("Failed to get user email for identification.")
411
515
 
412
- user_google_email = user_info['email']
516
+ user_google_email = user_info["email"]
413
517
  logger.info(f"Identified user_google_email: {user_google_email}")
414
518
 
415
519
  # Save the credentials to file
@@ -421,16 +525,17 @@ def handle_auth_callback(
421
525
 
422
526
  return user_google_email, credentials
423
527
 
424
- except Exception as e: # Catch specific exceptions like FlowExchangeError if needed
528
+ except Exception as e: # Catch specific exceptions like FlowExchangeError if needed
425
529
  logger.error(f"Error handling auth callback: {e}")
426
- raise # Re-raise for the caller
530
+ raise # Re-raise for the caller
531
+
427
532
 
428
533
  def get_credentials(
429
- user_google_email: Optional[str], # Can be None if relying on session_id
534
+ user_google_email: Optional[str], # Can be None if relying on session_id
430
535
  required_scopes: List[str],
431
536
  client_secrets_path: Optional[str] = None,
432
537
  credentials_base_dir: str = DEFAULT_CREDENTIALS_DIR,
433
- session_id: Optional[str] = None
538
+ session_id: Optional[str] = None,
434
539
  ) -> Optional[Credentials]:
435
540
  """
436
541
  Retrieves stored credentials, prioritizing session, then file. Refreshes if necessary.
@@ -448,11 +553,15 @@ def get_credentials(
448
553
  Valid Credentials object or None.
449
554
  """
450
555
  # Check for single-user mode
451
- if os.getenv('MCP_SINGLE_USER_MODE') == '1':
452
- logger.info(f"[get_credentials] Single-user mode: bypassing session mapping, finding any credentials")
556
+ if os.getenv("MCP_SINGLE_USER_MODE") == "1":
557
+ logger.info(
558
+ f"[get_credentials] Single-user mode: bypassing session mapping, finding any credentials"
559
+ )
453
560
  credentials = _find_any_credentials(credentials_base_dir)
454
561
  if not credentials:
455
- logger.info(f"[get_credentials] Single-user mode: No credentials found in {credentials_base_dir}")
562
+ logger.info(
563
+ f"[get_credentials] Single-user mode: No credentials found in {credentials_base_dir}"
564
+ )
456
565
  return None
457
566
 
458
567
  # In single-user mode, if user_google_email wasn't provided, try to get it from user info
@@ -460,11 +569,15 @@ def get_credentials(
460
569
  if not user_google_email and credentials.valid:
461
570
  try:
462
571
  user_info = get_user_info(credentials)
463
- if user_info and 'email' in user_info:
464
- user_google_email = user_info['email']
465
- logger.debug(f"[get_credentials] Single-user mode: extracted user email {user_google_email} from credentials")
572
+ if user_info and "email" in user_info:
573
+ user_google_email = user_info["email"]
574
+ logger.debug(
575
+ f"[get_credentials] Single-user mode: extracted user email {user_google_email} from credentials"
576
+ )
466
577
  except Exception as e:
467
- logger.debug(f"[get_credentials] Single-user mode: could not extract user email: {e}")
578
+ logger.debug(
579
+ f"[get_credentials] Single-user mode: could not extract user email: {e}"
580
+ )
468
581
  else:
469
582
  credentials: Optional[Credentials] = None
470
583
 
@@ -472,61 +585,100 @@ def get_credentials(
472
585
  if not session_id:
473
586
  logger.debug("[get_credentials] No session_id provided")
474
587
 
475
- logger.debug(f"[get_credentials] Called for user_google_email: '{user_google_email}', session_id: '{session_id}', required_scopes: {required_scopes}")
588
+ logger.debug(
589
+ f"[get_credentials] Called for user_google_email: '{user_google_email}', session_id: '{session_id}', required_scopes: {required_scopes}"
590
+ )
476
591
 
477
592
  if session_id:
478
593
  credentials = load_credentials_from_session(session_id)
479
594
  if credentials:
480
- logger.debug(f"[get_credentials] Loaded credentials from session for session_id '{session_id}'.")
595
+ logger.debug(
596
+ f"[get_credentials] Loaded credentials from session for session_id '{session_id}'."
597
+ )
481
598
 
482
599
  if not credentials and user_google_email:
483
- logger.debug(f"[get_credentials] No session credentials, trying file for user_google_email '{user_google_email}'.")
484
- credentials = load_credentials_from_file(user_google_email, credentials_base_dir)
600
+ logger.debug(
601
+ f"[get_credentials] No session credentials, trying file for user_google_email '{user_google_email}'."
602
+ )
603
+ credentials = load_credentials_from_file(
604
+ user_google_email, credentials_base_dir
605
+ )
485
606
  if credentials and session_id:
486
- logger.debug(f"[get_credentials] Loaded from file for user '{user_google_email}', caching to session '{session_id}'.")
487
- save_credentials_to_session(session_id, credentials) # Cache for current session
607
+ logger.debug(
608
+ f"[get_credentials] Loaded from file for user '{user_google_email}', caching to session '{session_id}'."
609
+ )
610
+ save_credentials_to_session(
611
+ session_id, credentials
612
+ ) # Cache for current session
488
613
 
489
614
  if not credentials:
490
- logger.info(f"[get_credentials] No credentials found for user '{user_google_email}' or session '{session_id}'.")
615
+ logger.info(
616
+ f"[get_credentials] No credentials found for user '{user_google_email}' or session '{session_id}'."
617
+ )
491
618
  return None
492
619
 
493
- logger.debug(f"[get_credentials] Credentials found. Scopes: {credentials.scopes}, Valid: {credentials.valid}, Expired: {credentials.expired}")
620
+ logger.debug(
621
+ f"[get_credentials] Credentials found. Scopes: {credentials.scopes}, Valid: {credentials.valid}, Expired: {credentials.expired}"
622
+ )
494
623
 
495
624
  if not all(scope in credentials.scopes for scope in required_scopes):
496
- logger.warning(f"[get_credentials] Credentials lack required scopes. Need: {required_scopes}, Have: {credentials.scopes}. User: '{user_google_email}', Session: '{session_id}'")
497
- return None # Re-authentication needed for scopes
625
+ logger.warning(
626
+ f"[get_credentials] Credentials lack required scopes. Need: {required_scopes}, Have: {credentials.scopes}. User: '{user_google_email}', Session: '{session_id}'"
627
+ )
628
+ return None # Re-authentication needed for scopes
498
629
 
499
- logger.debug(f"[get_credentials] Credentials have sufficient scopes. User: '{user_google_email}', Session: '{session_id}'")
630
+ logger.debug(
631
+ f"[get_credentials] Credentials have sufficient scopes. User: '{user_google_email}', Session: '{session_id}'"
632
+ )
500
633
 
501
634
  if credentials.valid:
502
- logger.debug(f"[get_credentials] Credentials are valid. User: '{user_google_email}', Session: '{session_id}'")
635
+ logger.debug(
636
+ f"[get_credentials] Credentials are valid. User: '{user_google_email}', Session: '{session_id}'"
637
+ )
503
638
  return credentials
504
639
  elif credentials.expired and credentials.refresh_token:
505
- logger.info(f"[get_credentials] Credentials expired. Attempting refresh. User: '{user_google_email}', Session: '{session_id}'")
640
+ logger.info(
641
+ f"[get_credentials] Credentials expired. Attempting refresh. User: '{user_google_email}', Session: '{session_id}'"
642
+ )
506
643
  if not client_secrets_path:
507
- logger.error("[get_credentials] Client secrets path required for refresh but not provided.")
508
- return None
644
+ logger.error(
645
+ "[get_credentials] Client secrets path required for refresh but not provided."
646
+ )
647
+ return None
509
648
  try:
510
- logger.debug(f"[get_credentials] Refreshing token using client_secrets_path: {client_secrets_path}")
649
+ logger.debug(
650
+ f"[get_credentials] Refreshing token using client_secrets_path: {client_secrets_path}"
651
+ )
511
652
  # client_config = load_client_secrets(client_secrets_path) # Not strictly needed if creds have client_id/secret
512
653
  credentials.refresh(Request())
513
- logger.info(f"[get_credentials] Credentials refreshed successfully. User: '{user_google_email}', Session: '{session_id}'")
654
+ logger.info(
655
+ f"[get_credentials] Credentials refreshed successfully. User: '{user_google_email}', Session: '{session_id}'"
656
+ )
514
657
 
515
658
  # Save refreshed credentials
516
- if user_google_email: # Always save to file if email is known
517
- save_credentials_to_file(user_google_email, credentials, credentials_base_dir)
518
- if session_id: # Update session cache if it was the source or is active
659
+ if user_google_email: # Always save to file if email is known
660
+ save_credentials_to_file(
661
+ user_google_email, credentials, credentials_base_dir
662
+ )
663
+ if session_id: # Update session cache if it was the source or is active
519
664
  save_credentials_to_session(session_id, credentials)
520
665
  return credentials
521
666
  except RefreshError as e:
522
- logger.warning(f"[get_credentials] RefreshError - token expired/revoked: {e}. User: '{user_google_email}', Session: '{session_id}'")
667
+ logger.warning(
668
+ f"[get_credentials] RefreshError - token expired/revoked: {e}. User: '{user_google_email}', Session: '{session_id}'"
669
+ )
523
670
  # For RefreshError, we should return None to trigger reauthentication
524
671
  return None
525
672
  except Exception as e:
526
- logger.error(f"[get_credentials] Error refreshing credentials: {e}. User: '{user_google_email}', Session: '{session_id}'", exc_info=True)
527
- return None # Failed to refresh
673
+ logger.error(
674
+ f"[get_credentials] Error refreshing credentials: {e}. User: '{user_google_email}', Session: '{session_id}'",
675
+ exc_info=True,
676
+ )
677
+ return None # Failed to refresh
528
678
  else:
529
- logger.warning(f"[get_credentials] Credentials invalid/cannot refresh. Valid: {credentials.valid}, Refresh Token: {credentials.refresh_token is not None}. User: '{user_google_email}', Session: '{session_id}'")
679
+ logger.warning(
680
+ f"[get_credentials] Credentials invalid/cannot refresh. Valid: {credentials.valid}, Refresh Token: {credentials.refresh_token is not None}. User: '{user_google_email}', Session: '{session_id}'"
681
+ )
530
682
  return None
531
683
 
532
684
 
@@ -538,7 +690,7 @@ def get_user_info(credentials: Credentials) -> Optional[Dict[str, Any]]:
538
690
  try:
539
691
  # Using googleapiclient discovery to get user info
540
692
  # Requires 'google-api-python-client' library
541
- service = build('oauth2', 'v2', credentials=credentials)
693
+ service = build("oauth2", "v2", credentials=credentials)
542
694
  user_info = service.userinfo().get().execute()
543
695
  logger.info(f"Successfully fetched user info: {user_info.get('email')}")
544
696
  return user_info
@@ -553,18 +705,20 @@ def get_user_info(credentials: Credentials) -> Optional[Dict[str, Any]]:
553
705
 
554
706
  # --- Centralized Google Service Authentication ---
555
707
 
708
+
556
709
  class GoogleAuthenticationError(Exception):
557
710
  """Exception raised when Google authentication is required or fails."""
711
+
558
712
  def __init__(self, message: str, auth_url: Optional[str] = None):
559
713
  super().__init__(message)
560
714
  self.auth_url = auth_url
561
715
 
562
716
 
563
717
  async def get_authenticated_google_service(
564
- service_name: str, # "gmail", "calendar", "drive", "docs"
565
- version: str, # "v1", "v3"
566
- tool_name: str, # For logging/debugging
567
- user_google_email: str, # Required - no more Optional
718
+ service_name: str, # "gmail", "calendar", "drive", "docs"
719
+ version: str, # "v1", "v3"
720
+ tool_name: str, # For logging/debugging
721
+ user_google_email: str, # Required - no more Optional
568
722
  required_scopes: List[str],
569
723
  ) -> tuple[Any, str]:
570
724
  """
@@ -602,7 +756,6 @@ async def get_authenticated_google_service(
602
756
  session_id=None, # Session ID not available in service layer
603
757
  )
604
758
 
605
-
606
759
  if not credentials or not credentials.valid:
607
760
  logger.warning(
608
761
  f"[{tool_name}] No valid credentials. Email: '{user_google_email}'."
@@ -637,8 +790,11 @@ async def get_authenticated_google_service(
637
790
  if credentials and credentials.id_token:
638
791
  try:
639
792
  import jwt
793
+
640
794
  # Decode without verification (just to get email for logging)
641
- decoded_token = jwt.decode(credentials.id_token, options={"verify_signature": False})
795
+ decoded_token = jwt.decode(
796
+ credentials.id_token, options={"verify_signature": False}
797
+ )
642
798
  token_email = decoded_token.get("email")
643
799
  if token_email:
644
800
  log_user_email = token_email
@@ -646,12 +802,12 @@ async def get_authenticated_google_service(
646
802
  except Exception as e:
647
803
  logger.debug(f"[{tool_name}] Could not decode id_token: {e}")
648
804
 
649
- logger.info(f"[{tool_name}] Successfully authenticated {service_name} service for user: {log_user_email}")
805
+ logger.info(
806
+ f"[{tool_name}] Successfully authenticated {service_name} service for user: {log_user_email}"
807
+ )
650
808
  return service, log_user_email
651
809
 
652
810
  except Exception as e:
653
811
  error_msg = f"[{tool_name}] Failed to build {service_name} service: {str(e)}"
654
812
  logger.error(error_msg, exc_info=True)
655
813
  raise GoogleAuthenticationError(error_msg)
656
-
657
-
@@ -8,17 +8,21 @@ from typing import List, Optional
8
8
 
9
9
  logger = logging.getLogger(__name__)
10
10
 
11
- def check_credentials_directory_permissions(credentials_dir: str = ".credentials") -> None:
11
+ def check_credentials_directory_permissions(credentials_dir: str = None) -> None:
12
12
  """
13
13
  Check if the service has appropriate permissions to create and write to the .credentials directory.
14
14
 
15
15
  Args:
16
- credentials_dir: Path to the credentials directory (default: ".credentials")
16
+ credentials_dir: Path to the credentials directory (default: uses get_default_credentials_dir())
17
17
 
18
18
  Raises:
19
19
  PermissionError: If the service lacks necessary permissions
20
20
  OSError: If there are other file system issues
21
21
  """
22
+ if credentials_dir is None:
23
+ from auth.google_auth import get_default_credentials_dir
24
+ credentials_dir = get_default_credentials_dir()
25
+
22
26
  try:
23
27
  # Check if directory exists
24
28
  if os.path.exists(credentials_dir):
@@ -34,10 +34,17 @@ except Exception as e:
34
34
  sys.stderr.write(f"CRITICAL: Failed to set up file logging to '{log_file_path}': {e}\n")
35
35
 
36
36
  def safe_print(text):
37
+ # Don't print to stderr when running as MCP server via uvx to avoid JSON parsing errors
38
+ # Check if we're running as MCP server (no TTY and uvx in process name)
39
+ if not sys.stderr.isatty():
40
+ # Running as MCP server, suppress output to avoid JSON parsing errors
41
+ logger.debug(f"[MCP Server] {text}")
42
+ return
43
+
37
44
  try:
38
- print(text)
45
+ print(text, file=sys.stderr)
39
46
  except UnicodeEncodeError:
40
- print(text.encode('ascii', errors='replace').decode())
47
+ print(text.encode('ascii', errors='replace').decode(), file=sys.stderr)
41
48
 
42
49
  def main():
43
50
  """
@@ -47,7 +54,7 @@ def main():
47
54
  # Parse command line arguments
48
55
  parser = argparse.ArgumentParser(description='Google Workspace MCP Server')
49
56
  parser.add_argument('--single-user', action='store_true',
50
- help='Run in single-user mode - bypass session mapping and use any credentials from ./credentials directory')
57
+ help='Run in single-user mode - bypass session mapping and use any credentials from the credentials directory')
51
58
  parser.add_argument('--tools', nargs='*',
52
59
  choices=['gmail', 'drive', 'calendar', 'docs', 'sheets', 'chat', 'forms', 'slides'],
53
60
  help='Specify which tools to register. If not provided, all tools are registered.')
@@ -73,7 +80,7 @@ def main():
73
80
  safe_print(f" 🔐 OAuth Callback: {base_uri}:{port}/oauth2callback")
74
81
  safe_print(f" 👤 Mode: {'Single-user' if args.single_user else 'Multi-user'}")
75
82
  safe_print(f" 🐍 Python: {sys.version.split()[0]}")
76
- print()
83
+ safe_print("")
77
84
 
78
85
  # Import tool modules to register them with the MCP server via decorators
79
86
  tool_imports = {
@@ -104,29 +111,29 @@ def main():
104
111
  for tool in tools_to_import:
105
112
  tool_imports[tool]()
106
113
  safe_print(f" {tool_icons[tool]} {tool.title()} - Google {tool.title()} API integration")
107
- print()
114
+ safe_print("")
108
115
 
109
116
  safe_print(f"📊 Configuration Summary:")
110
117
  safe_print(f" 🔧 Tools Enabled: {len(tools_to_import)}/{len(tool_imports)}")
111
118
  safe_print(f" 🔑 Auth Method: OAuth 2.0 with PKCE")
112
119
  safe_print(f" 📝 Log Level: {logging.getLogger().getEffectiveLevel()}")
113
- print()
120
+ safe_print("")
114
121
 
115
122
  # Set global single-user mode flag
116
123
  if args.single_user:
117
124
  os.environ['MCP_SINGLE_USER_MODE'] = '1'
118
125
  safe_print("🔐 Single-user mode enabled")
119
- print()
126
+ safe_print("")
120
127
 
121
128
  # Check credentials directory permissions before starting
122
129
  try:
123
130
  safe_print("🔍 Checking credentials directory permissions...")
124
131
  check_credentials_directory_permissions()
125
132
  safe_print("✅ Credentials directory permissions verified")
126
- print()
133
+ safe_print("")
127
134
  except (PermissionError, OSError) as e:
128
135
  safe_print(f"❌ Credentials directory permission check failed: {e}")
129
- print(" Please ensure the service has write permissions to create/access the .credentials directory")
136
+ safe_print(" Please ensure the service has write permissions to create/access the credentials directory")
130
137
  logger.error(f"Failed credentials directory permission check: {e}")
131
138
  sys.exit(1)
132
139
 
@@ -141,12 +148,12 @@ def main():
141
148
  # Start minimal OAuth callback server for stdio mode
142
149
  from auth.oauth_callback_server import ensure_oauth_callback_available
143
150
  if ensure_oauth_callback_available('stdio', port, base_uri):
144
- print(f" OAuth callback server started on {base_uri}:{port}/oauth2callback")
151
+ safe_print(f" OAuth callback server started on {base_uri}:{port}/oauth2callback")
145
152
  else:
146
153
  safe_print(" ⚠️ Warning: Failed to start OAuth callback server")
147
154
 
148
- print(" Ready for MCP connections!")
149
- print()
155
+ safe_print(" Ready for MCP connections!")
156
+ safe_print("")
150
157
 
151
158
  if args.transport == 'streamable-http':
152
159
  # The server is already configured with port and server_url in core/server.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "workspace-mcp"
7
- version = "1.0.4"
7
+ version = "1.0.6"
8
8
  description = "Comprehensive, highly performant Google Workspace Streamable HTTP & SSE MCP Server for Calendar, Gmail, Docs, Sheets, Slides & Drive"
9
9
  readme = "README.md"
10
10
  keywords = [ "mcp", "google", "workspace", "llm", "ai", "claude", "model", "context", "protocol", "server"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workspace-mcp
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: Comprehensive, highly performant Google Workspace Streamable HTTP & SSE MCP Server for Calendar, Gmail, Docs, Sheets, Slides & Drive
5
5
  Author-email: Taylor Wilsdon <taylor@taylorwilsdon.com>
6
6
  License: MIT
File without changes
File without changes
File without changes