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.
- auth/google_auth.py +120 -12
- auth/oauth_callback_server.py +7 -3
- auth/service_decorator.py +31 -32
- core/context.py +22 -0
- core/server.py +5 -7
- core/utils.py +36 -0
- gcalendar/calendar_tools.py +308 -258
- gchat/chat_tools.py +131 -158
- gdocs/docs_tools.py +299 -149
- gdrive/drive_tools.py +168 -171
- gforms/forms_tools.py +118 -157
- gmail/gmail_tools.py +319 -400
- gsheets/sheets_tools.py +144 -197
- gslides/slides_tools.py +113 -157
- main.py +30 -24
- {workspace_mcp-1.0.1.dist-info → workspace_mcp-1.0.3.dist-info}/METADATA +61 -9
- workspace_mcp-1.0.3.dist-info/RECORD +33 -0
- workspace_mcp-1.0.1.dist-info/RECORD +0 -32
- {workspace_mcp-1.0.1.dist-info → workspace_mcp-1.0.3.dist-info}/WHEEL +0 -0
- {workspace_mcp-1.0.1.dist-info → workspace_mcp-1.0.3.dist-info}/entry_points.txt +0 -0
- {workspace_mcp-1.0.1.dist-info → workspace_mcp-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {workspace_mcp-1.0.1.dist-info → workspace_mcp-1.0.3.dist-info}/top_level.txt +0 -0
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
58
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
115
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
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
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
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
|
-
|
252
|
-
|
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
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
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
|
-
|
307
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
64
|
-
|
69
|
+
safe_print(f" 📦 Version: {version}")
|
70
|
+
safe_print(f" 🌐 Transport: {args.transport}")
|
65
71
|
if args.transport == 'streamable-http':
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
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
|
-
|
106
|
+
safe_print(f" {tool_icons[tool]} {tool.title()} - Google {tool.title()} API integration")
|
101
107
|
print()
|
102
108
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
118
|
+
safe_print("🔐 Single-user mode enabled")
|
113
119
|
print()
|
114
120
|
|
115
121
|
# Check credentials directory permissions before starting
|
116
122
|
try:
|
117
|
-
|
123
|
+
safe_print("🔍 Checking credentials directory permissions...")
|
118
124
|
check_credentials_directory_permissions()
|
119
|
-
|
125
|
+
safe_print("✅ Credentials directory permissions verified")
|
120
126
|
print()
|
121
127
|
except (PermissionError, OSError) as e:
|
122
|
-
|
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
|
-
|
138
|
+
safe_print(f"🚀 Starting server on {base_uri}:{port}")
|
133
139
|
else:
|
134
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
[](https://pypi.org/project/workspace-mcp/)
|
49
50
|
[](https://github.com/astral-sh/uv)
|
50
51
|
[](https://workspacemcp.com)
|
52
|
+
[](https://mseep.ai/app/eebbc4a6-0f8c-41b2-ace8-038e5516dba0)
|
51
53
|
|
52
|
-
**
|
54
|
+
**This is the single most feature-complete Google Workspace MCP server**
|
53
55
|
|
54
|
-
*
|
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
|
-
| `
|
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,,
|