workspace-mcp 1.0.0__py3-none-any.whl → 1.0.2__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
- version = metadata.version("google-workspace-mcp")
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