workspace-mcp 1.1.10__py3-none-any.whl → 1.1.13__py3-none-any.whl
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.
- auth/google_auth.py +13 -15
- auth/oauth_callback_server.py +27 -27
- auth/scopes.py +0 -5
- core/server.py +12 -17
- gcalendar/calendar_tools.py +19 -10
- main.py +6 -2
- {workspace_mcp-1.1.10.dist-info → workspace_mcp-1.1.13.dist-info}/METADATA +14 -6
- {workspace_mcp-1.1.10.dist-info → workspace_mcp-1.1.13.dist-info}/RECORD +12 -12
- {workspace_mcp-1.1.10.dist-info → workspace_mcp-1.1.13.dist-info}/WHEEL +0 -0
- {workspace_mcp-1.1.10.dist-info → workspace_mcp-1.1.13.dist-info}/entry_points.txt +0 -0
- {workspace_mcp-1.1.10.dist-info → workspace_mcp-1.1.13.dist-info}/licenses/LICENSE +0 -0
- {workspace_mcp-1.1.10.dist-info → workspace_mcp-1.1.13.dist-info}/top_level.txt +0 -0
auth/google_auth.py
CHANGED
@@ -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
|
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}
|
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,
|
auth/oauth_callback_server.py
CHANGED
@@ -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
|
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
|
-
|
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=
|
79
|
+
session_id=None
|
84
80
|
)
|
85
81
|
|
86
|
-
|
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
|
-
|
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
|
-
|
121
|
-
|
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
|
-
|
159
|
-
|
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
|
-
|
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
|
-
|
241
|
-
if
|
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
|
-
|
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
|
-
|
252
|
-
|
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."""
|
auth/scopes.py
CHANGED
@@ -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'
|
core/server.py
CHANGED
@@ -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
|
-
|
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=
|
139
|
+
session_id=None # Session ID tracking removed
|
145
140
|
)
|
146
141
|
|
147
|
-
|
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
|
-
|
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}'
|
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
|
-
|
205
|
-
|
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
|
gcalendar/calendar_tools.py
CHANGED
@@ -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", [])
|
main.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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("")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: workspace-mcp
|
3
|
-
Version: 1.1.
|
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.
|
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
|
[](https://opensource.org/licenses/MIT)
|
49
|
-
[](https://www.python.org/downloads/)
|
50
50
|
[](https://pypi.org/project/workspace-mcp/)
|
51
51
|
[](https://pepy.tech/projects/workspace-mcp)
|
52
52
|
[](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.
|
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.
|
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.
|
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:
|
@@ -1,9 +1,9 @@
|
|
1
|
-
main.py,sha256=
|
1
|
+
main.py,sha256=QnpxBnYTqrLaEkO5sqkIeVjiJsvQ9PqnIPk9HiaCs0Y,7765
|
2
2
|
auth/__init__.py,sha256=gPCU3GE-SLy91S3D3CbX-XfKBm6hteK_VSPKx7yjT5s,42
|
3
|
-
auth/google_auth.py,sha256=
|
4
|
-
auth/oauth_callback_server.py,sha256=
|
3
|
+
auth/google_auth.py,sha256=KJjwEgeysKTiqymf_zkx6Z4YIWo94gdSG7aFsZ_G2JQ,32942
|
4
|
+
auth/oauth_callback_server.py,sha256=mUgfE4OqftTbXfWvKJmrJ3PjCxz8HZ5nvJW3Emk7_20,9630
|
5
5
|
auth/oauth_responses.py,sha256=qbirSB4d7mBRKcJKqGLrJxRAPaLHqObf9t-VMAq6UKA,7020
|
6
|
-
auth/scopes.py,sha256=
|
6
|
+
auth/scopes.py,sha256=QdeoTq7MtWRcf5G9CqbpEUI5yVbu7JriCtY7zupGVeY,3331
|
7
7
|
auth/service_decorator.py,sha256=crHax8t3EL4DXLPg4i3h9YHyfdpegRex880yNBTNqqI,15795
|
8
8
|
auth/oauth21/__init__.py,sha256=5SiZ9_fISDlCaNKIE0yRy7GaaEZRJlNy034YalgkPuI,2592
|
9
9
|
auth/oauth21/compat.py,sha256=PnpYmJrnUqd_XHOjT-CV6QeRwCGl-lKdStu0riciKfM,15830
|
@@ -20,10 +20,10 @@ auth/oauth21/tokens.py,sha256=0BzYU0vdSXq8LiUpVQ8nncZ8UnvjAhXEVw0iY8NYIhs,14745
|
|
20
20
|
core/__init__.py,sha256=AHVKdPl6v4lUFm2R-KuGuAgEmCyfxseMeLGtntMcqCs,43
|
21
21
|
core/comments.py,sha256=ZiPxdZOsQYhxFwkClCDlcefP0iyLvO23qvN7OPLGSPk,11119
|
22
22
|
core/context.py,sha256=zNgPXf9EO2EMs9sQkfKiywoy6sEOksVNgOrJMA_c30Y,768
|
23
|
-
core/server.py,sha256=
|
23
|
+
core/server.py,sha256=GslrWM4P6bK5vOrcmT8qZtwQe5JnpSjA3xC1X7c41J4,9075
|
24
24
|
core/utils.py,sha256=sebbgJtzgmtoj-5PNgAvKEjmPtIKmtMY6b-faVf4Oec,13101
|
25
25
|
gcalendar/__init__.py,sha256=D5fSdAwbeomoaj7XAdxSnIy-NVKNkpExs67175bOtfc,46
|
26
|
-
gcalendar/calendar_tools.py,sha256=
|
26
|
+
gcalendar/calendar_tools.py,sha256=44RLehBqiCnuSWM50T0ZPnx06Xmbsu2bbUot-2WJ_qc,22562
|
27
27
|
gchat/__init__.py,sha256=XBjH4SbtULfZHgFCxk3moel5XqG599HCgZWl_veIncg,88
|
28
28
|
gchat/chat_tools.py,sha256=89Gh_6-r8a6CJJjL43CqAmOB0POogzNLMJry4-U-KR4,7252
|
29
29
|
gdocs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -40,9 +40,9 @@ gslides/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
40
|
gslides/slides_tools.py,sha256=vbhdzqHtNHfkNXN3h2_E9j_rMc747OzUaXH5XBu6XQY,10017
|
41
41
|
gtasks/__init__.py,sha256=qwOWUzQbkYLSBrdhCqEkAWPH2lEOljk1mLtrlab9YZc,107
|
42
42
|
gtasks/tasks_tools.py,sha256=s95fEMI7-TxyP8K2JoDMiRWftnVE9Fkrrr4j6jVgxAE,26469
|
43
|
-
workspace_mcp-1.1.
|
44
|
-
workspace_mcp-1.1.
|
45
|
-
workspace_mcp-1.1.
|
46
|
-
workspace_mcp-1.1.
|
47
|
-
workspace_mcp-1.1.
|
48
|
-
workspace_mcp-1.1.
|
43
|
+
workspace_mcp-1.1.13.dist-info/licenses/LICENSE,sha256=bB8L7rIyRy5o-WHxGgvRuY8hUTzNu4h3DTkvyV8XFJo,1070
|
44
|
+
workspace_mcp-1.1.13.dist-info/METADATA,sha256=HbbexOvBqcxjgOzfqWA0Rj8rfWKzKbQRl0mnHcdRQD0,23846
|
45
|
+
workspace_mcp-1.1.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
46
|
+
workspace_mcp-1.1.13.dist-info/entry_points.txt,sha256=kPiEfOTuf-ptDM0Rf2OlyrFudGW7hCZGg4MCn2Foxs4,44
|
47
|
+
workspace_mcp-1.1.13.dist-info/top_level.txt,sha256=uAg7uV2mETWYRw5g80XtO1lhxVO1sY6_IihNdG_4n24,80
|
48
|
+
workspace_mcp-1.1.13.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|