workspace-mcp 1.0.3__py3-none-any.whl → 1.0.5__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.
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/server.py CHANGED
@@ -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
@@ -155,8 +156,8 @@ async def oauth2_callback(request: Request) -> HTMLResponse:
155
156
 
156
157
  @server.tool()
157
158
  async def start_google_auth(
158
- user_google_email: str,
159
159
  service_name: str,
160
+ user_google_email: str = USER_GOOGLE_EMAIL,
160
161
  mcp_session_id: Optional[str] = Header(None, alias="Mcp-Session-Id")
161
162
  ) -> str:
162
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
 
@@ -216,179 +217,11 @@ async def create_doc(
216
217
  return msg
217
218
 
218
219
 
219
- @server.tool()
220
- @require_google_service("drive", "drive_read")
221
- @handle_http_errors("read_doc_comments")
222
- async def read_doc_comments(
223
- service,
224
- user_google_email: str,
225
- document_id: str,
226
- ) -> str:
227
- """
228
- Read all comments from a Google Doc.
229
-
230
- Args:
231
- document_id: The ID of the Google Document
232
-
233
- Returns:
234
- str: A formatted list of all comments and replies in the document.
235
- """
236
- logger.info(f"[read_doc_comments] Reading comments for document {document_id}")
237
-
238
- response = await asyncio.to_thread(
239
- service.comments().list(
240
- fileId=document_id,
241
- fields="comments(id,content,author,createdTime,modifiedTime,resolved,replies(content,author,id,createdTime,modifiedTime))"
242
- ).execute
243
- )
244
-
245
- comments = response.get('comments', [])
246
-
247
- if not comments:
248
- return f"No comments found in document {document_id}"
249
-
250
- output = [f"Found {len(comments)} comments in document {document_id}:\n"]
251
-
252
- for comment in comments:
253
- author = comment.get('author', {}).get('displayName', 'Unknown')
254
- content = comment.get('content', '')
255
- created = comment.get('createdTime', '')
256
- resolved = comment.get('resolved', False)
257
- comment_id = comment.get('id', '')
258
- status = " [RESOLVED]" if resolved else ""
259
-
260
- output.append(f"Comment ID: {comment_id}")
261
- output.append(f"Author: {author}")
262
- output.append(f"Created: {created}{status}")
263
- output.append(f"Content: {content}")
264
-
265
- # Add replies if any
266
- replies = comment.get('replies', [])
267
- if replies:
268
- output.append(f" Replies ({len(replies)}):")
269
- for reply in replies:
270
- reply_author = reply.get('author', {}).get('displayName', 'Unknown')
271
- reply_content = reply.get('content', '')
272
- reply_created = reply.get('createdTime', '')
273
- reply_id = reply.get('id', '')
274
- output.append(f" Reply ID: {reply_id}")
275
- output.append(f" Author: {reply_author}")
276
- output.append(f" Created: {reply_created}")
277
- output.append(f" Content: {reply_content}")
278
-
279
- output.append("") # Empty line between comments
280
-
281
- return "\n".join(output)
282
-
283
-
284
- @server.tool()
285
- @require_google_service("drive", "drive_file")
286
- @handle_http_errors("reply_to_comment")
287
- async def reply_to_comment(
288
- service,
289
- user_google_email: str,
290
- document_id: str,
291
- comment_id: str,
292
- reply_content: str,
293
- ) -> str:
294
- """
295
- Reply to a specific comment in a Google Doc.
296
-
297
- Args:
298
- document_id: The ID of the Google Document
299
- comment_id: The ID of the comment to reply to
300
- reply_content: The content of the reply
301
-
302
- Returns:
303
- str: Confirmation message with reply details.
304
- """
305
- logger.info(f"[reply_to_comment] Replying to comment {comment_id} in document {document_id}")
306
-
307
- body = {'content': reply_content}
308
-
309
- reply = await asyncio.to_thread(
310
- service.replies().create(
311
- fileId=document_id,
312
- commentId=comment_id,
313
- body=body,
314
- fields="id,content,author,createdTime,modifiedTime"
315
- ).execute
316
- )
317
-
318
- reply_id = reply.get('id', '')
319
- author = reply.get('author', {}).get('displayName', 'Unknown')
320
- created = reply.get('createdTime', '')
321
-
322
- return f"Reply posted successfully!\nReply ID: {reply_id}\nAuthor: {author}\nCreated: {created}\nContent: {reply_content}"
323
-
324
-
325
- @server.tool()
326
- @require_google_service("drive", "drive_file")
327
- @handle_http_errors("create_doc_comment")
328
- async def create_doc_comment(
329
- service,
330
- user_google_email: str,
331
- document_id: str,
332
- comment_content: str,
333
- ) -> str:
334
- """
335
- Create a new comment on a Google Doc.
336
-
337
- Args:
338
- document_id: The ID of the Google Document
339
- comment_content: The content of the comment
340
-
341
- Returns:
342
- str: Confirmation message with comment details.
343
- """
344
- logger.info(f"[create_doc_comment] Creating comment in document {document_id}")
345
-
346
- body = {"content": comment_content}
347
-
348
- comment = await asyncio.to_thread(
349
- service.comments().create(
350
- fileId=document_id,
351
- body=body,
352
- fields="id,content,author,createdTime,modifiedTime"
353
- ).execute
354
- )
355
-
356
- comment_id = comment.get('id', '')
357
- author = comment.get('author', {}).get('displayName', 'Unknown')
358
- created = comment.get('createdTime', '')
359
-
360
- return f"Comment created successfully!\nComment ID: {comment_id}\nAuthor: {author}\nCreated: {created}\nContent: {comment_content}"
361
-
220
+ # Create comment management tools for documents
221
+ _comment_tools = create_comment_tools("document", "document_id")
362
222
 
363
- @server.tool()
364
- @require_google_service("drive", "drive_file")
365
- @handle_http_errors("resolve_comment")
366
- async def resolve_comment(
367
- service,
368
- user_google_email: str,
369
- document_id: str,
370
- comment_id: str,
371
- ) -> str:
372
- """
373
- Resolve a comment in a Google Doc.
374
-
375
- Args:
376
- document_id: The ID of the Google Document
377
- comment_id: The ID of the comment to resolve
378
-
379
- Returns:
380
- str: Confirmation message.
381
- """
382
- logger.info(f"[resolve_comment] Resolving comment {comment_id} in document {document_id}")
383
-
384
- body = {"resolved": True}
385
-
386
- await asyncio.to_thread(
387
- service.comments().update(
388
- fileId=document_id,
389
- commentId=comment_id,
390
- body=body
391
- ).execute
392
- )
393
-
394
- return f"Comment {comment_id} has been resolved successfully."
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
main.py CHANGED
@@ -35,9 +35,9 @@ except Exception as e:
35
35
 
36
36
  def safe_print(text):
37
37
  try:
38
- print(text)
38
+ print(text, file=sys.stderr)
39
39
  except UnicodeEncodeError:
40
- print(text.encode('ascii', errors='replace').decode())
40
+ print(text.encode('ascii', errors='replace').decode(), file=sys.stderr)
41
41
 
42
42
  def main():
43
43
  """
@@ -73,7 +73,7 @@ def main():
73
73
  safe_print(f" 🔐 OAuth Callback: {base_uri}:{port}/oauth2callback")
74
74
  safe_print(f" 👤 Mode: {'Single-user' if args.single_user else 'Multi-user'}")
75
75
  safe_print(f" 🐍 Python: {sys.version.split()[0]}")
76
- print()
76
+ print(file=sys.stderr)
77
77
 
78
78
  # Import tool modules to register them with the MCP server via decorators
79
79
  tool_imports = {
@@ -104,29 +104,29 @@ def main():
104
104
  for tool in tools_to_import:
105
105
  tool_imports[tool]()
106
106
  safe_print(f" {tool_icons[tool]} {tool.title()} - Google {tool.title()} API integration")
107
- print()
107
+ print(file=sys.stderr)
108
108
 
109
109
  safe_print(f"📊 Configuration Summary:")
110
110
  safe_print(f" 🔧 Tools Enabled: {len(tools_to_import)}/{len(tool_imports)}")
111
111
  safe_print(f" 🔑 Auth Method: OAuth 2.0 with PKCE")
112
112
  safe_print(f" 📝 Log Level: {logging.getLogger().getEffectiveLevel()}")
113
- print()
113
+ print(file=sys.stderr)
114
114
 
115
115
  # Set global single-user mode flag
116
116
  if args.single_user:
117
117
  os.environ['MCP_SINGLE_USER_MODE'] = '1'
118
118
  safe_print("🔐 Single-user mode enabled")
119
- print()
119
+ print(file=sys.stderr)
120
120
 
121
121
  # Check credentials directory permissions before starting
122
122
  try:
123
123
  safe_print("🔍 Checking credentials directory permissions...")
124
124
  check_credentials_directory_permissions()
125
125
  safe_print("✅ Credentials directory permissions verified")
126
- print()
126
+ print(file=sys.stderr)
127
127
  except (PermissionError, OSError) as e:
128
128
  safe_print(f"❌ Credentials directory permission check failed: {e}")
129
- print(" Please ensure the service has write permissions to create/access the .credentials directory")
129
+ print(" Please ensure the service has write permissions to create/access the .credentials directory", file=sys.stderr)
130
130
  logger.error(f"Failed credentials directory permission check: {e}")
131
131
  sys.exit(1)
132
132
 
@@ -141,12 +141,12 @@ def main():
141
141
  # Start minimal OAuth callback server for stdio mode
142
142
  from auth.oauth_callback_server import ensure_oauth_callback_available
143
143
  if ensure_oauth_callback_available('stdio', port, base_uri):
144
- print(f" OAuth callback server started on {base_uri}:{port}/oauth2callback")
144
+ print(f" OAuth callback server started on {base_uri}:{port}/oauth2callback", file=sys.stderr)
145
145
  else:
146
146
  safe_print(" ⚠️ Warning: Failed to start OAuth callback server")
147
147
 
148
- print(" Ready for MCP connections!")
149
- print()
148
+ print(" Ready for MCP connections!", file=sys.stderr)
149
+ print(file=sys.stderr)
150
150
 
151
151
  if args.transport == 'streamable-http':
152
152
  # The server is already configured with port and server_url in core/server.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workspace-mcp
3
- Version: 1.0.3
3
+ Version: 1.0.5
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,20 +76,14 @@ Dynamic: license-file
76
76
 
77
77
  ---
78
78
 
79
- ## AI-Enhanced Documentation
79
+ ### A quick plug for AI-Enhanced Docs
80
80
 
81
81
  > **This README was crafted with AI assistance, and here's why that matters**
82
82
  >
83
- > When people dismiss documentation as "AI-generated," they're missing the bigger picture
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.
84
86
 
85
- 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. When done right—using agents like **Roo** or **Claude Code** that understand the entire codebase, AI doesn't just regurgitate generic content - it extracts real implementation details and creates accurate, specific documentation.
86
-
87
- **The alternative? No docs at all.**
88
-
89
- I hope the community can appreciate these tools for what they enable: solo developers maintaining professional documentation standards while focusing on building great software.
90
-
91
- ---
92
- *This documentation was enhanced by AI with full codebase context. The result? You're reading docs that otherwise might not exist.*
93
87
 
94
88
  ## 🌐 Overview
95
89
 
@@ -101,9 +95,9 @@ A production-ready MCP server that integrates all major Google Workspace service
101
95
  - **📅 Google Calendar**: Full calendar management with event CRUD operations
102
96
  - **📁 Google Drive**: File operations with native Microsoft Office format support (.docx, .xlsx)
103
97
  - **📧 Gmail**: Complete email management with search, send, and draft capabilities
104
- - **📄 Google Docs**: Document operations including content extraction and creation
105
- - **📊 Google Sheets**: Comprehensive spreadsheet management with flexible cell operations
106
- - **🖼️ Google Slides**: Presentation management with slide creation, updates, and content manipulation
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
107
101
  - **📝 Google Forms**: Form creation, retrieval, publish settings, and response management
108
102
  - **💬 Google Chat**: Space management and messaging capabilities
109
103
  - **🔄 Multiple Transports**: HTTP with SSE fallback, OpenAPI compatibility via `mcpo`
@@ -185,12 +179,14 @@ uv run main.py
185
179
  2. **Environment**:
186
180
  ```bash
187
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
188
183
  ```
189
184
 
190
185
  3. **Server Configuration**:
191
186
  The server's base URL and port can be customized using environment variables:
192
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.
193
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`.
194
190
 
195
191
  ### Start the Server
196
192
 
@@ -338,6 +334,10 @@ When calling a tool:
338
334
  | `get_doc_content` | Extract document text |
339
335
  | `list_docs_in_folder` | List docs in folder |
340
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 |
341
341
 
342
342
  ### 📊 Google Sheets ([`sheets_tools.py`](gsheets/sheets_tools.py))
343
343
 
@@ -349,6 +349,24 @@ When calling a tool:
349
349
  | `modify_sheet_values` | Write/update/clear cells |
350
350
  | `create_spreadsheet` | Create new spreadsheets |
351
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 |
352
370
 
353
371
  ### 📝 Google Forms ([`forms_tools.py`](gforms/forms_tools.py))
354
372
 
@@ -1,4 +1,4 @@
1
- main.py,sha256=Mv5jfggqQ5XqzetKhccD2OmeWFnhidSbgzyKppCfywo,7078
1
+ main.py,sha256=u2V2l4wlYxP0MwSITukYxtha3HSdmNGHSllIv5z-WxA,7253
2
2
  auth/__init__.py,sha256=gPCU3GE-SLy91S3D3CbX-XfKBm6hteK_VSPKx7yjT5s,42
3
3
  auth/google_auth.py,sha256=2UBbQgGcUPdUFWDbzdFy60NJLQ3SI45GIASzuzO1Tew,30717
4
4
  auth/oauth_callback_server.py,sha256=igrur3fkZSY0bawufrH4AN9fMNpobUdAUp1BG7AQC6w,9341
@@ -6,15 +6,16 @@ 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/comments.py,sha256=n-S84v5N5x3LbL45vGUerERhNPYvuSlugpOboYtPGgw,11328
9
10
  core/context.py,sha256=zNgPXf9EO2EMs9sQkfKiywoy6sEOksVNgOrJMA_c30Y,768
10
- core/server.py,sha256=fBPGMy9axIUOppsRFB31rlkaxEV32JRvaw2ZN_UZ9ms,9188
11
+ core/server.py,sha256=8A5_o6RCZ3hhsAiCszZhHiUJbVVrxJLspcvCiMmt27Q,9265
11
12
  core/utils.py,sha256=2t5wbLtSLodxNKNAZb-jmR8Zg6mm-Rady-LpnXCP-1g,10297
12
13
  gcalendar/__init__.py,sha256=D5fSdAwbeomoaj7XAdxSnIy-NVKNkpExs67175bOtfc,46
13
14
  gcalendar/calendar_tools.py,sha256=SIiSJRxG3G9KsScow0pYwew600_PdtFqlOo-y2vXQRo,22144
14
15
  gchat/__init__.py,sha256=XBjH4SbtULfZHgFCxk3moel5XqG599HCgZWl_veIncg,88
15
16
  gchat/chat_tools.py,sha256=cIeXBBxWkFCdQNJ23BkX8IoDho6J8ZcfLsPjctUWyfA,7274
16
17
  gdocs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- gdocs/docs_tools.py,sha256=AN9yG0c2AWCTd3bjc9guYBaOOF_PS_c_xKV6UMXclvU,13555
18
+ gdocs/docs_tools.py,sha256=gWPBXf2M_ucP9LasAW0JAlCFAwixlcbAFDGS62xspZ4,8482
18
19
  gdrive/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
20
  gdrive/drive_tools.py,sha256=l-6IpHTstRMKIY2CU4DFTTNfEQ5rVbafgwo8BrbJ9Bk,15257
20
21
  gforms/__init__.py,sha256=pL91XixrEp9YjpM-AYwONIEfeCP2OumkEG0Io5V4boE,37
@@ -22,12 +23,12 @@ gforms/forms_tools.py,sha256=reJF3qw9WwW6-aCOkS2x5jVBvdRx4Za8onEZBC57RXk,9663
22
23
  gmail/__init__.py,sha256=l8PZ4_7Oet6ZE7tVu9oQ3-BaRAmI4YzAO86kf9uu6pU,60
23
24
  gmail/gmail_tools.py,sha256=UIcws__Akw0kxbasc9fYH7rkzDw_7L-LJU1LQU_p-sA,24754
24
25
  gsheets/__init__.py,sha256=jFfhD52w_EOVw6N5guf_dIc9eP2khW_eS9UAPJg_K3k,446
25
- gsheets/sheets_tools.py,sha256=ctUvaA-3I-iGwCCHOk9Bloh5P7XQDqxBnFAxFTqCTPc,11466
26
+ gsheets/sheets_tools.py,sha256=TVlJ-jcIvJ_sJt8xO4-sBWIshb8rabJhjTmZfzHIJsU,11898
26
27
  gslides/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- gslides/slides_tools.py,sha256=FyFbpUxfbaueyN4lbRk5WeoxK7NWbLDTBCiyDPtPFgM,9426
28
- workspace_mcp-1.0.3.dist-info/licenses/LICENSE,sha256=bB8L7rIyRy5o-WHxGgvRuY8hUTzNu4h3DTkvyV8XFJo,1070
29
- workspace_mcp-1.0.3.dist-info/METADATA,sha256=Ix57WY-9KB0uDjyFNMWYh42CcEpJEmDtx9Xe3N-bIgY,18318
30
- workspace_mcp-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- workspace_mcp-1.0.3.dist-info/entry_points.txt,sha256=kPiEfOTuf-ptDM0Rf2OlyrFudGW7hCZGg4MCn2Foxs4,44
32
- workspace_mcp-1.0.3.dist-info/top_level.txt,sha256=Y8mAkTitLNE2zZEJ-DbqR9R7Cs1V1MMf-UploVdOvlw,73
33
- workspace_mcp-1.0.3.dist-info/RECORD,,
28
+ gslides/slides_tools.py,sha256=wil3XRyUMzUbpBUMqis0CW5eRuwOrP0Lp7-6WbF4QVU,10117
29
+ workspace_mcp-1.0.5.dist-info/licenses/LICENSE,sha256=bB8L7rIyRy5o-WHxGgvRuY8hUTzNu4h3DTkvyV8XFJo,1070
30
+ workspace_mcp-1.0.5.dist-info/METADATA,sha256=I81V5KDQkJCvFm2zuAnZFqPgXAfvRWJKlFR56fYH4ek,19406
31
+ workspace_mcp-1.0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
+ workspace_mcp-1.0.5.dist-info/entry_points.txt,sha256=kPiEfOTuf-ptDM0Rf2OlyrFudGW7hCZGg4MCn2Foxs4,44
33
+ workspace_mcp-1.0.5.dist-info/top_level.txt,sha256=Y8mAkTitLNE2zZEJ-DbqR9R7Cs1V1MMf-UploVdOvlw,73
34
+ workspace_mcp-1.0.5.dist-info/RECORD,,