workspace-mcp 1.0.1__py3-none-any.whl → 1.0.3__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.
gslides/slides_tools.py CHANGED
@@ -13,12 +13,14 @@ from googleapiclient.errors import HttpError
13
13
 
14
14
  from auth.service_decorator import require_google_service
15
15
  from core.server import server
16
+ from core.utils import handle_http_errors
16
17
 
17
18
  logger = logging.getLogger(__name__)
18
19
 
19
20
 
20
21
  @server.tool()
21
22
  @require_google_service("slides", "slides")
23
+ @handle_http_errors("create_presentation")
22
24
  async def create_presentation(
23
25
  service,
24
26
  user_google_email: str,
@@ -36,39 +38,30 @@ async def create_presentation(
36
38
  """
37
39
  logger.info(f"[create_presentation] Invoked. Email: '{user_google_email}', Title: '{title}'")
38
40
 
39
- try:
40
- body = {
41
- 'title': title
42
- }
43
-
44
- result = await asyncio.to_thread(
45
- service.presentations().create(body=body).execute
46
- )
47
-
48
- presentation_id = result.get('presentationId')
49
- presentation_url = f"https://docs.google.com/presentation/d/{presentation_id}/edit"
50
-
51
- confirmation_message = f"""Presentation Created Successfully for {user_google_email}:
41
+ body = {
42
+ 'title': title
43
+ }
44
+
45
+ result = await asyncio.to_thread(
46
+ service.presentations().create(body=body).execute
47
+ )
48
+
49
+ presentation_id = result.get('presentationId')
50
+ presentation_url = f"https://docs.google.com/presentation/d/{presentation_id}/edit"
51
+
52
+ confirmation_message = f"""Presentation Created Successfully for {user_google_email}:
52
53
  - Title: {title}
53
54
  - Presentation ID: {presentation_id}
54
55
  - URL: {presentation_url}
55
56
  - Slides: {len(result.get('slides', []))} slide(s) created"""
56
-
57
- logger.info(f"Presentation created successfully for {user_google_email}")
58
- return confirmation_message
59
-
60
- except HttpError as error:
61
- message = f"API error: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with the user's email ({user_google_email}) and service_name='Google Slides'."
62
- logger.error(message, exc_info=True)
63
- raise Exception(message)
64
- except Exception as e:
65
- message = f"Unexpected error: {e}."
66
- logger.exception(message)
67
- raise Exception(message)
57
+
58
+ logger.info(f"Presentation created successfully for {user_google_email}")
59
+ return confirmation_message
68
60
 
69
61
 
70
62
  @server.tool()
71
63
  @require_google_service("slides", "slides_read")
64
+ @handle_http_errors("get_presentation")
72
65
  async def get_presentation(
73
66
  service,
74
67
  user_google_email: str,
@@ -86,22 +79,21 @@ async def get_presentation(
86
79
  """
87
80
  logger.info(f"[get_presentation] Invoked. Email: '{user_google_email}', ID: '{presentation_id}'")
88
81
 
89
- try:
90
- result = await asyncio.to_thread(
91
- service.presentations().get(presentationId=presentation_id).execute
92
- )
93
-
94
- title = result.get('title', 'Untitled')
95
- slides = result.get('slides', [])
96
- page_size = result.get('pageSize', {})
97
-
98
- slides_info = []
99
- for i, slide in enumerate(slides, 1):
100
- slide_id = slide.get('objectId', 'Unknown')
101
- page_elements = slide.get('pageElements', [])
102
- slides_info.append(f" Slide {i}: ID {slide_id}, {len(page_elements)} element(s)")
103
-
104
- confirmation_message = f"""Presentation Details for {user_google_email}:
82
+ result = await asyncio.to_thread(
83
+ service.presentations().get(presentationId=presentation_id).execute
84
+ )
85
+
86
+ title = result.get('title', 'Untitled')
87
+ slides = result.get('slides', [])
88
+ page_size = result.get('pageSize', {})
89
+
90
+ slides_info = []
91
+ for i, slide in enumerate(slides, 1):
92
+ slide_id = slide.get('objectId', 'Unknown')
93
+ page_elements = slide.get('pageElements', [])
94
+ slides_info.append(f" Slide {i}: ID {slide_id}, {len(page_elements)} element(s)")
95
+
96
+ confirmation_message = f"""Presentation Details for {user_google_email}:
105
97
  - Title: {title}
106
98
  - Presentation ID: {presentation_id}
107
99
  - URL: https://docs.google.com/presentation/d/{presentation_id}/edit
@@ -110,22 +102,14 @@ async def get_presentation(
110
102
 
111
103
  Slides Breakdown:
112
104
  {chr(10).join(slides_info) if slides_info else ' No slides found'}"""
113
-
114
- logger.info(f"Presentation retrieved successfully for {user_google_email}")
115
- return confirmation_message
116
-
117
- except HttpError as error:
118
- message = f"API error: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with the user's email ({user_google_email}) and service_name='Google Slides'."
119
- logger.error(message, exc_info=True)
120
- raise Exception(message)
121
- except Exception as e:
122
- message = f"Unexpected error: {e}."
123
- logger.exception(message)
124
- raise Exception(message)
105
+
106
+ logger.info(f"Presentation retrieved successfully for {user_google_email}")
107
+ return confirmation_message
125
108
 
126
109
 
127
110
  @server.tool()
128
111
  @require_google_service("slides", "slides")
112
+ @handle_http_errors("batch_update_presentation")
129
113
  async def batch_update_presentation(
130
114
  service,
131
115
  user_google_email: str,
@@ -145,53 +129,44 @@ async def batch_update_presentation(
145
129
  """
146
130
  logger.info(f"[batch_update_presentation] Invoked. Email: '{user_google_email}', ID: '{presentation_id}', Requests: {len(requests)}")
147
131
 
148
- try:
149
- body = {
150
- 'requests': requests
151
- }
152
-
153
- result = await asyncio.to_thread(
154
- service.presentations().batchUpdate(
155
- presentationId=presentation_id,
156
- body=body
157
- ).execute
158
- )
159
-
160
- replies = result.get('replies', [])
161
-
162
- confirmation_message = f"""Batch Update Completed for {user_google_email}:
132
+ body = {
133
+ 'requests': requests
134
+ }
135
+
136
+ result = await asyncio.to_thread(
137
+ service.presentations().batchUpdate(
138
+ presentationId=presentation_id,
139
+ body=body
140
+ ).execute
141
+ )
142
+
143
+ replies = result.get('replies', [])
144
+
145
+ confirmation_message = f"""Batch Update Completed for {user_google_email}:
163
146
  - Presentation ID: {presentation_id}
164
147
  - URL: https://docs.google.com/presentation/d/{presentation_id}/edit
165
148
  - Requests Applied: {len(requests)}
166
149
  - Replies Received: {len(replies)}"""
167
-
168
- if replies:
169
- confirmation_message += "\n\nUpdate Results:"
170
- for i, reply in enumerate(replies, 1):
171
- if 'createSlide' in reply:
172
- slide_id = reply['createSlide'].get('objectId', 'Unknown')
173
- confirmation_message += f"\n Request {i}: Created slide with ID {slide_id}"
174
- elif 'createShape' in reply:
175
- shape_id = reply['createShape'].get('objectId', 'Unknown')
176
- confirmation_message += f"\n Request {i}: Created shape with ID {shape_id}"
177
- else:
178
- confirmation_message += f"\n Request {i}: Operation completed"
179
-
180
- logger.info(f"Batch update completed successfully for {user_google_email}")
181
- return confirmation_message
182
-
183
- except HttpError as error:
184
- message = f"API error: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with the user's email ({user_google_email}) and service_name='Google Slides'."
185
- logger.error(message, exc_info=True)
186
- raise Exception(message)
187
- except Exception as e:
188
- message = f"Unexpected error: {e}."
189
- logger.exception(message)
190
- raise Exception(message)
150
+
151
+ if replies:
152
+ confirmation_message += "\n\nUpdate Results:"
153
+ for i, reply in enumerate(replies, 1):
154
+ if 'createSlide' in reply:
155
+ slide_id = reply['createSlide'].get('objectId', 'Unknown')
156
+ confirmation_message += f"\n Request {i}: Created slide with ID {slide_id}"
157
+ elif 'createShape' in reply:
158
+ shape_id = reply['createShape'].get('objectId', 'Unknown')
159
+ confirmation_message += f"\n Request {i}: Created shape with ID {shape_id}"
160
+ else:
161
+ confirmation_message += f"\n Request {i}: Operation completed"
162
+
163
+ logger.info(f"Batch update completed successfully for {user_google_email}")
164
+ return confirmation_message
191
165
 
192
166
 
193
167
  @server.tool()
194
168
  @require_google_service("slides", "slides_read")
169
+ @handle_http_errors("get_page")
195
170
  async def get_page(
196
171
  service,
197
172
  user_google_email: str,
@@ -211,35 +186,34 @@ async def get_page(
211
186
  """
212
187
  logger.info(f"[get_page] Invoked. Email: '{user_google_email}', Presentation: '{presentation_id}', Page: '{page_object_id}'")
213
188
 
214
- try:
215
- result = await asyncio.to_thread(
216
- service.presentations().pages().get(
217
- presentationId=presentation_id,
218
- pageObjectId=page_object_id
219
- ).execute
220
- )
221
-
222
- page_type = result.get('pageType', 'Unknown')
223
- page_elements = result.get('pageElements', [])
224
-
225
- elements_info = []
226
- for element in page_elements:
227
- element_id = element.get('objectId', 'Unknown')
228
- if 'shape' in element:
229
- shape_type = element['shape'].get('shapeType', 'Unknown')
230
- elements_info.append(f" Shape: ID {element_id}, Type: {shape_type}")
231
- elif 'table' in element:
232
- table = element['table']
233
- rows = table.get('rows', 0)
234
- cols = table.get('columns', 0)
235
- elements_info.append(f" Table: ID {element_id}, Size: {rows}x{cols}")
236
- elif 'line' in element:
237
- line_type = element['line'].get('lineType', 'Unknown')
238
- elements_info.append(f" Line: ID {element_id}, Type: {line_type}")
239
- else:
240
- elements_info.append(f" Element: ID {element_id}, Type: Unknown")
241
-
242
- confirmation_message = f"""Page Details for {user_google_email}:
189
+ result = await asyncio.to_thread(
190
+ service.presentations().pages().get(
191
+ presentationId=presentation_id,
192
+ pageObjectId=page_object_id
193
+ ).execute
194
+ )
195
+
196
+ page_type = result.get('pageType', 'Unknown')
197
+ page_elements = result.get('pageElements', [])
198
+
199
+ elements_info = []
200
+ for element in page_elements:
201
+ element_id = element.get('objectId', 'Unknown')
202
+ if 'shape' in element:
203
+ shape_type = element['shape'].get('shapeType', 'Unknown')
204
+ elements_info.append(f" Shape: ID {element_id}, Type: {shape_type}")
205
+ elif 'table' in element:
206
+ table = element['table']
207
+ rows = table.get('rows', 0)
208
+ cols = table.get('columns', 0)
209
+ elements_info.append(f" Table: ID {element_id}, Size: {rows}x{cols}")
210
+ elif 'line' in element:
211
+ line_type = element['line'].get('lineType', 'Unknown')
212
+ elements_info.append(f" Line: ID {element_id}, Type: {line_type}")
213
+ else:
214
+ elements_info.append(f" Element: ID {element_id}, Type: Unknown")
215
+
216
+ confirmation_message = f"""Page Details for {user_google_email}:
243
217
  - Presentation ID: {presentation_id}
244
218
  - Page ID: {page_object_id}
245
219
  - Page Type: {page_type}
@@ -247,22 +221,14 @@ async def get_page(
247
221
 
248
222
  Page Elements:
249
223
  {chr(10).join(elements_info) if elements_info else ' No elements found'}"""
250
-
251
- logger.info(f"Page retrieved successfully for {user_google_email}")
252
- return confirmation_message
253
-
254
- except HttpError as error:
255
- message = f"API error: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with the user's email ({user_google_email}) and service_name='Google Slides'."
256
- logger.error(message, exc_info=True)
257
- raise Exception(message)
258
- except Exception as e:
259
- message = f"Unexpected error: {e}."
260
- logger.exception(message)
261
- raise Exception(message)
224
+
225
+ logger.info(f"Page retrieved successfully for {user_google_email}")
226
+ return confirmation_message
262
227
 
263
228
 
264
229
  @server.tool()
265
230
  @require_google_service("slides", "slides_read")
231
+ @handle_http_errors("get_page_thumbnail")
266
232
  async def get_page_thumbnail(
267
233
  service,
268
234
  user_google_email: str,
@@ -284,33 +250,23 @@ async def get_page_thumbnail(
284
250
  """
285
251
  logger.info(f"[get_page_thumbnail] Invoked. Email: '{user_google_email}', Presentation: '{presentation_id}', Page: '{page_object_id}', Size: '{thumbnail_size}'")
286
252
 
287
- try:
288
- result = await asyncio.to_thread(
289
- service.presentations().pages().getThumbnail(
290
- presentationId=presentation_id,
291
- pageObjectId=page_object_id,
292
- thumbnailPropertiesImageSize=thumbnail_size
293
- ).execute
294
- )
295
-
296
- thumbnail_url = result.get('contentUrl', '')
297
-
298
- confirmation_message = f"""Thumbnail Generated for {user_google_email}:
253
+ result = await asyncio.to_thread(
254
+ service.presentations().pages().getThumbnail(
255
+ presentationId=presentation_id,
256
+ pageObjectId=page_object_id,
257
+ thumbnailPropertiesImageSize=thumbnail_size
258
+ ).execute
259
+ )
260
+
261
+ thumbnail_url = result.get('contentUrl', '')
262
+
263
+ confirmation_message = f"""Thumbnail Generated for {user_google_email}:
299
264
  - Presentation ID: {presentation_id}
300
265
  - Page ID: {page_object_id}
301
266
  - Thumbnail Size: {thumbnail_size}
302
267
  - Thumbnail URL: {thumbnail_url}
303
268
 
304
269
  You can view or download the thumbnail using the provided URL."""
305
-
306
- logger.info(f"Thumbnail generated successfully for {user_google_email}")
307
- return confirmation_message
308
-
309
- except HttpError as error:
310
- message = f"API error: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with the user's email ({user_google_email}) and service_name='Google Slides'."
311
- logger.error(message, exc_info=True)
312
- raise Exception(message)
313
- except Exception as e:
314
- message = f"Unexpected error: {e}."
315
- logger.exception(message)
316
- raise Exception(message)
270
+
271
+ logger.info(f"Thumbnail generated successfully for {user_google_email}")
272
+ return confirmation_message
main.py CHANGED
@@ -33,6 +33,12 @@ try:
33
33
  except Exception as e:
34
34
  sys.stderr.write(f"CRITICAL: Failed to set up file logging to '{log_file_path}': {e}\n")
35
35
 
36
+ def safe_print(text):
37
+ try:
38
+ print(text)
39
+ except UnicodeEncodeError:
40
+ print(text.encode('ascii', errors='replace').decode())
41
+
36
42
  def main():
37
43
  """
38
44
  Main entry point for the Google Workspace MCP server.
@@ -53,20 +59,20 @@ def main():
53
59
  port = int(os.getenv("PORT", os.getenv("WORKSPACE_MCP_PORT", 8000)))
54
60
  base_uri = os.getenv("WORKSPACE_MCP_BASE_URI", "http://localhost")
55
61
 
56
- print("🔧 Google Workspace MCP Server")
57
- print("=" * 35)
58
- print("📋 Server Information:")
62
+ safe_print("🔧 Google Workspace MCP Server")
63
+ safe_print("=" * 35)
64
+ safe_print("📋 Server Information:")
59
65
  try:
60
66
  version = metadata.version("workspace-mcp")
61
67
  except metadata.PackageNotFoundError:
62
68
  version = "dev"
63
- print(f" 📦 Version: {version}")
64
- print(f" 🌐 Transport: {args.transport}")
69
+ safe_print(f" 📦 Version: {version}")
70
+ safe_print(f" 🌐 Transport: {args.transport}")
65
71
  if args.transport == 'streamable-http':
66
- print(f" 🔗 URL: {base_uri}:{port}")
67
- print(f" 🔐 OAuth Callback: {base_uri}:{port}/oauth2callback")
68
- print(f" 👤 Mode: {'Single-user' if args.single_user else 'Multi-user'}")
69
- print(f" 🐍 Python: {sys.version.split()[0]}")
72
+ safe_print(f" 🔗 URL: {base_uri}:{port}")
73
+ safe_print(f" 🔐 OAuth Callback: {base_uri}:{port}/oauth2callback")
74
+ safe_print(f" 👤 Mode: {'Single-user' if args.single_user else 'Multi-user'}")
75
+ safe_print(f" 🐍 Python: {sys.version.split()[0]}")
70
76
  print()
71
77
 
72
78
  # Import tool modules to register them with the MCP server via decorators
@@ -94,32 +100,32 @@ def main():
94
100
 
95
101
  # Import specified tools or all tools if none specified
96
102
  tools_to_import = args.tools if args.tools is not None else tool_imports.keys()
97
- print(f"🛠️ Loading {len(tools_to_import)} tool module{'s' if len(tools_to_import) != 1 else ''}:")
103
+ safe_print(f"🛠️ Loading {len(tools_to_import)} tool module{'s' if len(tools_to_import) != 1 else ''}:")
98
104
  for tool in tools_to_import:
99
105
  tool_imports[tool]()
100
- print(f" {tool_icons[tool]} {tool.title()} - Google {tool.title()} API integration")
106
+ safe_print(f" {tool_icons[tool]} {tool.title()} - Google {tool.title()} API integration")
101
107
  print()
102
108
 
103
- print(f"📊 Configuration Summary:")
104
- print(f" 🔧 Tools Enabled: {len(tools_to_import)}/{len(tool_imports)}")
105
- print(f" 🔑 Auth Method: OAuth 2.0 with PKCE")
106
- print(f" 📝 Log Level: {logging.getLogger().getEffectiveLevel()}")
109
+ safe_print(f"📊 Configuration Summary:")
110
+ safe_print(f" 🔧 Tools Enabled: {len(tools_to_import)}/{len(tool_imports)}")
111
+ safe_print(f" 🔑 Auth Method: OAuth 2.0 with PKCE")
112
+ safe_print(f" 📝 Log Level: {logging.getLogger().getEffectiveLevel()}")
107
113
  print()
108
114
 
109
115
  # Set global single-user mode flag
110
116
  if args.single_user:
111
117
  os.environ['MCP_SINGLE_USER_MODE'] = '1'
112
- print("🔐 Single-user mode enabled")
118
+ safe_print("🔐 Single-user mode enabled")
113
119
  print()
114
120
 
115
121
  # Check credentials directory permissions before starting
116
122
  try:
117
- print("🔍 Checking credentials directory permissions...")
123
+ safe_print("🔍 Checking credentials directory permissions...")
118
124
  check_credentials_directory_permissions()
119
- print("✅ Credentials directory permissions verified")
125
+ safe_print("✅ Credentials directory permissions verified")
120
126
  print()
121
127
  except (PermissionError, OSError) as e:
122
- print(f"❌ Credentials directory permission check failed: {e}")
128
+ safe_print(f"❌ Credentials directory permission check failed: {e}")
123
129
  print(" Please ensure the service has write permissions to create/access the .credentials directory")
124
130
  logger.error(f"Failed credentials directory permission check: {e}")
125
131
  sys.exit(1)
@@ -129,15 +135,15 @@ def main():
129
135
  set_transport_mode(args.transport)
130
136
 
131
137
  if args.transport == 'streamable-http':
132
- print(f"🚀 Starting server on {base_uri}:{port}")
138
+ safe_print(f"🚀 Starting server on {base_uri}:{port}")
133
139
  else:
134
- print("🚀 Starting server in stdio mode")
140
+ safe_print("🚀 Starting server in stdio mode")
135
141
  # Start minimal OAuth callback server for stdio mode
136
142
  from auth.oauth_callback_server import ensure_oauth_callback_available
137
143
  if ensure_oauth_callback_available('stdio', port, base_uri):
138
144
  print(f" OAuth callback server started on {base_uri}:{port}/oauth2callback")
139
145
  else:
140
- print(" ⚠️ Warning: Failed to start OAuth callback server")
146
+ safe_print(" ⚠️ Warning: Failed to start OAuth callback server")
141
147
 
142
148
  print(" Ready for MCP connections!")
143
149
  print()
@@ -148,13 +154,13 @@ def main():
148
154
  else:
149
155
  server.run()
150
156
  except KeyboardInterrupt:
151
- print("\n👋 Server shutdown requested")
157
+ safe_print("\n👋 Server shutdown requested")
152
158
  # Clean up OAuth callback server if running
153
159
  from auth.oauth_callback_server import cleanup_oauth_callback_server
154
160
  cleanup_oauth_callback_server()
155
161
  sys.exit(0)
156
162
  except Exception as e:
157
- print(f"\n❌ Server error: {e}")
163
+ safe_print(f"\n❌ Server error: {e}")
158
164
  logger.error(f"Unexpected error running server: {e}", exc_info=True)
159
165
  # Clean up OAuth callback server if running
160
166
  from auth.oauth_callback_server import cleanup_oauth_callback_server
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workspace-mcp
3
- Version: 1.0.1
3
+ Version: 1.0.3
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
@@ -36,6 +36,7 @@ Requires-Dist: google-api-python-client>=2.168.0
36
36
  Requires-Dist: google-auth-httplib2>=0.2.0
37
37
  Requires-Dist: google-auth-oauthlib>=1.2.2
38
38
  Requires-Dist: httpx>=0.28.1
39
+ Requires-Dist: pyjwt>=2.10.1
39
40
  Requires-Dist: tomlkit
40
41
  Dynamic: license-file
41
42
 
@@ -48,10 +49,11 @@ Dynamic: license-file
48
49
  [![PyPI](https://img.shields.io/pypi/v/workspace-mcp.svg)](https://pypi.org/project/workspace-mcp/)
49
50
  [![UV](https://img.shields.io/badge/Package%20Installer-UV-blueviolet)](https://github.com/astral-sh/uv)
50
51
  [![Website](https://img.shields.io/badge/Website-workspacemcp.com-green.svg)](https://workspacemcp.com)
52
+ [![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/eebbc4a6-0f8c-41b2-ace8-038e5516dba0)
51
53
 
52
- **The world's most feature-complete Google Workspace MCP server**
54
+ **This is the single most feature-complete Google Workspace MCP server**
53
55
 
54
- *Connect MCP Clients, AI assistants and developer tools to Google Calendar, Drive, Gmail, Docs, Sheets, Slides, Forms, and Chat*
56
+ *Full natural language control over Google Calendar, Drive, Gmail, Docs, Sheets, Slides, Forms, and Chat through all MCP clients, AI assistants and developer tools*
55
57
 
56
58
  </div>
57
59
 
@@ -74,6 +76,20 @@ Dynamic: license-file
74
76
 
75
77
  ---
76
78
 
79
+ ## AI-Enhanced Documentation
80
+
81
+ > **This README was crafted with AI assistance, and here's why that matters**
82
+ >
83
+ > When people dismiss documentation as "AI-generated," they're missing the bigger picture
84
+
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.*
77
93
 
78
94
  ## 🌐 Overview
79
95
 
@@ -100,9 +116,13 @@ A production-ready MCP server that integrates all major Google Workspace service
100
116
 
101
117
  ### Simplest Start (uvx - Recommended)
102
118
 
103
- Run instantly without installation:
119
+ > 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.
104
120
 
105
121
  ```bash
122
+ # Set OAuth credentials via environment variables (recommended)
123
+ export GOOGLE_OAUTH_CLIENT_ID="your-client-id.apps.googleusercontent.com"
124
+ export GOOGLE_OAUTH_CLIENT_SECRET="your-client-secret"
125
+
106
126
  # Start the server with all Google Workspace tools
107
127
  uvx workspace-mcp
108
128
 
@@ -136,8 +156,31 @@ uv run main.py
136
156
  1. **Google Cloud Setup**:
137
157
  - Create OAuth 2.0 credentials (web application) in [Google Cloud Console](https://console.cloud.google.com/)
138
158
  - Enable APIs: Calendar, Drive, Gmail, Docs, Sheets, Slides, Forms, Chat
139
- - Download credentials as `client_secret.json` in project root
140
159
  - Add redirect URI: `http://localhost:8000/oauth2callback`
160
+ - Configure credentials using one of these methods:
161
+
162
+ **Option A: Environment Variables (Recommended for Production)**
163
+ ```bash
164
+ export GOOGLE_OAUTH_CLIENT_ID="your-client-id.apps.googleusercontent.com"
165
+ export GOOGLE_OAUTH_CLIENT_SECRET="your-client-secret"
166
+ export GOOGLE_OAUTH_REDIRECT_URI="http://localhost:8000/oauth2callback" # Optional
167
+ ```
168
+
169
+ **Option B: File-based (Traditional)**
170
+ - Download credentials as `client_secret.json` in project root
171
+ - To use a different location, set `GOOGLE_CLIENT_SECRET_PATH` (or legacy `GOOGLE_CLIENT_SECRETS`) environment variable with the file path
172
+
173
+ **Credential Loading Priority**:
174
+ 1. Environment variables (`GOOGLE_OAUTH_CLIENT_ID`, `GOOGLE_OAUTH_CLIENT_SECRET`)
175
+ 2. File specified by `GOOGLE_CLIENT_SECRET_PATH` or `GOOGLE_CLIENT_SECRETS` environment variable
176
+ 3. Default file (`client_secret.json` in project root)
177
+
178
+ **Why Environment Variables?**
179
+ - ✅ Containerized deployments (Docker, Kubernetes)
180
+ - ✅ Cloud platforms (Heroku, Railway, etc.)
181
+ - ✅ CI/CD pipelines
182
+ - ✅ No secrets in version control
183
+ - ✅ Easy credential rotation
141
184
 
142
185
  2. **Environment**:
143
186
  ```bash
@@ -195,7 +238,11 @@ python install_claude.py
195
238
  "mcpServers": {
196
239
  "google_workspace": {
197
240
  "command": "uvx",
198
- "args": ["workspace-mcp"]
241
+ "args": ["workspace-mcp"],
242
+ "env": {
243
+ "GOOGLE_OAUTH_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
244
+ "GOOGLE_OAUTH_CLIENT_SECRET": "your-client-secret"
245
+ }
199
246
  }
200
247
  }
201
248
  }
@@ -208,7 +255,11 @@ python install_claude.py
208
255
  "google_workspace": {
209
256
  "command": "uv",
210
257
  "args": ["run", "main.py"],
211
- "cwd": "/path/to/google_workspace_mcp"
258
+ "cwd": "/path/to/google_workspace_mcp",
259
+ "env": {
260
+ "GOOGLE_OAUTH_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
261
+ "GOOGLE_OAUTH_CLIENT_SECRET": "your-client-secret"
262
+ }
212
263
  }
213
264
  }
214
265
  }
@@ -256,7 +307,8 @@ When calling a tool:
256
307
  |------|-------------|
257
308
  | `list_calendars` | List accessible calendars |
258
309
  | `get_events` | Retrieve events with time range filtering |
259
- | `create_event` | Create events (all-day or timed) |
310
+ | `get_event` | Fetch detailed information of a single event by ID |
311
+ | `create_event` | Create events (all-day or timed) with optional Drive file attachments |
260
312
  | `modify_event` | Update existing events |
261
313
  | `delete_event` | Remove events |
262
314
 
@@ -267,7 +319,7 @@ When calling a tool:
267
319
  | `search_drive_files` | Search files with query syntax |
268
320
  | `get_drive_file_content` | Read file content (supports Office formats) |
269
321
  | `list_drive_items` | List folder contents |
270
- | `create_drive_file` | Create new files |
322
+ | `create_drive_file` | Create new files or fetch content from public URLs |
271
323
 
272
324
  ### 📧 Gmail ([`gmail_tools.py`](gmail/gmail_tools.py))
273
325
 
@@ -0,0 +1,33 @@
1
+ main.py,sha256=Mv5jfggqQ5XqzetKhccD2OmeWFnhidSbgzyKppCfywo,7078
2
+ auth/__init__.py,sha256=gPCU3GE-SLy91S3D3CbX-XfKBm6hteK_VSPKx7yjT5s,42
3
+ auth/google_auth.py,sha256=2UBbQgGcUPdUFWDbzdFy60NJLQ3SI45GIASzuzO1Tew,30717
4
+ auth/oauth_callback_server.py,sha256=igrur3fkZSY0bawufrH4AN9fMNpobUdAUp1BG7AQC6w,9341
5
+ auth/oauth_responses.py,sha256=qbirSB4d7mBRKcJKqGLrJxRAPaLHqObf9t-VMAq6UKA,7020
6
+ auth/scopes.py,sha256=kMRdFN0wLyipFkp7IitTHs-M6zhZD-oieVd7fylueBc,3320
7
+ auth/service_decorator.py,sha256=h9bkG1O6U-p4_yT1KseBKJvueprKd4SVJe1Bj2VrdXA,15669
8
+ core/__init__.py,sha256=AHVKdPl6v4lUFm2R-KuGuAgEmCyfxseMeLGtntMcqCs,43
9
+ core/context.py,sha256=zNgPXf9EO2EMs9sQkfKiywoy6sEOksVNgOrJMA_c30Y,768
10
+ core/server.py,sha256=fBPGMy9axIUOppsRFB31rlkaxEV32JRvaw2ZN_UZ9ms,9188
11
+ core/utils.py,sha256=2t5wbLtSLodxNKNAZb-jmR8Zg6mm-Rady-LpnXCP-1g,10297
12
+ gcalendar/__init__.py,sha256=D5fSdAwbeomoaj7XAdxSnIy-NVKNkpExs67175bOtfc,46
13
+ gcalendar/calendar_tools.py,sha256=SIiSJRxG3G9KsScow0pYwew600_PdtFqlOo-y2vXQRo,22144
14
+ gchat/__init__.py,sha256=XBjH4SbtULfZHgFCxk3moel5XqG599HCgZWl_veIncg,88
15
+ gchat/chat_tools.py,sha256=cIeXBBxWkFCdQNJ23BkX8IoDho6J8ZcfLsPjctUWyfA,7274
16
+ gdocs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ gdocs/docs_tools.py,sha256=AN9yG0c2AWCTd3bjc9guYBaOOF_PS_c_xKV6UMXclvU,13555
18
+ gdrive/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ gdrive/drive_tools.py,sha256=l-6IpHTstRMKIY2CU4DFTTNfEQ5rVbafgwo8BrbJ9Bk,15257
20
+ gforms/__init__.py,sha256=pL91XixrEp9YjpM-AYwONIEfeCP2OumkEG0Io5V4boE,37
21
+ gforms/forms_tools.py,sha256=reJF3qw9WwW6-aCOkS2x5jVBvdRx4Za8onEZBC57RXk,9663
22
+ gmail/__init__.py,sha256=l8PZ4_7Oet6ZE7tVu9oQ3-BaRAmI4YzAO86kf9uu6pU,60
23
+ gmail/gmail_tools.py,sha256=UIcws__Akw0kxbasc9fYH7rkzDw_7L-LJU1LQU_p-sA,24754
24
+ gsheets/__init__.py,sha256=jFfhD52w_EOVw6N5guf_dIc9eP2khW_eS9UAPJg_K3k,446
25
+ gsheets/sheets_tools.py,sha256=ctUvaA-3I-iGwCCHOk9Bloh5P7XQDqxBnFAxFTqCTPc,11466
26
+ 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,,