workspace-mcp 1.1.10__tar.gz → 1.1.13__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 (55) hide show
  1. {workspace_mcp-1.1.10/workspace_mcp.egg-info → workspace_mcp-1.1.13}/PKG-INFO +14 -6
  2. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/README.md +12 -4
  3. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/google_auth.py +13 -15
  4. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth_callback_server.py +27 -27
  5. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/scopes.py +0 -5
  6. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/core/server.py +12 -17
  7. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gcalendar/calendar_tools.py +19 -10
  8. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/main.py +6 -2
  9. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/pyproject.toml +2 -2
  10. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13/workspace_mcp.egg-info}/PKG-INFO +14 -6
  11. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/LICENSE +0 -0
  12. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/__init__.py +0 -0
  13. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth21/__init__.py +0 -0
  14. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth21/compat.py +0 -0
  15. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth21/config.py +0 -0
  16. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth21/discovery.py +0 -0
  17. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth21/example_config.py +0 -0
  18. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth21/handler.py +0 -0
  19. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth21/http.py +0 -0
  20. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth21/jwt.py +0 -0
  21. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth21/middleware.py +0 -0
  22. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth21/oauth2.py +0 -0
  23. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth21/sessions.py +0 -0
  24. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth21/tokens.py +0 -0
  25. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/oauth_responses.py +0 -0
  26. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/auth/service_decorator.py +0 -0
  27. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/core/__init__.py +0 -0
  28. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/core/comments.py +0 -0
  29. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/core/context.py +0 -0
  30. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/core/utils.py +0 -0
  31. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gTasks/__init__.py +0 -0
  32. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gTasks/tasks_tools.py +0 -0
  33. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gcalendar/__init__.py +0 -0
  34. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gchat/__init__.py +0 -0
  35. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gchat/chat_tools.py +0 -0
  36. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gdocs/__init__.py +0 -0
  37. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gdocs/docs_tools.py +0 -0
  38. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gdrive/__init__.py +0 -0
  39. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gdrive/drive_tools.py +0 -0
  40. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gforms/__init__.py +0 -0
  41. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gforms/forms_tools.py +0 -0
  42. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gmail/__init__.py +0 -0
  43. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gmail/gmail_tools.py +0 -0
  44. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gsheets/__init__.py +0 -0
  45. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gsheets/sheets_tools.py +0 -0
  46. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gslides/__init__.py +0 -0
  47. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/gslides/slides_tools.py +0 -0
  48. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/setup.cfg +0 -0
  49. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/tests/test_auth.py +0 -0
  50. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/tests/test_oauth_callback_server.py +0 -0
  51. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/workspace_mcp.egg-info/SOURCES.txt +0 -0
  52. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/workspace_mcp.egg-info/dependency_links.txt +0 -0
  53. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/workspace_mcp.egg-info/entry_points.txt +0 -0
  54. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/workspace_mcp.egg-info/requires.txt +0 -0
  55. {workspace_mcp-1.1.10 → workspace_mcp-1.1.13}/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.1.10
3
+ Version: 1.1.13
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
@@ -27,7 +27,7 @@ Classifier: Topic :: Communications :: Chat
27
27
  Classifier: Topic :: Office/Business
28
28
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
29
29
  Classifier: Typing :: Typed
30
- Requires-Python: >=3.11
30
+ Requires-Python: >=3.10
31
31
  Description-Content-Type: text/markdown
32
32
  License-File: LICENSE
33
33
  Requires-Dist: fastapi>=0.115.12
@@ -46,7 +46,7 @@ Dynamic: license-file
46
46
  # Google Workspace MCP Server <img src="https://github.com/user-attachments/assets/b89524e4-6e6e-49e6-ba77-00d6df0c6e5c" width="80" align="right" />
47
47
 
48
48
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
49
- [![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
49
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
50
50
  [![PyPI](https://img.shields.io/pypi/v/workspace-mcp.svg)](https://pypi.org/project/workspace-mcp/)
51
51
  [![PyPI Downloads](https://static.pepy.tech/badge/workspace-mcp/month)](https://pepy.tech/projects/workspace-mcp)
52
52
  [![Website](https://img.shields.io/badge/Website-workspacemcp.com-green.svg)](https://workspacemcp.com)
@@ -151,7 +151,7 @@ If you’re developing, deploying to servers, or using another MCP-capable clien
151
151
  #### Instant CLI (uvx)
152
152
 
153
153
  ```bash
154
- # Requires Python 3.11+ and uvx
154
+ # Requires Python 3.10+ and uvx
155
155
  export GOOGLE_OAUTH_CLIENT_ID="xxx"
156
156
  export GOOGLE_OAUTH_CLIENT_SECRET="yyy"
157
157
  uvx workspace-mcp --tools gmail drive calendar
@@ -174,7 +174,7 @@ uvx workspace-mcp --tools gmail drive calendar tasks
174
174
  uvx workspace-mcp --transport streamable-http
175
175
  ```
176
176
 
177
- *Requires Python 3.11+ and [uvx](https://github.com/astral-sh/uv). The package is available on [PyPI](https://pypi.org/project/workspace-mcp).*
177
+ *Requires Python 3.10+ and [uvx](https://github.com/astral-sh/uv). The package is available on [PyPI](https://pypi.org/project/workspace-mcp).*
178
178
 
179
179
  ### Development Installation
180
180
 
@@ -188,7 +188,7 @@ uv run main.py
188
188
 
189
189
  ### Prerequisites
190
190
 
191
- - **Python 3.11+**
191
+ - **Python 3.10+**
192
192
  - **[uvx](https://github.com/astral-sh/uv)** (for instant installation) or [uv](https://github.com/astral-sh/uv) (for development)
193
193
  - **Google Cloud Project** with OAuth 2.0 credentials
194
194
 
@@ -524,6 +524,14 @@ async def your_new_tool(service, param1: str, param2: int = 10):
524
524
 
525
525
  To use this server as a tool provider within Open WebUI:
526
526
 
527
+ ### Instant Start (No Config Needed)
528
+ Just copy and paste the below, set your values and you're off!
529
+ ```bash
530
+ GOOGLE_OAUTH_CLIENT_ID="your_client_id" GOOGLE_OAUTH_CLIENT_SECRET="your_client_secret" uvx mcpo --port 8000 --api-key "top-secret" -- uvx workspace-mcp
531
+ ```
532
+
533
+ Otherwise:
534
+
527
535
  ### 1. Create MCPO Configuration
528
536
 
529
537
  Create a file named `config.json` with the following structure to have `mcpo` make the streamable HTTP endpoint available as an OpenAPI spec tool:
@@ -3,7 +3,7 @@
3
3
  # Google Workspace MCP Server <img src="https://github.com/user-attachments/assets/b89524e4-6e6e-49e6-ba77-00d6df0c6e5c" width="80" align="right" />
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
- [![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
6
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
7
7
  [![PyPI](https://img.shields.io/pypi/v/workspace-mcp.svg)](https://pypi.org/project/workspace-mcp/)
8
8
  [![PyPI Downloads](https://static.pepy.tech/badge/workspace-mcp/month)](https://pepy.tech/projects/workspace-mcp)
9
9
  [![Website](https://img.shields.io/badge/Website-workspacemcp.com-green.svg)](https://workspacemcp.com)
@@ -108,7 +108,7 @@ If you’re developing, deploying to servers, or using another MCP-capable clien
108
108
  #### Instant CLI (uvx)
109
109
 
110
110
  ```bash
111
- # Requires Python 3.11+ and uvx
111
+ # Requires Python 3.10+ and uvx
112
112
  export GOOGLE_OAUTH_CLIENT_ID="xxx"
113
113
  export GOOGLE_OAUTH_CLIENT_SECRET="yyy"
114
114
  uvx workspace-mcp --tools gmail drive calendar
@@ -131,7 +131,7 @@ uvx workspace-mcp --tools gmail drive calendar tasks
131
131
  uvx workspace-mcp --transport streamable-http
132
132
  ```
133
133
 
134
- *Requires Python 3.11+ and [uvx](https://github.com/astral-sh/uv). The package is available on [PyPI](https://pypi.org/project/workspace-mcp).*
134
+ *Requires Python 3.10+ and [uvx](https://github.com/astral-sh/uv). The package is available on [PyPI](https://pypi.org/project/workspace-mcp).*
135
135
 
136
136
  ### Development Installation
137
137
 
@@ -145,7 +145,7 @@ uv run main.py
145
145
 
146
146
  ### Prerequisites
147
147
 
148
- - **Python 3.11+**
148
+ - **Python 3.10+**
149
149
  - **[uvx](https://github.com/astral-sh/uv)** (for instant installation) or [uv](https://github.com/astral-sh/uv) (for development)
150
150
  - **Google Cloud Project** with OAuth 2.0 credentials
151
151
 
@@ -481,6 +481,14 @@ async def your_new_tool(service, param1: str, param2: int = 10):
481
481
 
482
482
  To use this server as a tool provider within Open WebUI:
483
483
 
484
+ ### Instant Start (No Config Needed)
485
+ Just copy and paste the below, set your values and you're off!
486
+ ```bash
487
+ GOOGLE_OAUTH_CLIENT_ID="your_client_id" GOOGLE_OAUTH_CLIENT_SECRET="your_client_secret" uvx mcpo --port 8000 --api-key "top-secret" -- uvx workspace-mcp
488
+ ```
489
+
490
+ Otherwise:
491
+
484
492
  ### 1. Create MCPO Configuration
485
493
 
486
494
  Create a file named `config.json` with the following structure to have `mcpo` make the streamable HTTP endpoint available as an OpenAPI spec tool:
@@ -15,7 +15,7 @@ from google.auth.transport.requests import Request
15
15
  from google.auth.exceptions import RefreshError
16
16
  from googleapiclient.discovery import build
17
17
  from googleapiclient.errors import HttpError
18
- from auth.scopes import OAUTH_STATE_TO_SESSION_ID_MAP, SCOPES
18
+ from auth.scopes import SCOPES
19
19
 
20
20
  # Configure logging
21
21
  logging.basicConfig(level=logging.INFO)
@@ -346,7 +346,6 @@ def create_oauth_flow(
346
346
 
347
347
 
348
348
  async def start_auth_flow(
349
- mcp_session_id: Optional[str],
350
349
  user_google_email: Optional[str],
351
350
  service_name: str, # e.g., "Google Calendar", "Gmail" for user messages
352
351
  redirect_uri: str, # Added redirect_uri as a required parameter
@@ -355,7 +354,6 @@ async def start_auth_flow(
355
354
  Initiates the Google OAuth flow and returns an actionable message for the user.
356
355
 
357
356
  Args:
358
- mcp_session_id: The active MCP session ID.
359
357
  user_google_email: The user's specified Google email, if provided.
360
358
  service_name: The name of the Google service requiring auth (for user messages).
361
359
  redirect_uri: The URI Google will redirect to after authorization.
@@ -378,9 +376,19 @@ async def start_auth_flow(
378
376
  )
379
377
 
380
378
  logger.info(
381
- f"[start_auth_flow] Initiating auth for {user_display_name} (session: {mcp_session_id}) with global SCOPES."
379
+ f"[start_auth_flow] Initiating auth for {user_display_name} with global SCOPES."
382
380
  )
383
381
 
382
+ # Import here to avoid circular imports
383
+ from auth.oauth_callback_server import ensure_oauth_callback_available
384
+ from core.server import _current_transport_mode, WORKSPACE_MCP_PORT, WORKSPACE_MCP_BASE_URI
385
+
386
+ # Ensure OAuth callback server is available before generating URLs
387
+ success, error_msg = ensure_oauth_callback_available(_current_transport_mode, WORKSPACE_MCP_PORT, WORKSPACE_MCP_BASE_URI)
388
+ if not success:
389
+ error_detail = f" ({error_msg})" if error_msg else ""
390
+ raise Exception(f"Cannot initiate OAuth flow - callback server unavailable{error_detail}. Please ensure the OAuth callback server can start before attempting authentication.")
391
+
384
392
  try:
385
393
  if "OAUTHLIB_INSECURE_TRANSPORT" not in os.environ and (
386
394
  "localhost" in redirect_uri or "127.0.0.1" in redirect_uri
@@ -391,11 +399,6 @@ async def start_auth_flow(
391
399
  os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
392
400
 
393
401
  oauth_state = os.urandom(16).hex()
394
- if mcp_session_id:
395
- OAUTH_STATE_TO_SESSION_ID_MAP[oauth_state] = mcp_session_id
396
- logger.info(
397
- f"[start_auth_flow] Stored mcp_session_id '{mcp_session_id}' for oauth_state '{oauth_state}'."
398
- )
399
402
 
400
403
  flow = create_oauth_flow(
401
404
  scopes=SCOPES, # Use global SCOPES
@@ -417,11 +420,7 @@ async def start_auth_flow(
417
420
  "**LLM, after presenting the link, instruct the user as follows:**",
418
421
  "1. Click the link and complete the authorization in their browser.",
419
422
  ]
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
- )
423
+ session_info_for_llm = ""
425
424
 
426
425
  if not initial_email_provided:
427
426
  message_lines.extend(
@@ -773,7 +772,6 @@ async def get_authenticated_google_service(
773
772
 
774
773
  # Generate auth URL and raise exception with it
775
774
  auth_response = await start_auth_flow(
776
- mcp_session_id=None, # Session ID not available in service layer
777
775
  user_google_email=user_google_email,
778
776
  service_name=f"Google {service_name.title()}",
779
777
  redirect_uri=redirect_uri,
@@ -18,7 +18,7 @@ from typing import Optional
18
18
  from urllib.parse import urlparse
19
19
 
20
20
  from auth.google_auth import handle_auth_callback, check_client_secrets
21
- from auth.scopes import OAUTH_STATE_TO_SESSION_ID_MAP, SCOPES
21
+ from auth.scopes import SCOPES
22
22
  from auth.oauth_responses import create_error_response, create_success_response, create_server_error_response
23
23
 
24
24
  logger = logging.getLogger(__name__)
@@ -68,11 +68,7 @@ class MinimalOAuthServer:
68
68
 
69
69
  logger.info(f"OAuth callback: Received code (state: {state}). Attempting to exchange for tokens.")
70
70
 
71
- mcp_session_id: Optional[str] = OAUTH_STATE_TO_SESSION_ID_MAP.pop(state, None)
72
- if mcp_session_id:
73
- logger.info(f"OAuth callback: Retrieved MCP session ID '{mcp_session_id}' for state '{state}'.")
74
- else:
75
- logger.warning(f"OAuth callback: No MCP session ID found for state '{state}'. Auth will not be tied to a specific session.")
71
+ # Session ID tracking removed - not needed
76
72
 
77
73
  # Exchange code for credentials
78
74
  redirect_uri = get_oauth_redirect_uri(port=self.port, base_uri=self.base_uri)
@@ -80,11 +76,10 @@ class MinimalOAuthServer:
80
76
  scopes=SCOPES,
81
77
  authorization_response=str(request.url),
82
78
  redirect_uri=redirect_uri,
83
- session_id=mcp_session_id
79
+ session_id=None
84
80
  )
85
81
 
86
- log_session_part = f" (linked to session: {mcp_session_id})" if mcp_session_id else ""
87
- logger.info(f"OAuth callback: Successfully authenticated user: {verified_user_id} (state: {state}){log_session_part}.")
82
+ logger.info(f"OAuth callback: Successfully authenticated user: {verified_user_id} (state: {state}).")
88
83
 
89
84
  # Return success page using shared template
90
85
  return create_success_response(verified_user_id)
@@ -94,16 +89,16 @@ class MinimalOAuthServer:
94
89
  logger.error(error_message_detail, exc_info=True)
95
90
  return create_server_error_response(str(e))
96
91
 
97
- def start(self) -> bool:
92
+ def start(self) -> tuple[bool, str]:
98
93
  """
99
94
  Start the minimal OAuth server.
100
95
 
101
96
  Returns:
102
- True if started successfully, False otherwise
97
+ Tuple of (success: bool, error_message: str)
103
98
  """
104
99
  if self.is_running:
105
100
  logger.info("Minimal OAuth server is already running")
106
- return True
101
+ return True, ""
107
102
 
108
103
  # Check if port is available
109
104
  # Extract hostname from base_uri (e.g., "http://localhost" -> "localhost")
@@ -117,8 +112,9 @@ class MinimalOAuthServer:
117
112
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
118
113
  s.bind((hostname, self.port))
119
114
  except OSError:
120
- logger.error(f"Port {self.port} is already in use on {hostname}. Cannot start minimal OAuth server.")
121
- return False
115
+ error_msg = f"Port {self.port} is already in use on {hostname}. Cannot start minimal OAuth server."
116
+ logger.error(error_msg)
117
+ return False, error_msg
122
118
 
123
119
  def run_server():
124
120
  """Run the server in a separate thread."""
@@ -135,6 +131,7 @@ class MinimalOAuthServer:
135
131
 
136
132
  except Exception as e:
137
133
  logger.error(f"Minimal OAuth server error: {e}", exc_info=True)
134
+ self.is_running = False
138
135
 
139
136
  # Start server in background thread
140
137
  self.server_thread = threading.Thread(target=run_server, daemon=True)
@@ -150,13 +147,14 @@ class MinimalOAuthServer:
150
147
  if result == 0:
151
148
  self.is_running = True
152
149
  logger.info(f"Minimal OAuth server started on {hostname}:{self.port}")
153
- return True
150
+ return True, ""
154
151
  except Exception:
155
152
  pass
156
153
  time.sleep(0.1)
157
154
 
158
- logger.error(f"Failed to start minimal OAuth server on {hostname}:{self.port}")
159
- return False
155
+ error_msg = f"Failed to start minimal OAuth server on {hostname}:{self.port} - server did not respond within {max_wait}s"
156
+ logger.error(error_msg)
157
+ return False, error_msg
160
158
 
161
159
  def stop(self):
162
160
  """Stop the minimal OAuth server."""
@@ -207,7 +205,7 @@ def get_oauth_redirect_uri(port: int = 8000, base_uri: str = "http://localhost")
207
205
  logger.info(f"Constructed redirect URI: {constructed_uri}")
208
206
  return constructed_uri
209
207
 
210
- def ensure_oauth_callback_available(transport_mode: str = "stdio", port: int = 8000, base_uri: str = "http://localhost") -> bool:
208
+ def ensure_oauth_callback_available(transport_mode: str = "stdio", port: int = 8000, base_uri: str = "http://localhost") -> tuple[bool, str]:
211
209
  """
212
210
  Ensure OAuth callback endpoint is available for the given transport mode.
213
211
 
@@ -220,14 +218,14 @@ def ensure_oauth_callback_available(transport_mode: str = "stdio", port: int = 8
220
218
  base_uri: Base URI (default "http://localhost")
221
219
 
222
220
  Returns:
223
- True if callback endpoint is available, False otherwise
221
+ Tuple of (success: bool, error_message: str)
224
222
  """
225
223
  global _minimal_oauth_server
226
224
 
227
225
  if transport_mode == "streamable-http":
228
226
  # In streamable-http mode, the main FastAPI server should handle callbacks
229
227
  logger.debug("Using existing FastAPI server for OAuth callbacks (streamable-http mode)")
230
- return True
228
+ return True, ""
231
229
 
232
230
  elif transport_mode == "stdio":
233
231
  # In stdio mode, start minimal server if not already running
@@ -237,19 +235,21 @@ def ensure_oauth_callback_available(transport_mode: str = "stdio", port: int = 8
237
235
 
238
236
  if not _minimal_oauth_server.is_running:
239
237
  logger.info("Starting minimal OAuth server for stdio mode")
240
- result = _minimal_oauth_server.start()
241
- if result:
238
+ success, error_msg = _minimal_oauth_server.start()
239
+ if success:
242
240
  logger.info(f"Minimal OAuth server successfully started on {base_uri}:{port}")
241
+ return True, ""
243
242
  else:
244
- logger.error(f"Failed to start minimal OAuth server on {base_uri}:{port}")
245
- return result
243
+ logger.error(f"Failed to start minimal OAuth server on {base_uri}:{port}: {error_msg}")
244
+ return False, error_msg
246
245
  else:
247
246
  logger.info("Minimal OAuth server is already running")
248
- return True
247
+ return True, ""
249
248
 
250
249
  else:
251
- logger.error(f"Unknown transport mode: {transport_mode}")
252
- return False
250
+ error_msg = f"Unknown transport mode: {transport_mode}"
251
+ logger.error(error_msg)
252
+ return False, error_msg
253
253
 
254
254
  def cleanup_oauth_callback_server():
255
255
  """Clean up the minimal OAuth server if it was started."""
@@ -5,14 +5,9 @@ This module centralizes OAuth scope definitions for Google Workspace integration
5
5
  Separated from service_decorator.py to avoid circular imports.
6
6
  """
7
7
  import logging
8
- from typing import Dict
9
8
 
10
9
  logger = logging.getLogger(__name__)
11
10
 
12
- # Temporary map to associate OAuth state with MCP session ID
13
- # This should ideally be a more robust cache in a production system (e.g., Redis)
14
- OAUTH_STATE_TO_SESSION_ID_MAP: Dict[str, str] = {}
15
-
16
11
  # Individual OAuth Scope Constants
17
12
  USERINFO_EMAIL_SCOPE = 'https://www.googleapis.com/auth/userinfo.email'
18
13
  OPENID_SCOPE = 'openid'
@@ -16,7 +16,6 @@ from auth.oauth_responses import create_error_response, create_success_response,
16
16
 
17
17
  # Import shared configuration
18
18
  from auth.scopes import (
19
- OAUTH_STATE_TO_SESSION_ID_MAP,
20
19
  SCOPES,
21
20
  USERINFO_EMAIL_SCOPE, # noqa: F401
22
21
  OPENID_SCOPE, # noqa: F401
@@ -129,11 +128,7 @@ async def oauth2_callback(request: Request) -> HTMLResponse:
129
128
 
130
129
  logger.info(f"OAuth callback: Received code (state: {state}). Attempting to exchange for tokens.")
131
130
 
132
- mcp_session_id: Optional[str] = OAUTH_STATE_TO_SESSION_ID_MAP.pop(state, None)
133
- if mcp_session_id:
134
- logger.info(f"OAuth callback: Retrieved MCP session ID '{mcp_session_id}' for state '{state}'.")
135
- else:
136
- logger.warning(f"OAuth callback: No MCP session ID found for state '{state}'. Auth will not be tied to a specific session directly via this callback.")
131
+ # Session ID tracking removed - not needed
137
132
 
138
133
  # Exchange code for credentials. handle_auth_callback will save them.
139
134
  # The user_id returned here is the Google-verified email.
@@ -141,11 +136,10 @@ async def oauth2_callback(request: Request) -> HTMLResponse:
141
136
  scopes=SCOPES, # Ensure all necessary scopes are requested
142
137
  authorization_response=str(request.url),
143
138
  redirect_uri=get_oauth_redirect_uri_for_current_mode(),
144
- session_id=mcp_session_id # Pass session_id if available
139
+ session_id=None # Session ID tracking removed
145
140
  )
146
141
 
147
- log_session_part = f" (linked to session: {mcp_session_id})" if mcp_session_id else ""
148
- logger.info(f"OAuth callback: Successfully authenticated user: {verified_user_id} (state: {state}){log_session_part}.")
142
+ logger.info(f"OAuth callback: Successfully authenticated user: {verified_user_id} (state: {state}).")
149
143
 
150
144
  # Return success page using shared template
151
145
  return create_success_response(verified_user_id)
@@ -159,14 +153,13 @@ async def oauth2_callback(request: Request) -> HTMLResponse:
159
153
  @server.tool()
160
154
  async def start_google_auth(
161
155
  service_name: str,
162
- user_google_email: str = USER_GOOGLE_EMAIL,
163
- mcp_session_id: Optional[str] = Header(None, alias="mcp-session-id")
156
+ user_google_email: str = USER_GOOGLE_EMAIL
164
157
  ) -> str:
165
158
  """
166
159
  Initiates the Google OAuth 2.0 authentication flow for the specified user email and service.
167
160
  This is the primary method to establish credentials when no valid session exists or when targeting a specific account for a particular service.
168
161
  It generates an authorization URL that the LLM must present to the user.
169
- The authentication attempt is linked to the current MCP session via `mcp_session_id`.
162
+ This initiates a new authentication flow for the specified user and service.
170
163
 
171
164
  LLM Guidance:
172
165
  - Use this tool when you need to authenticate a user for a specific Google service (e.g., "Google Calendar", "Google Docs", "Gmail", "Google Drive")
@@ -182,7 +175,6 @@ async def start_google_auth(
182
175
  Args:
183
176
  user_google_email (str): The user's full Google email address (e.g., 'example@gmail.com'). This is REQUIRED.
184
177
  service_name (str): The name of the Google service for which authentication is being requested (e.g., "Google Calendar", "Google Docs"). This is REQUIRED.
185
- mcp_session_id (Optional[str]): The active MCP session ID (automatically injected by FastMCP from the mcp-session-id header). Links the OAuth flow state to the session.
186
178
 
187
179
  Returns:
188
180
  str: A detailed message for the LLM with the authorization URL and instructions to guide the user through the authentication process.
@@ -197,15 +189,18 @@ async def start_google_auth(
197
189
  logger.error(f"[start_google_auth] {error_msg}")
198
190
  raise Exception(error_msg)
199
191
 
200
- logger.info(f"Tool 'start_google_auth' invoked for user_google_email: '{user_google_email}', service: '{service_name}', session: '{mcp_session_id}'.")
192
+ logger.info(f"Tool 'start_google_auth' invoked for user_google_email: '{user_google_email}', service: '{service_name}'.")
201
193
 
202
194
  # Ensure OAuth callback is available for current transport mode
203
195
  redirect_uri = get_oauth_redirect_uri_for_current_mode()
204
- if not ensure_oauth_callback_available(_current_transport_mode, WORKSPACE_MCP_PORT, WORKSPACE_MCP_BASE_URI):
205
- raise Exception("Failed to start OAuth callback server. Please try again.")
196
+ success, error_msg = ensure_oauth_callback_available(_current_transport_mode, WORKSPACE_MCP_PORT, WORKSPACE_MCP_BASE_URI)
197
+ if not success:
198
+ if error_msg:
199
+ raise Exception(f"Failed to start OAuth callback server: {error_msg}")
200
+ else:
201
+ raise Exception("Failed to start OAuth callback server. Please try again.")
206
202
 
207
203
  auth_result = await start_auth_flow(
208
- mcp_session_id=mcp_session_id,
209
204
  user_google_email=user_google_email,
210
205
  service_name=service_name,
211
206
  redirect_uri=redirect_uri
@@ -122,9 +122,11 @@ async def get_events(
122
122
  time_min: Optional[str] = None,
123
123
  time_max: Optional[str] = None,
124
124
  max_results: int = 25,
125
+ query: Optional[str] = None,
125
126
  ) -> str:
126
127
  """
127
128
  Retrieves a list of events from a specified Google Calendar within a given time range.
129
+ You can also search for events by keyword by supplying the optional "query" param.
128
130
 
129
131
  Args:
130
132
  user_google_email (str): The user's Google email address. Required.
@@ -132,12 +134,13 @@ async def get_events(
132
134
  time_min (Optional[str]): The start of the time range (inclusive) in RFC3339 format (e.g., '2024-05-12T10:00:00Z' or '2024-05-12'). If omitted, defaults to the current time.
133
135
  time_max (Optional[str]): The end of the time range (exclusive) in RFC3339 format. If omitted, events starting from `time_min` onwards are considered (up to `max_results`).
134
136
  max_results (int): The maximum number of events to return. Defaults to 25.
137
+ query (Optional[str]): A keyword to search for within event fields (summary, description, location).
135
138
 
136
139
  Returns:
137
140
  str: A formatted list of events (summary, start and end times, link) within the specified range.
138
141
  """
139
142
  logger.info(
140
- f"[get_events] Raw time parameters - time_min: '{time_min}', time_max: '{time_max}'"
143
+ f"[get_events] Raw time parameters - time_min: '{time_min}', time_max: '{time_max}', query: '{query}'"
141
144
  )
142
145
 
143
146
  # Ensure time_min and time_max are correctly formatted for the API
@@ -161,19 +164,25 @@ async def get_events(
161
164
  )
162
165
 
163
166
  logger.info(
164
- f"[get_events] Final API parameters - calendarId: '{calendar_id}', timeMin: '{effective_time_min}', timeMax: '{effective_time_max}', maxResults: {max_results}"
167
+ f"[get_events] Final API parameters - calendarId: '{calendar_id}', timeMin: '{effective_time_min}', timeMax: '{effective_time_max}', maxResults: {max_results}, query: '{query}'"
165
168
  )
166
169
 
170
+ # Build the request parameters dynamically
171
+ request_params = {
172
+ "calendarId": calendar_id,
173
+ "timeMin": effective_time_min,
174
+ "timeMax": effective_time_max,
175
+ "maxResults": max_results,
176
+ "singleEvents": True,
177
+ "orderBy": "startTime",
178
+ }
179
+
180
+ if query:
181
+ request_params["q"] = query
182
+
167
183
  events_result = await asyncio.to_thread(
168
184
  lambda: service.events()
169
- .list(
170
- calendarId=calendar_id,
171
- timeMin=effective_time_min,
172
- timeMax=effective_time_max,
173
- maxResults=max_results,
174
- singleEvents=True,
175
- orderBy="startTime",
176
- )
185
+ .list(**request_params)
177
186
  .execute()
178
187
  )
179
188
  items = events_result.get("items", [])
@@ -149,10 +149,14 @@ def main():
149
149
  safe_print("🚀 Starting server in stdio mode")
150
150
  # Start minimal OAuth callback server for stdio mode
151
151
  from auth.oauth_callback_server import ensure_oauth_callback_available
152
- if ensure_oauth_callback_available('stdio', port, base_uri):
152
+ success, error_msg = ensure_oauth_callback_available('stdio', port, base_uri)
153
+ if success:
153
154
  safe_print(f" OAuth callback server started on {base_uri}:{port}/oauth2callback")
154
155
  else:
155
- safe_print(" ⚠️ Warning: Failed to start OAuth callback server")
156
+ warning_msg = f" ⚠️ Warning: Failed to start OAuth callback server"
157
+ if error_msg:
158
+ warning_msg += f": {error_msg}"
159
+ safe_print(warning_msg)
156
160
 
157
161
  safe_print(" Ready for MCP connections!")
158
162
  safe_print("")
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "workspace-mcp"
7
- version = "1.1.10"
7
+ version = "1.1.13"
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"]
11
- requires-python = ">=3.11"
11
+ requires-python = ">=3.10"
12
12
  dependencies = [
13
13
  "fastapi>=0.115.12",
14
14
  "fastmcp>=2.3.3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workspace-mcp
3
- Version: 1.1.10
3
+ Version: 1.1.13
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
@@ -27,7 +27,7 @@ Classifier: Topic :: Communications :: Chat
27
27
  Classifier: Topic :: Office/Business
28
28
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
29
29
  Classifier: Typing :: Typed
30
- Requires-Python: >=3.11
30
+ Requires-Python: >=3.10
31
31
  Description-Content-Type: text/markdown
32
32
  License-File: LICENSE
33
33
  Requires-Dist: fastapi>=0.115.12
@@ -46,7 +46,7 @@ Dynamic: license-file
46
46
  # Google Workspace MCP Server <img src="https://github.com/user-attachments/assets/b89524e4-6e6e-49e6-ba77-00d6df0c6e5c" width="80" align="right" />
47
47
 
48
48
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
49
- [![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
49
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
50
50
  [![PyPI](https://img.shields.io/pypi/v/workspace-mcp.svg)](https://pypi.org/project/workspace-mcp/)
51
51
  [![PyPI Downloads](https://static.pepy.tech/badge/workspace-mcp/month)](https://pepy.tech/projects/workspace-mcp)
52
52
  [![Website](https://img.shields.io/badge/Website-workspacemcp.com-green.svg)](https://workspacemcp.com)
@@ -151,7 +151,7 @@ If you’re developing, deploying to servers, or using another MCP-capable clien
151
151
  #### Instant CLI (uvx)
152
152
 
153
153
  ```bash
154
- # Requires Python 3.11+ and uvx
154
+ # Requires Python 3.10+ and uvx
155
155
  export GOOGLE_OAUTH_CLIENT_ID="xxx"
156
156
  export GOOGLE_OAUTH_CLIENT_SECRET="yyy"
157
157
  uvx workspace-mcp --tools gmail drive calendar
@@ -174,7 +174,7 @@ uvx workspace-mcp --tools gmail drive calendar tasks
174
174
  uvx workspace-mcp --transport streamable-http
175
175
  ```
176
176
 
177
- *Requires Python 3.11+ and [uvx](https://github.com/astral-sh/uv). The package is available on [PyPI](https://pypi.org/project/workspace-mcp).*
177
+ *Requires Python 3.10+ and [uvx](https://github.com/astral-sh/uv). The package is available on [PyPI](https://pypi.org/project/workspace-mcp).*
178
178
 
179
179
  ### Development Installation
180
180
 
@@ -188,7 +188,7 @@ uv run main.py
188
188
 
189
189
  ### Prerequisites
190
190
 
191
- - **Python 3.11+**
191
+ - **Python 3.10+**
192
192
  - **[uvx](https://github.com/astral-sh/uv)** (for instant installation) or [uv](https://github.com/astral-sh/uv) (for development)
193
193
  - **Google Cloud Project** with OAuth 2.0 credentials
194
194
 
@@ -524,6 +524,14 @@ async def your_new_tool(service, param1: str, param2: int = 10):
524
524
 
525
525
  To use this server as a tool provider within Open WebUI:
526
526
 
527
+ ### Instant Start (No Config Needed)
528
+ Just copy and paste the below, set your values and you're off!
529
+ ```bash
530
+ GOOGLE_OAUTH_CLIENT_ID="your_client_id" GOOGLE_OAUTH_CLIENT_SECRET="your_client_secret" uvx mcpo --port 8000 --api-key "top-secret" -- uvx workspace-mcp
531
+ ```
532
+
533
+ Otherwise:
534
+
527
535
  ### 1. Create MCPO Configuration
528
536
 
529
537
  Create a file named `config.json` with the following structure to have `mcpo` make the streamable HTTP endpoint available as an OpenAPI spec tool:
File without changes
File without changes