workspace-mcp 1.0.2__py3-none-any.whl → 1.0.4__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 +120 -12
- auth/oauth_callback_server.py +7 -3
- core/comments.py +257 -0
- core/context.py +22 -0
- core/server.py +7 -8
- gdocs/docs_tools.py +11 -0
- gsheets/sheets_tools.py +11 -0
- gslides/slides_tools.py +16 -1
- {workspace_mcp-1.0.2.dist-info → workspace_mcp-1.0.4.dist-info}/METADATA +78 -11
- {workspace_mcp-1.0.2.dist-info → workspace_mcp-1.0.4.dist-info}/RECORD +14 -12
- {workspace_mcp-1.0.2.dist-info → workspace_mcp-1.0.4.dist-info}/WHEEL +0 -0
- {workspace_mcp-1.0.2.dist-info → workspace_mcp-1.0.4.dist-info}/entry_points.txt +0 -0
- {workspace_mcp-1.0.2.dist-info → workspace_mcp-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {workspace_mcp-1.0.2.dist-info → workspace_mcp-1.0.4.dist-info}/top_level.txt +0 -0
auth/google_auth.py
CHANGED
@@ -27,7 +27,7 @@ DEFAULT_CREDENTIALS_DIR = ".credentials"
|
|
27
27
|
# This should be more robust in a production system once OAuth2.1 is implemented in client.
|
28
28
|
_SESSION_CREDENTIALS_CACHE: Dict[str, Credentials] = {}
|
29
29
|
# Centralized Client Secrets Path Logic
|
30
|
-
_client_secrets_env = os.getenv("GOOGLE_CLIENT_SECRETS")
|
30
|
+
_client_secrets_env = os.getenv("GOOGLE_CLIENT_SECRET_PATH") or os.getenv("GOOGLE_CLIENT_SECRETS")
|
31
31
|
if _client_secrets_env:
|
32
32
|
CONFIG_CLIENT_SECRETS_PATH = _client_secrets_env
|
33
33
|
else:
|
@@ -151,22 +151,128 @@ def load_credentials_from_session(session_id: str) -> Optional[Credentials]:
|
|
151
151
|
logger.debug(f"No credentials found in session cache for session_id: {session_id}")
|
152
152
|
return credentials
|
153
153
|
|
154
|
+
def load_client_secrets_from_env() -> Optional[Dict[str, Any]]:
|
155
|
+
"""
|
156
|
+
Loads the client secrets from environment variables.
|
157
|
+
|
158
|
+
Environment variables used:
|
159
|
+
- GOOGLE_OAUTH_CLIENT_ID: OAuth 2.0 client ID
|
160
|
+
- GOOGLE_OAUTH_CLIENT_SECRET: OAuth 2.0 client secret
|
161
|
+
- GOOGLE_OAUTH_REDIRECT_URI: (optional) OAuth redirect URI
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
Client secrets configuration dict compatible with Google OAuth library,
|
165
|
+
or None if required environment variables are not set.
|
166
|
+
"""
|
167
|
+
client_id = os.getenv("GOOGLE_OAUTH_CLIENT_ID")
|
168
|
+
client_secret = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET")
|
169
|
+
redirect_uri = os.getenv("GOOGLE_OAUTH_REDIRECT_URI")
|
170
|
+
|
171
|
+
if client_id and client_secret:
|
172
|
+
# Create config structure that matches Google client secrets format
|
173
|
+
web_config = {
|
174
|
+
"client_id": client_id,
|
175
|
+
"client_secret": client_secret,
|
176
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
177
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
178
|
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs"
|
179
|
+
}
|
180
|
+
|
181
|
+
# Add redirect_uri if provided via environment variable
|
182
|
+
if redirect_uri:
|
183
|
+
web_config["redirect_uris"] = [redirect_uri]
|
184
|
+
|
185
|
+
# Return the full config structure expected by Google OAuth library
|
186
|
+
config = {"web": web_config}
|
187
|
+
|
188
|
+
logger.info("Loaded OAuth client credentials from environment variables")
|
189
|
+
return config
|
190
|
+
|
191
|
+
logger.debug("OAuth client credentials not found in environment variables")
|
192
|
+
return None
|
193
|
+
|
154
194
|
def load_client_secrets(client_secrets_path: str) -> Dict[str, Any]:
|
155
|
-
"""
|
195
|
+
"""
|
196
|
+
Loads the client secrets from environment variables (preferred) or from the client secrets file.
|
197
|
+
|
198
|
+
Priority order:
|
199
|
+
1. Environment variables (GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET)
|
200
|
+
2. File-based credentials at the specified path
|
201
|
+
|
202
|
+
Args:
|
203
|
+
client_secrets_path: Path to the client secrets JSON file (used as fallback)
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
Client secrets configuration dict
|
207
|
+
|
208
|
+
Raises:
|
209
|
+
ValueError: If client secrets file has invalid format
|
210
|
+
IOError: If file cannot be read and no environment variables are set
|
211
|
+
"""
|
212
|
+
# First, try to load from environment variables
|
213
|
+
env_config = load_client_secrets_from_env()
|
214
|
+
if env_config:
|
215
|
+
# Extract the "web" config from the environment structure
|
216
|
+
return env_config["web"]
|
217
|
+
|
218
|
+
# Fall back to loading from file
|
156
219
|
try:
|
157
220
|
with open(client_secrets_path, 'r') as f:
|
158
221
|
client_config = json.load(f)
|
159
222
|
# The file usually contains a top-level key like "web" or "installed"
|
160
223
|
if "web" in client_config:
|
224
|
+
logger.info(f"Loaded OAuth client credentials from file: {client_secrets_path}")
|
161
225
|
return client_config["web"]
|
162
226
|
elif "installed" in client_config:
|
163
|
-
|
227
|
+
logger.info(f"Loaded OAuth client credentials from file: {client_secrets_path}")
|
228
|
+
return client_config["installed"]
|
164
229
|
else:
|
165
230
|
logger.error(f"Client secrets file {client_secrets_path} has unexpected format.")
|
166
231
|
raise ValueError("Invalid client secrets file format")
|
167
232
|
except (IOError, json.JSONDecodeError) as e:
|
168
233
|
logger.error(f"Error loading client secrets file {client_secrets_path}: {e}")
|
169
234
|
raise
|
235
|
+
def check_client_secrets() -> Optional[str]:
|
236
|
+
"""
|
237
|
+
Checks for the presence of OAuth client secrets, either as environment
|
238
|
+
variables or as a file.
|
239
|
+
|
240
|
+
Returns:
|
241
|
+
An error message string if secrets are not found, otherwise None.
|
242
|
+
"""
|
243
|
+
env_config = load_client_secrets_from_env()
|
244
|
+
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}")
|
246
|
+
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
|
+
return None
|
248
|
+
|
249
|
+
def create_oauth_flow(scopes: List[str], redirect_uri: str, state: Optional[str] = None) -> Flow:
|
250
|
+
"""Creates an OAuth flow using environment variables or client secrets file."""
|
251
|
+
# Try environment variables first
|
252
|
+
env_config = load_client_secrets_from_env()
|
253
|
+
if env_config:
|
254
|
+
# Use client config directly
|
255
|
+
flow = Flow.from_client_config(
|
256
|
+
env_config,
|
257
|
+
scopes=scopes,
|
258
|
+
redirect_uri=redirect_uri,
|
259
|
+
state=state
|
260
|
+
)
|
261
|
+
logger.debug("Created OAuth flow from environment variables")
|
262
|
+
return flow
|
263
|
+
|
264
|
+
# Fall back to file-based config
|
265
|
+
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")
|
267
|
+
|
268
|
+
flow = Flow.from_client_secrets_file(
|
269
|
+
CONFIG_CLIENT_SECRETS_PATH,
|
270
|
+
scopes=scopes,
|
271
|
+
redirect_uri=redirect_uri,
|
272
|
+
state=state
|
273
|
+
)
|
274
|
+
logger.debug(f"Created OAuth flow from client secrets file: {CONFIG_CLIENT_SECRETS_PATH}")
|
275
|
+
return flow
|
170
276
|
|
171
277
|
# --- Core OAuth Logic ---
|
172
278
|
|
@@ -206,8 +312,7 @@ async def start_auth_flow(
|
|
206
312
|
OAUTH_STATE_TO_SESSION_ID_MAP[oauth_state] = mcp_session_id
|
207
313
|
logger.info(f"[start_auth_flow] Stored mcp_session_id '{mcp_session_id}' for oauth_state '{oauth_state}'.")
|
208
314
|
|
209
|
-
flow =
|
210
|
-
CONFIG_CLIENT_SECRETS_PATH, # Use module constant
|
315
|
+
flow = create_oauth_flow(
|
211
316
|
scopes=SCOPES, # Use global SCOPES
|
212
317
|
redirect_uri=redirect_uri, # Use passed redirect_uri
|
213
318
|
state=oauth_state
|
@@ -240,7 +345,7 @@ async def start_auth_flow(
|
|
240
345
|
return "\n".join(message_lines)
|
241
346
|
|
242
347
|
except FileNotFoundError as e:
|
243
|
-
error_text = f"OAuth client
|
348
|
+
error_text = f"OAuth client credentials not found: {e}. Please either:\n1. Set environment variables: GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET\n2. Ensure '{CONFIG_CLIENT_SECRETS_PATH}' file exists"
|
244
349
|
logger.error(error_text, exc_info=True)
|
245
350
|
raise Exception(error_text)
|
246
351
|
except Exception as e:
|
@@ -249,12 +354,12 @@ async def start_auth_flow(
|
|
249
354
|
raise Exception(error_text)
|
250
355
|
|
251
356
|
def handle_auth_callback(
|
252
|
-
client_secrets_path: str,
|
253
357
|
scopes: List[str],
|
254
358
|
authorization_response: str,
|
255
|
-
redirect_uri: str,
|
359
|
+
redirect_uri: str,
|
256
360
|
credentials_base_dir: str = DEFAULT_CREDENTIALS_DIR,
|
257
|
-
session_id: Optional[str] = None
|
361
|
+
session_id: Optional[str] = None,
|
362
|
+
client_secrets_path: Optional[str] = None # Deprecated: kept for backward compatibility
|
258
363
|
) -> Tuple[str, Credentials]:
|
259
364
|
"""
|
260
365
|
Handles the callback from Google, exchanges the code for credentials,
|
@@ -262,12 +367,12 @@ def handle_auth_callback(
|
|
262
367
|
and returns them.
|
263
368
|
|
264
369
|
Args:
|
265
|
-
client_secrets_path: Path to the Google client secrets JSON file.
|
266
370
|
scopes: List of OAuth scopes requested.
|
267
371
|
authorization_response: The full callback URL from Google.
|
268
372
|
redirect_uri: The redirect URI.
|
269
373
|
credentials_base_dir: Base directory for credential files.
|
270
374
|
session_id: Optional MCP session ID to associate with the credentials.
|
375
|
+
client_secrets_path: (Deprecated) Path to client secrets file. Ignored if environment variables are set.
|
271
376
|
|
272
377
|
Returns:
|
273
378
|
A tuple containing the user_google_email and the obtained Credentials object.
|
@@ -278,13 +383,16 @@ def handle_auth_callback(
|
|
278
383
|
HttpError: If fetching user info fails.
|
279
384
|
"""
|
280
385
|
try:
|
386
|
+
# Log deprecation warning if old parameter is used
|
387
|
+
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.")
|
389
|
+
|
281
390
|
# Allow HTTP for localhost in development
|
282
391
|
if 'OAUTHLIB_INSECURE_TRANSPORT' not in os.environ:
|
283
392
|
logger.warning("OAUTHLIB_INSECURE_TRANSPORT not set. Setting it for localhost development.")
|
284
393
|
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
|
285
394
|
|
286
|
-
flow =
|
287
|
-
client_secrets_path,
|
395
|
+
flow = create_oauth_flow(
|
288
396
|
scopes=scopes,
|
289
397
|
redirect_uri=redirect_uri
|
290
398
|
)
|
auth/oauth_callback_server.py
CHANGED
@@ -15,7 +15,7 @@ import socket
|
|
15
15
|
from fastapi import FastAPI, Request
|
16
16
|
import uvicorn
|
17
17
|
|
18
|
-
from auth.google_auth import handle_auth_callback,
|
18
|
+
from auth.google_auth import handle_auth_callback, check_client_secrets
|
19
19
|
from auth.scopes import OAUTH_STATE_TO_SESSION_ID_MAP, SCOPES
|
20
20
|
from auth.oauth_responses import create_error_response, create_success_response, create_server_error_response
|
21
21
|
|
@@ -59,6 +59,11 @@ class MinimalOAuthServer:
|
|
59
59
|
return create_error_response(error_message)
|
60
60
|
|
61
61
|
try:
|
62
|
+
# Check if we have credentials available (environment variables or file)
|
63
|
+
error_message = check_client_secrets()
|
64
|
+
if error_message:
|
65
|
+
return create_server_error_response(error_message)
|
66
|
+
|
62
67
|
logger.info(f"OAuth callback: Received code (state: {state}). Attempting to exchange for tokens.")
|
63
68
|
|
64
69
|
mcp_session_id: Optional[str] = OAUTH_STATE_TO_SESSION_ID_MAP.pop(state, None)
|
@@ -69,7 +74,6 @@ class MinimalOAuthServer:
|
|
69
74
|
|
70
75
|
# Exchange code for credentials
|
71
76
|
verified_user_id, credentials = handle_auth_callback(
|
72
|
-
client_secrets_path=CONFIG_CLIENT_SECRETS_PATH,
|
73
77
|
scopes=SCOPES,
|
74
78
|
authorization_response=str(request.url),
|
75
79
|
redirect_uri=f"{self.base_uri}:{self.port}/oauth2callback",
|
@@ -106,7 +110,7 @@ class MinimalOAuthServer:
|
|
106
110
|
hostname = parsed_uri.hostname or 'localhost'
|
107
111
|
except Exception:
|
108
112
|
hostname = 'localhost'
|
109
|
-
|
113
|
+
|
110
114
|
try:
|
111
115
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
112
116
|
s.bind((hostname, self.port))
|
core/comments.py
ADDED
@@ -0,0 +1,257 @@
|
|
1
|
+
"""
|
2
|
+
Core Comments Module
|
3
|
+
|
4
|
+
This module provides reusable comment management functions for Google Workspace applications.
|
5
|
+
All Google Workspace apps (Docs, Sheets, Slides) use the Drive API for comment operations.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
import asyncio
|
10
|
+
from typing import Dict, Any
|
11
|
+
|
12
|
+
from mcp import types
|
13
|
+
from googleapiclient.errors import HttpError
|
14
|
+
|
15
|
+
from auth.service_decorator import require_google_service
|
16
|
+
from core.server import server
|
17
|
+
from core.utils import handle_http_errors
|
18
|
+
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
|
21
|
+
|
22
|
+
def create_comment_tools(app_name: str, file_id_param: str):
|
23
|
+
"""
|
24
|
+
Factory function to create comment management tools for a specific Google Workspace app.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
app_name: Name of the app (e.g., "document", "spreadsheet", "presentation")
|
28
|
+
file_id_param: Parameter name for the file ID (e.g., "document_id", "spreadsheet_id", "presentation_id")
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
Dict containing the four comment management functions with unique names
|
32
|
+
"""
|
33
|
+
|
34
|
+
# Create unique function names based on the app type
|
35
|
+
read_func_name = f"read_{app_name}_comments"
|
36
|
+
create_func_name = f"create_{app_name}_comment"
|
37
|
+
reply_func_name = f"reply_to_{app_name}_comment"
|
38
|
+
resolve_func_name = f"resolve_{app_name}_comment"
|
39
|
+
|
40
|
+
# Create read comments function
|
41
|
+
if file_id_param == "document_id":
|
42
|
+
@server.tool()
|
43
|
+
@require_google_service("drive", "drive_read")
|
44
|
+
@handle_http_errors(read_func_name)
|
45
|
+
async def read_comments(service, user_google_email: str, document_id: str) -> str:
|
46
|
+
"""Read all comments from a Google Slide, Sheet or Doc."""
|
47
|
+
return await _read_comments_impl(service, app_name, document_id)
|
48
|
+
|
49
|
+
@server.tool()
|
50
|
+
@require_google_service("drive", "drive_file")
|
51
|
+
@handle_http_errors(create_func_name)
|
52
|
+
async def create_comment(service, user_google_email: str, document_id: str, comment_content: str) -> str:
|
53
|
+
"""Create a new comment on a Google Slide, Sheet or Doc."""
|
54
|
+
return await _create_comment_impl(service, app_name, document_id, comment_content)
|
55
|
+
|
56
|
+
@server.tool()
|
57
|
+
@require_google_service("drive", "drive_file")
|
58
|
+
@handle_http_errors(reply_func_name)
|
59
|
+
async def reply_to_comment(service, user_google_email: str, document_id: str, comment_id: str, reply_content: str) -> str:
|
60
|
+
"""Reply to a specific comment in a Google Document."""
|
61
|
+
return await _reply_to_comment_impl(service, app_name, document_id, comment_id, reply_content)
|
62
|
+
|
63
|
+
@server.tool()
|
64
|
+
@require_google_service("drive", "drive_file")
|
65
|
+
@handle_http_errors(resolve_func_name)
|
66
|
+
async def resolve_comment(service, user_google_email: str, document_id: str, comment_id: str) -> str:
|
67
|
+
"""Resolve a comment in a Google Slide, Sheet or Doc."""
|
68
|
+
return await _resolve_comment_impl(service, app_name, document_id, comment_id)
|
69
|
+
|
70
|
+
elif file_id_param == "spreadsheet_id":
|
71
|
+
@server.tool()
|
72
|
+
@require_google_service("drive", "drive_read")
|
73
|
+
@handle_http_errors(read_func_name)
|
74
|
+
async def read_comments(service, user_google_email: str, spreadsheet_id: str) -> str:
|
75
|
+
"""Read all comments from a Google Slide, Sheet or Doc."""
|
76
|
+
return await _read_comments_impl(service, app_name, spreadsheet_id)
|
77
|
+
|
78
|
+
@server.tool()
|
79
|
+
@require_google_service("drive", "drive_file")
|
80
|
+
@handle_http_errors(create_func_name)
|
81
|
+
async def create_comment(service, user_google_email: str, spreadsheet_id: str, comment_content: str) -> str:
|
82
|
+
"""Create a new comment on a Google Slide, Sheet or Doc."""
|
83
|
+
return await _create_comment_impl(service, app_name, spreadsheet_id, comment_content)
|
84
|
+
|
85
|
+
@server.tool()
|
86
|
+
@require_google_service("drive", "drive_file")
|
87
|
+
@handle_http_errors(reply_func_name)
|
88
|
+
async def reply_to_comment(service, user_google_email: str, spreadsheet_id: str, comment_id: str, reply_content: str) -> str:
|
89
|
+
"""Reply to a specific comment in a Google Slide, Sheet or Doc."""
|
90
|
+
return await _reply_to_comment_impl(service, app_name, spreadsheet_id, comment_id, reply_content)
|
91
|
+
|
92
|
+
@server.tool()
|
93
|
+
@require_google_service("drive", "drive_file")
|
94
|
+
@handle_http_errors(resolve_func_name)
|
95
|
+
async def resolve_comment(service, user_google_email: str, spreadsheet_id: str, comment_id: str) -> str:
|
96
|
+
"""Resolve a comment in a Google Slide, Sheet or Doc."""
|
97
|
+
return await _resolve_comment_impl(service, app_name, spreadsheet_id, comment_id)
|
98
|
+
|
99
|
+
elif file_id_param == "presentation_id":
|
100
|
+
@server.tool()
|
101
|
+
@require_google_service("drive", "drive_read")
|
102
|
+
@handle_http_errors(read_func_name)
|
103
|
+
async def read_comments(service, user_google_email: str, presentation_id: str) -> str:
|
104
|
+
"""Read all comments from a Google Slide, Sheet or Doc."""
|
105
|
+
return await _read_comments_impl(service, app_name, presentation_id)
|
106
|
+
|
107
|
+
@server.tool()
|
108
|
+
@require_google_service("drive", "drive_file")
|
109
|
+
@handle_http_errors(create_func_name)
|
110
|
+
async def create_comment(service, user_google_email: str, presentation_id: str, comment_content: str) -> str:
|
111
|
+
"""Create a new comment on a Google Slide, Sheet or Doc."""
|
112
|
+
return await _create_comment_impl(service, app_name, presentation_id, comment_content)
|
113
|
+
|
114
|
+
@server.tool()
|
115
|
+
@require_google_service("drive", "drive_file")
|
116
|
+
@handle_http_errors(reply_func_name)
|
117
|
+
async def reply_to_comment(service, user_google_email: str, presentation_id: str, comment_id: str, reply_content: str) -> str:
|
118
|
+
"""Reply to a specific comment in a Google Slide, Sheet or Doc."""
|
119
|
+
return await _reply_to_comment_impl(service, app_name, presentation_id, comment_id, reply_content)
|
120
|
+
|
121
|
+
@server.tool()
|
122
|
+
@require_google_service("drive", "drive_file")
|
123
|
+
@handle_http_errors(resolve_func_name)
|
124
|
+
async def resolve_comment(service, user_google_email: str, presentation_id: str, comment_id: str) -> str:
|
125
|
+
"""Resolve a comment in a Google Slide, Sheet or Doc."""
|
126
|
+
return await _resolve_comment_impl(service, app_name, presentation_id, comment_id)
|
127
|
+
|
128
|
+
# Set the proper function names for MCP registration
|
129
|
+
read_comments.__name__ = read_func_name
|
130
|
+
create_comment.__name__ = create_func_name
|
131
|
+
reply_to_comment.__name__ = reply_func_name
|
132
|
+
resolve_comment.__name__ = resolve_func_name
|
133
|
+
|
134
|
+
return {
|
135
|
+
'read_comments': read_comments,
|
136
|
+
'create_comment': create_comment,
|
137
|
+
'reply_to_comment': reply_to_comment,
|
138
|
+
'resolve_comment': resolve_comment
|
139
|
+
}
|
140
|
+
|
141
|
+
|
142
|
+
async def _read_comments_impl(service, app_name: str, file_id: str) -> str:
|
143
|
+
"""Implementation for reading comments from any Google Workspace file."""
|
144
|
+
logger.info(f"[read_{app_name}_comments] Reading comments for {app_name} {file_id}")
|
145
|
+
|
146
|
+
response = await asyncio.to_thread(
|
147
|
+
service.comments().list(
|
148
|
+
fileId=file_id,
|
149
|
+
fields="comments(id,content,author,createdTime,modifiedTime,resolved,replies(content,author,id,createdTime,modifiedTime))"
|
150
|
+
).execute
|
151
|
+
)
|
152
|
+
|
153
|
+
comments = response.get('comments', [])
|
154
|
+
|
155
|
+
if not comments:
|
156
|
+
return f"No comments found in {app_name} {file_id}"
|
157
|
+
|
158
|
+
output = [f"Found {len(comments)} comments in {app_name} {file_id}:\\n"]
|
159
|
+
|
160
|
+
for comment in comments:
|
161
|
+
author = comment.get('author', {}).get('displayName', 'Unknown')
|
162
|
+
content = comment.get('content', '')
|
163
|
+
created = comment.get('createdTime', '')
|
164
|
+
resolved = comment.get('resolved', False)
|
165
|
+
comment_id = comment.get('id', '')
|
166
|
+
status = " [RESOLVED]" if resolved else ""
|
167
|
+
|
168
|
+
output.append(f"Comment ID: {comment_id}")
|
169
|
+
output.append(f"Author: {author}")
|
170
|
+
output.append(f"Created: {created}{status}")
|
171
|
+
output.append(f"Content: {content}")
|
172
|
+
|
173
|
+
# Add replies if any
|
174
|
+
replies = comment.get('replies', [])
|
175
|
+
if replies:
|
176
|
+
output.append(f" Replies ({len(replies)}):")
|
177
|
+
for reply in replies:
|
178
|
+
reply_author = reply.get('author', {}).get('displayName', 'Unknown')
|
179
|
+
reply_content = reply.get('content', '')
|
180
|
+
reply_created = reply.get('createdTime', '')
|
181
|
+
reply_id = reply.get('id', '')
|
182
|
+
output.append(f" Reply ID: {reply_id}")
|
183
|
+
output.append(f" Author: {reply_author}")
|
184
|
+
output.append(f" Created: {reply_created}")
|
185
|
+
output.append(f" Content: {reply_content}")
|
186
|
+
|
187
|
+
output.append("") # Empty line between comments
|
188
|
+
|
189
|
+
return "\\n".join(output)
|
190
|
+
|
191
|
+
|
192
|
+
async def _create_comment_impl(service, app_name: str, file_id: str, comment_content: str) -> str:
|
193
|
+
"""Implementation for creating a comment on any Google Workspace file."""
|
194
|
+
logger.info(f"[create_{app_name}_comment] Creating comment in {app_name} {file_id}")
|
195
|
+
|
196
|
+
body = {"content": comment_content}
|
197
|
+
|
198
|
+
comment = await asyncio.to_thread(
|
199
|
+
service.comments().create(
|
200
|
+
fileId=file_id,
|
201
|
+
body=body,
|
202
|
+
fields="id,content,author,createdTime,modifiedTime"
|
203
|
+
).execute
|
204
|
+
)
|
205
|
+
|
206
|
+
comment_id = comment.get('id', '')
|
207
|
+
author = comment.get('author', {}).get('displayName', 'Unknown')
|
208
|
+
created = comment.get('createdTime', '')
|
209
|
+
|
210
|
+
return f"Comment created successfully!\\nComment ID: {comment_id}\\nAuthor: {author}\\nCreated: {created}\\nContent: {comment_content}"
|
211
|
+
|
212
|
+
|
213
|
+
async def _reply_to_comment_impl(service, app_name: str, file_id: str, comment_id: str, reply_content: str) -> str:
|
214
|
+
"""Implementation for replying to a comment on any Google Workspace file."""
|
215
|
+
logger.info(f"[reply_to_{app_name}_comment] Replying to comment {comment_id} in {app_name} {file_id}")
|
216
|
+
|
217
|
+
body = {'content': reply_content}
|
218
|
+
|
219
|
+
reply = await asyncio.to_thread(
|
220
|
+
service.replies().create(
|
221
|
+
fileId=file_id,
|
222
|
+
commentId=comment_id,
|
223
|
+
body=body,
|
224
|
+
fields="id,content,author,createdTime,modifiedTime"
|
225
|
+
).execute
|
226
|
+
)
|
227
|
+
|
228
|
+
reply_id = reply.get('id', '')
|
229
|
+
author = reply.get('author', {}).get('displayName', 'Unknown')
|
230
|
+
created = reply.get('createdTime', '')
|
231
|
+
|
232
|
+
return f"Reply posted successfully!\\nReply ID: {reply_id}\\nAuthor: {author}\\nCreated: {created}\\nContent: {reply_content}"
|
233
|
+
|
234
|
+
|
235
|
+
async def _resolve_comment_impl(service, app_name: str, file_id: str, comment_id: str) -> str:
|
236
|
+
"""Implementation for resolving a comment on any Google Workspace file."""
|
237
|
+
logger.info(f"[resolve_{app_name}_comment] Resolving comment {comment_id} in {app_name} {file_id}")
|
238
|
+
|
239
|
+
body = {
|
240
|
+
"content": "This comment has been resolved.",
|
241
|
+
"action": "resolve"
|
242
|
+
}
|
243
|
+
|
244
|
+
reply = await asyncio.to_thread(
|
245
|
+
service.replies().create(
|
246
|
+
fileId=file_id,
|
247
|
+
commentId=comment_id,
|
248
|
+
body=body,
|
249
|
+
fields="id,content,author,createdTime,modifiedTime"
|
250
|
+
).execute
|
251
|
+
)
|
252
|
+
|
253
|
+
reply_id = reply.get('id', '')
|
254
|
+
author = reply.get('author', {}).get('displayName', 'Unknown')
|
255
|
+
created = reply.get('createdTime', '')
|
256
|
+
|
257
|
+
return f"Comment {comment_id} has been resolved successfully.\\nResolve reply ID: {reply_id}\\nAuthor: {author}\\nCreated: {created}"
|
core/context.py
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# core/context.py
|
2
|
+
import contextvars
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
# Context variable to hold injected credentials for the life of a single request.
|
6
|
+
_injected_oauth_credentials = contextvars.ContextVar(
|
7
|
+
"injected_oauth_credentials", default=None
|
8
|
+
)
|
9
|
+
|
10
|
+
def get_injected_oauth_credentials():
|
11
|
+
"""
|
12
|
+
Retrieve injected OAuth credentials for the current request context.
|
13
|
+
This is called by the authentication layer to check for request-scoped credentials.
|
14
|
+
"""
|
15
|
+
return _injected_oauth_credentials.get()
|
16
|
+
|
17
|
+
def set_injected_oauth_credentials(credentials: Optional[dict]):
|
18
|
+
"""
|
19
|
+
Set or clear the injected OAuth credentials for the current request context.
|
20
|
+
This is called by the service decorator.
|
21
|
+
"""
|
22
|
+
_injected_oauth_credentials.set(credentials)
|
core/server.py
CHANGED
@@ -11,7 +11,7 @@ from mcp import types
|
|
11
11
|
from mcp.server.fastmcp import FastMCP
|
12
12
|
from starlette.requests import Request
|
13
13
|
|
14
|
-
from auth.google_auth import handle_auth_callback, start_auth_flow,
|
14
|
+
from auth.google_auth import handle_auth_callback, start_auth_flow, check_client_secrets
|
15
15
|
from auth.oauth_callback_server import get_oauth_redirect_uri, ensure_oauth_callback_available
|
16
16
|
from auth.oauth_responses import create_error_response, create_success_response, create_server_error_response
|
17
17
|
|
@@ -58,6 +58,7 @@ logger = logging.getLogger(__name__)
|
|
58
58
|
|
59
59
|
WORKSPACE_MCP_PORT = int(os.getenv("PORT", os.getenv("WORKSPACE_MCP_PORT", 8000)))
|
60
60
|
WORKSPACE_MCP_BASE_URI = os.getenv("WORKSPACE_MCP_BASE_URI", "http://localhost")
|
61
|
+
USER_GOOGLE_EMAIL = os.getenv("USER_GOOGLE_EMAIL", None)
|
61
62
|
|
62
63
|
# Transport mode detection (will be set by main.py)
|
63
64
|
_current_transport_mode = "stdio" # Default to stdio
|
@@ -119,11 +120,10 @@ async def oauth2_callback(request: Request) -> HTMLResponse:
|
|
119
120
|
return create_error_response(error_message)
|
120
121
|
|
121
122
|
try:
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
return HTMLResponse(content="Server Configuration Error: Client secrets not found.", status_code=500)
|
123
|
+
# Check if we have credentials available (environment variables or file)
|
124
|
+
error_message = check_client_secrets()
|
125
|
+
if error_message:
|
126
|
+
return create_server_error_response(error_message)
|
127
127
|
|
128
128
|
logger.info(f"OAuth callback: Received code (state: {state}). Attempting to exchange for tokens.")
|
129
129
|
|
@@ -136,7 +136,6 @@ async def oauth2_callback(request: Request) -> HTMLResponse:
|
|
136
136
|
# Exchange code for credentials. handle_auth_callback will save them.
|
137
137
|
# The user_id returned here is the Google-verified email.
|
138
138
|
verified_user_id, credentials = handle_auth_callback(
|
139
|
-
client_secrets_path=client_secrets_path,
|
140
139
|
scopes=SCOPES, # Ensure all necessary scopes are requested
|
141
140
|
authorization_response=str(request.url),
|
142
141
|
redirect_uri=get_oauth_redirect_uri_for_current_mode(),
|
@@ -157,8 +156,8 @@ async def oauth2_callback(request: Request) -> HTMLResponse:
|
|
157
156
|
|
158
157
|
@server.tool()
|
159
158
|
async def start_google_auth(
|
160
|
-
user_google_email: str,
|
161
159
|
service_name: str,
|
160
|
+
user_google_email: str = USER_GOOGLE_EMAIL,
|
162
161
|
mcp_session_id: Optional[str] = Header(None, alias="Mcp-Session-Id")
|
163
162
|
) -> str:
|
164
163
|
"""
|
gdocs/docs_tools.py
CHANGED
@@ -16,6 +16,7 @@ from googleapiclient.http import MediaIoBaseDownload
|
|
16
16
|
from auth.service_decorator import require_google_service, require_multiple_services
|
17
17
|
from core.utils import extract_office_xml_text, handle_http_errors
|
18
18
|
from core.server import server
|
19
|
+
from core.comments import create_comment_tools
|
19
20
|
|
20
21
|
logger = logging.getLogger(__name__)
|
21
22
|
|
@@ -214,3 +215,13 @@ async def create_doc(
|
|
214
215
|
msg = f"Created Google Doc '{title}' (ID: {doc_id}) for {user_google_email}. Link: {link}"
|
215
216
|
logger.info(f"Successfully created Google Doc '{title}' (ID: {doc_id}) for {user_google_email}. Link: {link}")
|
216
217
|
return msg
|
218
|
+
|
219
|
+
|
220
|
+
# Create comment management tools for documents
|
221
|
+
_comment_tools = create_comment_tools("document", "document_id")
|
222
|
+
|
223
|
+
# Extract and register the functions
|
224
|
+
read_doc_comments = _comment_tools['read_comments']
|
225
|
+
create_doc_comment = _comment_tools['create_comment']
|
226
|
+
reply_to_comment = _comment_tools['reply_to_comment']
|
227
|
+
resolve_comment = _comment_tools['resolve_comment']
|
gsheets/sheets_tools.py
CHANGED
@@ -14,6 +14,7 @@ from googleapiclient.errors import HttpError
|
|
14
14
|
from auth.service_decorator import require_google_service
|
15
15
|
from core.server import server
|
16
16
|
from core.utils import handle_http_errors
|
17
|
+
from core.comments import create_comment_tools
|
17
18
|
|
18
19
|
# Configure module logger
|
19
20
|
logger = logging.getLogger(__name__)
|
@@ -338,3 +339,13 @@ async def create_sheet(
|
|
338
339
|
return text_output
|
339
340
|
|
340
341
|
|
342
|
+
# Create comment management tools for sheets
|
343
|
+
_comment_tools = create_comment_tools("spreadsheet", "spreadsheet_id")
|
344
|
+
|
345
|
+
# Extract and register the functions
|
346
|
+
read_sheet_comments = _comment_tools['read_comments']
|
347
|
+
create_sheet_comment = _comment_tools['create_comment']
|
348
|
+
reply_to_sheet_comment = _comment_tools['reply_to_comment']
|
349
|
+
resolve_sheet_comment = _comment_tools['resolve_comment']
|
350
|
+
|
351
|
+
|
gslides/slides_tools.py
CHANGED
@@ -14,6 +14,7 @@ from googleapiclient.errors import HttpError
|
|
14
14
|
from auth.service_decorator import require_google_service
|
15
15
|
from core.server import server
|
16
16
|
from core.utils import handle_http_errors
|
17
|
+
from core.comments import create_comment_tools
|
17
18
|
|
18
19
|
logger = logging.getLogger(__name__)
|
19
20
|
|
@@ -269,4 +270,18 @@ async def get_page_thumbnail(
|
|
269
270
|
You can view or download the thumbnail using the provided URL."""
|
270
271
|
|
271
272
|
logger.info(f"Thumbnail generated successfully for {user_google_email}")
|
272
|
-
return confirmation_message
|
273
|
+
return confirmation_message
|
274
|
+
|
275
|
+
|
276
|
+
# Create comment management tools for slides
|
277
|
+
_comment_tools = create_comment_tools("presentation", "presentation_id")
|
278
|
+
read_presentation_comments = _comment_tools['read_comments']
|
279
|
+
create_presentation_comment = _comment_tools['create_comment']
|
280
|
+
reply_to_presentation_comment = _comment_tools['reply_to_comment']
|
281
|
+
resolve_presentation_comment = _comment_tools['resolve_comment']
|
282
|
+
|
283
|
+
# Aliases for backwards compatibility and intuitive naming
|
284
|
+
read_slide_comments = read_presentation_comments
|
285
|
+
create_slide_comment = create_presentation_comment
|
286
|
+
reply_to_slide_comment = reply_to_presentation_comment
|
287
|
+
resolve_slide_comment = resolve_presentation_comment
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: workspace-mcp
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.4
|
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
|
@@ -76,6 +76,14 @@ Dynamic: license-file
|
|
76
76
|
|
77
77
|
---
|
78
78
|
|
79
|
+
### A quick plug for AI-Enhanced Docs
|
80
|
+
|
81
|
+
> **This README was crafted with AI assistance, and here's why that matters**
|
82
|
+
>
|
83
|
+
> As a solo developer building open source tools that may only ever serve my own needs, comprehensive documentation often wouldn't happen without AI help. Using agentic dev tools like **Roo** & **Claude Code** that understand the entire codebase, AI doesn't just regurgitate generic content - it extracts real implementation details and creates accurate, specific documentation.
|
84
|
+
>
|
85
|
+
> In this case, Sonnet 4 took a pass & a human (me) verified them 6/28/25.
|
86
|
+
|
79
87
|
|
80
88
|
## 🌐 Overview
|
81
89
|
|
@@ -87,9 +95,9 @@ A production-ready MCP server that integrates all major Google Workspace service
|
|
87
95
|
- **📅 Google Calendar**: Full calendar management with event CRUD operations
|
88
96
|
- **📁 Google Drive**: File operations with native Microsoft Office format support (.docx, .xlsx)
|
89
97
|
- **📧 Gmail**: Complete email management with search, send, and draft capabilities
|
90
|
-
- **📄 Google Docs**: Document operations including content extraction and
|
91
|
-
- **📊 Google Sheets**: Comprehensive spreadsheet management with flexible cell operations
|
92
|
-
- **🖼️ Google Slides**: Presentation management with slide creation, updates, and
|
98
|
+
- **📄 Google Docs**: Document operations including content extraction, creation, and comment management
|
99
|
+
- **📊 Google Sheets**: Comprehensive spreadsheet management with flexible cell operations and comment management
|
100
|
+
- **🖼️ Google Slides**: Presentation management with slide creation, updates, content manipulation, and comment management
|
93
101
|
- **📝 Google Forms**: Form creation, retrieval, publish settings, and response management
|
94
102
|
- **💬 Google Chat**: Space management and messaging capabilities
|
95
103
|
- **🔄 Multiple Transports**: HTTP with SSE fallback, OpenAPI compatibility via `mcpo`
|
@@ -102,9 +110,13 @@ A production-ready MCP server that integrates all major Google Workspace service
|
|
102
110
|
|
103
111
|
### Simplest Start (uvx - Recommended)
|
104
112
|
|
105
|
-
Run instantly without installation
|
113
|
+
> Run instantly without manual installation - you must configure OAuth credentials when using uvx. You can use either environment variables (recommended for production) or set the `GOOGLE_CLIENT_SECRET_PATH` (or legacy `GOOGLE_CLIENT_SECRETS`) environment variable to point to your `client_secret.json` file.
|
106
114
|
|
107
115
|
```bash
|
116
|
+
# Set OAuth credentials via environment variables (recommended)
|
117
|
+
export GOOGLE_OAUTH_CLIENT_ID="your-client-id.apps.googleusercontent.com"
|
118
|
+
export GOOGLE_OAUTH_CLIENT_SECRET="your-client-secret"
|
119
|
+
|
108
120
|
# Start the server with all Google Workspace tools
|
109
121
|
uvx workspace-mcp
|
110
122
|
|
@@ -138,19 +150,43 @@ uv run main.py
|
|
138
150
|
1. **Google Cloud Setup**:
|
139
151
|
- Create OAuth 2.0 credentials (web application) in [Google Cloud Console](https://console.cloud.google.com/)
|
140
152
|
- Enable APIs: Calendar, Drive, Gmail, Docs, Sheets, Slides, Forms, Chat
|
141
|
-
- Download credentials as `client_secret.json` in project root
|
142
|
-
- To use a different location for `client_secret.json`, you can set the `GOOGLE_CLIENT_SECRETS` environment variable with that path
|
143
153
|
- Add redirect URI: `http://localhost:8000/oauth2callback`
|
154
|
+
- Configure credentials using one of these methods:
|
155
|
+
|
156
|
+
**Option A: Environment Variables (Recommended for Production)**
|
157
|
+
```bash
|
158
|
+
export GOOGLE_OAUTH_CLIENT_ID="your-client-id.apps.googleusercontent.com"
|
159
|
+
export GOOGLE_OAUTH_CLIENT_SECRET="your-client-secret"
|
160
|
+
export GOOGLE_OAUTH_REDIRECT_URI="http://localhost:8000/oauth2callback" # Optional
|
161
|
+
```
|
162
|
+
|
163
|
+
**Option B: File-based (Traditional)**
|
164
|
+
- Download credentials as `client_secret.json` in project root
|
165
|
+
- To use a different location, set `GOOGLE_CLIENT_SECRET_PATH` (or legacy `GOOGLE_CLIENT_SECRETS`) environment variable with the file path
|
166
|
+
|
167
|
+
**Credential Loading Priority**:
|
168
|
+
1. Environment variables (`GOOGLE_OAUTH_CLIENT_ID`, `GOOGLE_OAUTH_CLIENT_SECRET`)
|
169
|
+
2. File specified by `GOOGLE_CLIENT_SECRET_PATH` or `GOOGLE_CLIENT_SECRETS` environment variable
|
170
|
+
3. Default file (`client_secret.json` in project root)
|
171
|
+
|
172
|
+
**Why Environment Variables?**
|
173
|
+
- ✅ Containerized deployments (Docker, Kubernetes)
|
174
|
+
- ✅ Cloud platforms (Heroku, Railway, etc.)
|
175
|
+
- ✅ CI/CD pipelines
|
176
|
+
- ✅ No secrets in version control
|
177
|
+
- ✅ Easy credential rotation
|
144
178
|
|
145
179
|
2. **Environment**:
|
146
180
|
```bash
|
147
181
|
export OAUTHLIB_INSECURE_TRANSPORT=1 # Development only
|
182
|
+
export USER_GOOGLE_EMAIL=your.email@gmail.com # Optional: Default email for auth - use this for single user setups and you won't need to set your email in system prompt for magic auth
|
148
183
|
```
|
149
184
|
|
150
185
|
3. **Server Configuration**:
|
151
186
|
The server's base URL and port can be customized using environment variables:
|
152
187
|
- `WORKSPACE_MCP_BASE_URI`: Sets the base URI for the server (default: http://localhost). This affects the server_url used for Gemini native function calling and the OAUTH_REDIRECT_URI.
|
153
188
|
- `WORKSPACE_MCP_PORT`: Sets the port the server listens on (default: 8000). This affects the server_url, port, and OAUTH_REDIRECT_URI.
|
189
|
+
- `USER_GOOGLE_EMAIL`: Optional default email for authentication flows. If set, the LLM won't need to specify your email when calling `start_google_auth`.
|
154
190
|
|
155
191
|
### Start the Server
|
156
192
|
|
@@ -198,7 +234,11 @@ python install_claude.py
|
|
198
234
|
"mcpServers": {
|
199
235
|
"google_workspace": {
|
200
236
|
"command": "uvx",
|
201
|
-
"args": ["workspace-mcp"]
|
237
|
+
"args": ["workspace-mcp"],
|
238
|
+
"env": {
|
239
|
+
"GOOGLE_OAUTH_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
|
240
|
+
"GOOGLE_OAUTH_CLIENT_SECRET": "your-client-secret"
|
241
|
+
}
|
202
242
|
}
|
203
243
|
}
|
204
244
|
}
|
@@ -211,7 +251,11 @@ python install_claude.py
|
|
211
251
|
"google_workspace": {
|
212
252
|
"command": "uv",
|
213
253
|
"args": ["run", "main.py"],
|
214
|
-
"cwd": "/path/to/google_workspace_mcp"
|
254
|
+
"cwd": "/path/to/google_workspace_mcp",
|
255
|
+
"env": {
|
256
|
+
"GOOGLE_OAUTH_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
|
257
|
+
"GOOGLE_OAUTH_CLIENT_SECRET": "your-client-secret"
|
258
|
+
}
|
215
259
|
}
|
216
260
|
}
|
217
261
|
}
|
@@ -259,7 +303,8 @@ When calling a tool:
|
|
259
303
|
|------|-------------|
|
260
304
|
| `list_calendars` | List accessible calendars |
|
261
305
|
| `get_events` | Retrieve events with time range filtering |
|
262
|
-
| `
|
306
|
+
| `get_event` | Fetch detailed information of a single event by ID |
|
307
|
+
| `create_event` | Create events (all-day or timed) with optional Drive file attachments |
|
263
308
|
| `modify_event` | Update existing events |
|
264
309
|
| `delete_event` | Remove events |
|
265
310
|
|
@@ -270,7 +315,7 @@ When calling a tool:
|
|
270
315
|
| `search_drive_files` | Search files with query syntax |
|
271
316
|
| `get_drive_file_content` | Read file content (supports Office formats) |
|
272
317
|
| `list_drive_items` | List folder contents |
|
273
|
-
| `create_drive_file` | Create new files |
|
318
|
+
| `create_drive_file` | Create new files or fetch content from public URLs |
|
274
319
|
|
275
320
|
### 📧 Gmail ([`gmail_tools.py`](gmail/gmail_tools.py))
|
276
321
|
|
@@ -289,6 +334,10 @@ When calling a tool:
|
|
289
334
|
| `get_doc_content` | Extract document text |
|
290
335
|
| `list_docs_in_folder` | List docs in folder |
|
291
336
|
| `create_doc` | Create new documents |
|
337
|
+
| `read_doc_comments` | Read all comments and replies |
|
338
|
+
| `create_doc_comment` | Create new comments |
|
339
|
+
| `reply_to_comment` | Reply to existing comments |
|
340
|
+
| `resolve_comment` | Resolve comments |
|
292
341
|
|
293
342
|
### 📊 Google Sheets ([`sheets_tools.py`](gsheets/sheets_tools.py))
|
294
343
|
|
@@ -300,6 +349,24 @@ When calling a tool:
|
|
300
349
|
| `modify_sheet_values` | Write/update/clear cells |
|
301
350
|
| `create_spreadsheet` | Create new spreadsheets |
|
302
351
|
| `create_sheet` | Add sheets to existing files |
|
352
|
+
| `read_sheet_comments` | Read all comments and replies |
|
353
|
+
| `create_sheet_comment` | Create new comments |
|
354
|
+
| `reply_to_sheet_comment` | Reply to existing comments |
|
355
|
+
| `resolve_sheet_comment` | Resolve comments |
|
356
|
+
|
357
|
+
### 🖼️ Google Slides ([`slides_tools.py`](gslides/slides_tools.py))
|
358
|
+
|
359
|
+
| Tool | Description |
|
360
|
+
|------|-------------|
|
361
|
+
| `create_presentation` | Create new presentations |
|
362
|
+
| `get_presentation` | Retrieve presentation details |
|
363
|
+
| `batch_update_presentation` | Apply multiple updates at once |
|
364
|
+
| `get_page` | Get specific slide information |
|
365
|
+
| `get_page_thumbnail` | Generate slide thumbnails |
|
366
|
+
| `read_presentation_comments` | Read all comments and replies |
|
367
|
+
| `create_presentation_comment` | Create new comments |
|
368
|
+
| `reply_to_presentation_comment` | Reply to existing comments |
|
369
|
+
| `resolve_presentation_comment` | Resolve comments |
|
303
370
|
|
304
371
|
### 📝 Google Forms ([`forms_tools.py`](gforms/forms_tools.py))
|
305
372
|
|
@@ -1,19 +1,21 @@
|
|
1
1
|
main.py,sha256=Mv5jfggqQ5XqzetKhccD2OmeWFnhidSbgzyKppCfywo,7078
|
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=2UBbQgGcUPdUFWDbzdFy60NJLQ3SI45GIASzuzO1Tew,30717
|
4
|
+
auth/oauth_callback_server.py,sha256=igrur3fkZSY0bawufrH4AN9fMNpobUdAUp1BG7AQC6w,9341
|
5
5
|
auth/oauth_responses.py,sha256=qbirSB4d7mBRKcJKqGLrJxRAPaLHqObf9t-VMAq6UKA,7020
|
6
6
|
auth/scopes.py,sha256=kMRdFN0wLyipFkp7IitTHs-M6zhZD-oieVd7fylueBc,3320
|
7
7
|
auth/service_decorator.py,sha256=h9bkG1O6U-p4_yT1KseBKJvueprKd4SVJe1Bj2VrdXA,15669
|
8
8
|
core/__init__.py,sha256=AHVKdPl6v4lUFm2R-KuGuAgEmCyfxseMeLGtntMcqCs,43
|
9
|
-
core/
|
9
|
+
core/comments.py,sha256=n-S84v5N5x3LbL45vGUerERhNPYvuSlugpOboYtPGgw,11328
|
10
|
+
core/context.py,sha256=zNgPXf9EO2EMs9sQkfKiywoy6sEOksVNgOrJMA_c30Y,768
|
11
|
+
core/server.py,sha256=8A5_o6RCZ3hhsAiCszZhHiUJbVVrxJLspcvCiMmt27Q,9265
|
10
12
|
core/utils.py,sha256=2t5wbLtSLodxNKNAZb-jmR8Zg6mm-Rady-LpnXCP-1g,10297
|
11
13
|
gcalendar/__init__.py,sha256=D5fSdAwbeomoaj7XAdxSnIy-NVKNkpExs67175bOtfc,46
|
12
14
|
gcalendar/calendar_tools.py,sha256=SIiSJRxG3G9KsScow0pYwew600_PdtFqlOo-y2vXQRo,22144
|
13
15
|
gchat/__init__.py,sha256=XBjH4SbtULfZHgFCxk3moel5XqG599HCgZWl_veIncg,88
|
14
16
|
gchat/chat_tools.py,sha256=cIeXBBxWkFCdQNJ23BkX8IoDho6J8ZcfLsPjctUWyfA,7274
|
15
17
|
gdocs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
-
gdocs/docs_tools.py,sha256=
|
18
|
+
gdocs/docs_tools.py,sha256=gWPBXf2M_ucP9LasAW0JAlCFAwixlcbAFDGS62xspZ4,8482
|
17
19
|
gdrive/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
20
|
gdrive/drive_tools.py,sha256=l-6IpHTstRMKIY2CU4DFTTNfEQ5rVbafgwo8BrbJ9Bk,15257
|
19
21
|
gforms/__init__.py,sha256=pL91XixrEp9YjpM-AYwONIEfeCP2OumkEG0Io5V4boE,37
|
@@ -21,12 +23,12 @@ gforms/forms_tools.py,sha256=reJF3qw9WwW6-aCOkS2x5jVBvdRx4Za8onEZBC57RXk,9663
|
|
21
23
|
gmail/__init__.py,sha256=l8PZ4_7Oet6ZE7tVu9oQ3-BaRAmI4YzAO86kf9uu6pU,60
|
22
24
|
gmail/gmail_tools.py,sha256=UIcws__Akw0kxbasc9fYH7rkzDw_7L-LJU1LQU_p-sA,24754
|
23
25
|
gsheets/__init__.py,sha256=jFfhD52w_EOVw6N5guf_dIc9eP2khW_eS9UAPJg_K3k,446
|
24
|
-
gsheets/sheets_tools.py,sha256=
|
26
|
+
gsheets/sheets_tools.py,sha256=TVlJ-jcIvJ_sJt8xO4-sBWIshb8rabJhjTmZfzHIJsU,11898
|
25
27
|
gslides/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
|
-
gslides/slides_tools.py,sha256=
|
27
|
-
workspace_mcp-1.0.
|
28
|
-
workspace_mcp-1.0.
|
29
|
-
workspace_mcp-1.0.
|
30
|
-
workspace_mcp-1.0.
|
31
|
-
workspace_mcp-1.0.
|
32
|
-
workspace_mcp-1.0.
|
28
|
+
gslides/slides_tools.py,sha256=wil3XRyUMzUbpBUMqis0CW5eRuwOrP0Lp7-6WbF4QVU,10117
|
29
|
+
workspace_mcp-1.0.4.dist-info/licenses/LICENSE,sha256=bB8L7rIyRy5o-WHxGgvRuY8hUTzNu4h3DTkvyV8XFJo,1070
|
30
|
+
workspace_mcp-1.0.4.dist-info/METADATA,sha256=PLCeesDmtActuNb-hVX3j0xA2wvLjHPWa9FFvjKNM_4,19406
|
31
|
+
workspace_mcp-1.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
32
|
+
workspace_mcp-1.0.4.dist-info/entry_points.txt,sha256=kPiEfOTuf-ptDM0Rf2OlyrFudGW7hCZGg4MCn2Foxs4,44
|
33
|
+
workspace_mcp-1.0.4.dist-info/top_level.txt,sha256=Y8mAkTitLNE2zZEJ-DbqR9R7Cs1V1MMf-UploVdOvlw,73
|
34
|
+
workspace_mcp-1.0.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|