workspace-mcp 0.2.0__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/__init__.py +1 -0
- auth/google_auth.py +549 -0
- auth/oauth_callback_server.py +241 -0
- auth/oauth_responses.py +223 -0
- auth/scopes.py +108 -0
- auth/service_decorator.py +404 -0
- core/__init__.py +1 -0
- core/server.py +214 -0
- core/utils.py +162 -0
- gcalendar/__init__.py +1 -0
- gcalendar/calendar_tools.py +496 -0
- gchat/__init__.py +6 -0
- gchat/chat_tools.py +254 -0
- gdocs/__init__.py +0 -0
- gdocs/docs_tools.py +244 -0
- gdrive/__init__.py +0 -0
- gdrive/drive_tools.py +362 -0
- gforms/__init__.py +3 -0
- gforms/forms_tools.py +318 -0
- gmail/__init__.py +1 -0
- gmail/gmail_tools.py +807 -0
- gsheets/__init__.py +23 -0
- gsheets/sheets_tools.py +393 -0
- gslides/__init__.py +0 -0
- gslides/slides_tools.py +316 -0
- main.py +160 -0
- workspace_mcp-0.2.0.dist-info/METADATA +29 -0
- workspace_mcp-0.2.0.dist-info/RECORD +32 -0
- workspace_mcp-0.2.0.dist-info/WHEEL +5 -0
- workspace_mcp-0.2.0.dist-info/entry_points.txt +2 -0
- workspace_mcp-0.2.0.dist-info/licenses/LICENSE +21 -0
- workspace_mcp-0.2.0.dist-info/top_level.txt +11 -0
gslides/slides_tools.py
ADDED
@@ -0,0 +1,316 @@
|
|
1
|
+
"""
|
2
|
+
Google Slides MCP Tools
|
3
|
+
|
4
|
+
This module provides MCP tools for interacting with Google Slides API.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import asyncio
|
9
|
+
from typing import List, Optional, Dict, Any
|
10
|
+
|
11
|
+
from mcp import types
|
12
|
+
from googleapiclient.errors import HttpError
|
13
|
+
|
14
|
+
from auth.service_decorator import require_google_service
|
15
|
+
from core.server import server
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
@server.tool()
|
21
|
+
@require_google_service("slides", "slides")
|
22
|
+
async def create_presentation(
|
23
|
+
service,
|
24
|
+
user_google_email: str,
|
25
|
+
title: str = "Untitled Presentation"
|
26
|
+
) -> str:
|
27
|
+
"""
|
28
|
+
Create a new Google Slides presentation.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
user_google_email (str): The user's Google email address. Required.
|
32
|
+
title (str): The title for the new presentation. Defaults to "Untitled Presentation".
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
str: Details about the created presentation including ID and URL.
|
36
|
+
"""
|
37
|
+
logger.info(f"[create_presentation] Invoked. Email: '{user_google_email}', Title: '{title}'")
|
38
|
+
|
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}:
|
52
|
+
- Title: {title}
|
53
|
+
- Presentation ID: {presentation_id}
|
54
|
+
- URL: {presentation_url}
|
55
|
+
- 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)
|
68
|
+
|
69
|
+
|
70
|
+
@server.tool()
|
71
|
+
@require_google_service("slides", "slides_read")
|
72
|
+
async def get_presentation(
|
73
|
+
service,
|
74
|
+
user_google_email: str,
|
75
|
+
presentation_id: str
|
76
|
+
) -> str:
|
77
|
+
"""
|
78
|
+
Get details about a Google Slides presentation.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
user_google_email (str): The user's Google email address. Required.
|
82
|
+
presentation_id (str): The ID of the presentation to retrieve.
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
str: Details about the presentation including title, slides count, and metadata.
|
86
|
+
"""
|
87
|
+
logger.info(f"[get_presentation] Invoked. Email: '{user_google_email}', ID: '{presentation_id}'")
|
88
|
+
|
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}:
|
105
|
+
- Title: {title}
|
106
|
+
- Presentation ID: {presentation_id}
|
107
|
+
- URL: https://docs.google.com/presentation/d/{presentation_id}/edit
|
108
|
+
- Total Slides: {len(slides)}
|
109
|
+
- Page Size: {page_size.get('width', {}).get('magnitude', 'Unknown')} x {page_size.get('height', {}).get('magnitude', 'Unknown')} {page_size.get('width', {}).get('unit', '')}
|
110
|
+
|
111
|
+
Slides Breakdown:
|
112
|
+
{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)
|
125
|
+
|
126
|
+
|
127
|
+
@server.tool()
|
128
|
+
@require_google_service("slides", "slides")
|
129
|
+
async def batch_update_presentation(
|
130
|
+
service,
|
131
|
+
user_google_email: str,
|
132
|
+
presentation_id: str,
|
133
|
+
requests: List[Dict[str, Any]]
|
134
|
+
) -> str:
|
135
|
+
"""
|
136
|
+
Apply batch updates to a Google Slides presentation.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
user_google_email (str): The user's Google email address. Required.
|
140
|
+
presentation_id (str): The ID of the presentation to update.
|
141
|
+
requests (List[Dict[str, Any]]): List of update requests to apply.
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
str: Details about the batch update operation results.
|
145
|
+
"""
|
146
|
+
logger.info(f"[batch_update_presentation] Invoked. Email: '{user_google_email}', ID: '{presentation_id}', Requests: {len(requests)}")
|
147
|
+
|
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}:
|
163
|
+
- Presentation ID: {presentation_id}
|
164
|
+
- URL: https://docs.google.com/presentation/d/{presentation_id}/edit
|
165
|
+
- Requests Applied: {len(requests)}
|
166
|
+
- 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)
|
191
|
+
|
192
|
+
|
193
|
+
@server.tool()
|
194
|
+
@require_google_service("slides", "slides_read")
|
195
|
+
async def get_page(
|
196
|
+
service,
|
197
|
+
user_google_email: str,
|
198
|
+
presentation_id: str,
|
199
|
+
page_object_id: str
|
200
|
+
) -> str:
|
201
|
+
"""
|
202
|
+
Get details about a specific page (slide) in a presentation.
|
203
|
+
|
204
|
+
Args:
|
205
|
+
user_google_email (str): The user's Google email address. Required.
|
206
|
+
presentation_id (str): The ID of the presentation.
|
207
|
+
page_object_id (str): The object ID of the page/slide to retrieve.
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
str: Details about the specific page including elements and layout.
|
211
|
+
"""
|
212
|
+
logger.info(f"[get_page] Invoked. Email: '{user_google_email}', Presentation: '{presentation_id}', Page: '{page_object_id}'")
|
213
|
+
|
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}:
|
243
|
+
- Presentation ID: {presentation_id}
|
244
|
+
- Page ID: {page_object_id}
|
245
|
+
- Page Type: {page_type}
|
246
|
+
- Total Elements: {len(page_elements)}
|
247
|
+
|
248
|
+
Page Elements:
|
249
|
+
{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)
|
262
|
+
|
263
|
+
|
264
|
+
@server.tool()
|
265
|
+
@require_google_service("slides", "slides_read")
|
266
|
+
async def get_page_thumbnail(
|
267
|
+
service,
|
268
|
+
user_google_email: str,
|
269
|
+
presentation_id: str,
|
270
|
+
page_object_id: str,
|
271
|
+
thumbnail_size: str = "MEDIUM"
|
272
|
+
) -> str:
|
273
|
+
"""
|
274
|
+
Generate a thumbnail URL for a specific page (slide) in a presentation.
|
275
|
+
|
276
|
+
Args:
|
277
|
+
user_google_email (str): The user's Google email address. Required.
|
278
|
+
presentation_id (str): The ID of the presentation.
|
279
|
+
page_object_id (str): The object ID of the page/slide.
|
280
|
+
thumbnail_size (str): Size of thumbnail ("LARGE", "MEDIUM", "SMALL"). Defaults to "MEDIUM".
|
281
|
+
|
282
|
+
Returns:
|
283
|
+
str: URL to the generated thumbnail image.
|
284
|
+
"""
|
285
|
+
logger.info(f"[get_page_thumbnail] Invoked. Email: '{user_google_email}', Presentation: '{presentation_id}', Page: '{page_object_id}', Size: '{thumbnail_size}'")
|
286
|
+
|
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}:
|
299
|
+
- Presentation ID: {presentation_id}
|
300
|
+
- Page ID: {page_object_id}
|
301
|
+
- Thumbnail Size: {thumbnail_size}
|
302
|
+
- Thumbnail URL: {thumbnail_url}
|
303
|
+
|
304
|
+
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)
|
main.py
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
import argparse
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
import sys
|
5
|
+
|
6
|
+
# Local imports
|
7
|
+
from core.server import server, set_transport_mode
|
8
|
+
from core.utils import check_credentials_directory_permissions
|
9
|
+
|
10
|
+
logging.basicConfig(
|
11
|
+
level=logging.INFO,
|
12
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
13
|
+
)
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
try:
|
17
|
+
root_logger = logging.getLogger()
|
18
|
+
log_file_dir = os.path.dirname(os.path.abspath(__file__))
|
19
|
+
log_file_path = os.path.join(log_file_dir, 'mcp_server_debug.log')
|
20
|
+
|
21
|
+
file_handler = logging.FileHandler(log_file_path, mode='a')
|
22
|
+
file_handler.setLevel(logging.DEBUG)
|
23
|
+
|
24
|
+
file_formatter = logging.Formatter(
|
25
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(process)d - %(threadName)s '
|
26
|
+
'[%(module)s.%(funcName)s:%(lineno)d] - %(message)s'
|
27
|
+
)
|
28
|
+
file_handler.setFormatter(file_formatter)
|
29
|
+
root_logger.addHandler(file_handler)
|
30
|
+
|
31
|
+
logger.debug(f"Detailed file logging configured to: {log_file_path}")
|
32
|
+
except Exception as e:
|
33
|
+
sys.stderr.write(f"CRITICAL: Failed to set up file logging to '{log_file_path}': {e}\n")
|
34
|
+
|
35
|
+
def main():
|
36
|
+
"""
|
37
|
+
Main entry point for the Google Workspace MCP server.
|
38
|
+
Uses FastMCP's native streamable-http transport.
|
39
|
+
"""
|
40
|
+
# Parse command line arguments
|
41
|
+
parser = argparse.ArgumentParser(description='Google Workspace MCP Server')
|
42
|
+
parser.add_argument('--single-user', action='store_true',
|
43
|
+
help='Run in single-user mode - bypass session mapping and use any credentials from ./credentials directory')
|
44
|
+
parser.add_argument('--tools', nargs='*',
|
45
|
+
choices=['gmail', 'drive', 'calendar', 'docs', 'sheets', 'chat', 'forms', 'slides'],
|
46
|
+
help='Specify which tools to register. If not provided, all tools are registered.')
|
47
|
+
parser.add_argument('--transport', choices=['stdio', 'streamable-http'], default='stdio',
|
48
|
+
help='Transport mode: stdio (default) or streamable-http')
|
49
|
+
args = parser.parse_args()
|
50
|
+
|
51
|
+
# Set port and base URI once for reuse throughout the function
|
52
|
+
port = int(os.getenv("PORT", os.getenv("WORKSPACE_MCP_PORT", 8000)))
|
53
|
+
base_uri = os.getenv("WORKSPACE_MCP_BASE_URI", "http://localhost")
|
54
|
+
|
55
|
+
print("š§ Google Workspace MCP Server")
|
56
|
+
print("=" * 35)
|
57
|
+
print("š Server Information:")
|
58
|
+
print(f" š¦ Version: 0.1.1")
|
59
|
+
print(f" š Transport: {args.transport}")
|
60
|
+
if args.transport == 'streamable-http':
|
61
|
+
print(f" š URL: {base_uri}:{port}")
|
62
|
+
print(f" š OAuth Callback: {base_uri}:{port}/oauth2callback")
|
63
|
+
print(f" š¤ Mode: {'Single-user' if args.single_user else 'Multi-user'}")
|
64
|
+
print(f" š Python: {sys.version.split()[0]}")
|
65
|
+
print()
|
66
|
+
|
67
|
+
# Import tool modules to register them with the MCP server via decorators
|
68
|
+
tool_imports = {
|
69
|
+
'gmail': lambda: __import__('gmail.gmail_tools'),
|
70
|
+
'drive': lambda: __import__('gdrive.drive_tools'),
|
71
|
+
'calendar': lambda: __import__('gcalendar.calendar_tools'),
|
72
|
+
'docs': lambda: __import__('gdocs.docs_tools'),
|
73
|
+
'sheets': lambda: __import__('gsheets.sheets_tools'),
|
74
|
+
'chat': lambda: __import__('gchat.chat_tools'),
|
75
|
+
'forms': lambda: __import__('gforms.forms_tools'),
|
76
|
+
'slides': lambda: __import__('gslides.slides_tools')
|
77
|
+
}
|
78
|
+
|
79
|
+
tool_icons = {
|
80
|
+
'gmail': 'š§',
|
81
|
+
'drive': 'š',
|
82
|
+
'calendar': 'š
',
|
83
|
+
'docs': 'š',
|
84
|
+
'sheets': 'š',
|
85
|
+
'chat': 'š¬',
|
86
|
+
'forms': 'š',
|
87
|
+
'slides': 'š¼ļø'
|
88
|
+
}
|
89
|
+
|
90
|
+
# Import specified tools or all tools if none specified
|
91
|
+
tools_to_import = args.tools if args.tools is not None else tool_imports.keys()
|
92
|
+
print(f"š ļø Loading {len(tools_to_import)} tool module{'s' if len(tools_to_import) != 1 else ''}:")
|
93
|
+
for tool in tools_to_import:
|
94
|
+
tool_imports[tool]()
|
95
|
+
print(f" {tool_icons[tool]} {tool.title()} - Google {tool.title()} API integration")
|
96
|
+
print()
|
97
|
+
|
98
|
+
print(f"š Configuration Summary:")
|
99
|
+
print(f" š§ Tools Enabled: {len(tools_to_import)}/{len(tool_imports)}")
|
100
|
+
print(f" š Auth Method: OAuth 2.0 with PKCE")
|
101
|
+
print(f" š Log Level: {logging.getLogger().getEffectiveLevel()}")
|
102
|
+
print()
|
103
|
+
|
104
|
+
# Set global single-user mode flag
|
105
|
+
if args.single_user:
|
106
|
+
os.environ['MCP_SINGLE_USER_MODE'] = '1'
|
107
|
+
print("š Single-user mode enabled")
|
108
|
+
print()
|
109
|
+
|
110
|
+
# Check credentials directory permissions before starting
|
111
|
+
try:
|
112
|
+
print("š Checking credentials directory permissions...")
|
113
|
+
check_credentials_directory_permissions()
|
114
|
+
print("ā
Credentials directory permissions verified")
|
115
|
+
print()
|
116
|
+
except (PermissionError, OSError) as e:
|
117
|
+
print(f"ā Credentials directory permission check failed: {e}")
|
118
|
+
print(" Please ensure the service has write permissions to create/access the .credentials directory")
|
119
|
+
logger.error(f"Failed credentials directory permission check: {e}")
|
120
|
+
sys.exit(1)
|
121
|
+
|
122
|
+
try:
|
123
|
+
# Set transport mode for OAuth callback handling
|
124
|
+
set_transport_mode(args.transport)
|
125
|
+
|
126
|
+
if args.transport == 'streamable-http':
|
127
|
+
print(f"š Starting server on {base_uri}:{port}")
|
128
|
+
else:
|
129
|
+
print("š Starting server in stdio mode")
|
130
|
+
# Start minimal OAuth callback server for stdio mode
|
131
|
+
from auth.oauth_callback_server import ensure_oauth_callback_available
|
132
|
+
if ensure_oauth_callback_available('stdio', port, base_uri):
|
133
|
+
print(f" OAuth callback server started on {base_uri}:{port}/oauth2callback")
|
134
|
+
else:
|
135
|
+
print(" ā ļø Warning: Failed to start OAuth callback server")
|
136
|
+
|
137
|
+
print(" Ready for MCP connections!")
|
138
|
+
print()
|
139
|
+
|
140
|
+
if args.transport == 'streamable-http':
|
141
|
+
# The server is already configured with port and server_url in core/server.py
|
142
|
+
server.run(transport="streamable-http")
|
143
|
+
else:
|
144
|
+
server.run()
|
145
|
+
except KeyboardInterrupt:
|
146
|
+
print("\nš Server shutdown requested")
|
147
|
+
# Clean up OAuth callback server if running
|
148
|
+
from auth.oauth_callback_server import cleanup_oauth_callback_server
|
149
|
+
cleanup_oauth_callback_server()
|
150
|
+
sys.exit(0)
|
151
|
+
except Exception as e:
|
152
|
+
print(f"\nā Server error: {e}")
|
153
|
+
logger.error(f"Unexpected error running server: {e}", exc_info=True)
|
154
|
+
# Clean up OAuth callback server if running
|
155
|
+
from auth.oauth_callback_server import cleanup_oauth_callback_server
|
156
|
+
cleanup_oauth_callback_server()
|
157
|
+
sys.exit(1)
|
158
|
+
|
159
|
+
if __name__ == "__main__":
|
160
|
+
main()
|
@@ -0,0 +1,29 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: workspace-mcp
|
3
|
+
Version: 0.2.0
|
4
|
+
Summary: Comprehensive, highly performant Google Workspace Streamable HTTP & SSE MCP Server for Calendar, Gmail, Docs, Sheets, Slides & Drive
|
5
|
+
Author-email: Taylor Wilsdon <taylor@taylorwilsdon.com>
|
6
|
+
License-Expression: MIT
|
7
|
+
Keywords: mcp,google,workspace,llm,ai,claude,model,context,protocol,server
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
9
|
+
Classifier: Environment :: Console
|
10
|
+
Classifier: Intended Audience :: Developers
|
11
|
+
Classifier: Natural Language :: English
|
12
|
+
Classifier: Operating System :: OS Independent
|
13
|
+
Classifier: Programming Language :: Python
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
20
|
+
Classifier: Typing :: Typed
|
21
|
+
Requires-Python: >=3.11
|
22
|
+
License-File: LICENSE
|
23
|
+
Requires-Dist: fastapi>=0.115.12
|
24
|
+
Requires-Dist: fastmcp>=2.3.3
|
25
|
+
Requires-Dist: google-api-python-client>=2.168.0
|
26
|
+
Requires-Dist: google-auth-httplib2>=0.2.0
|
27
|
+
Requires-Dist: google-auth-oauthlib>=1.2.2
|
28
|
+
Requires-Dist: httpx>=0.28.1
|
29
|
+
Dynamic: license-file
|
@@ -0,0 +1,32 @@
|
|
1
|
+
main.py,sha256=0BYBsSW2f-hLOhAZpSlqLNA3ccvmV-yOSt4ZY8qAjJA,6650
|
2
|
+
auth/__init__.py,sha256=gPCU3GE-SLy91S3D3CbX-XfKBm6hteK_VSPKx7yjT5s,42
|
3
|
+
auth/google_auth.py,sha256=OdKFI7kW-R0peZutv9VXCnDvxH5jAtbeFYiXK9sxqjQ,25998
|
4
|
+
auth/oauth_callback_server.py,sha256=-vaaLO9f7oz6QsHUXjRFHjlEleFm0Y9ccBB33YX22wQ,9177
|
5
|
+
auth/oauth_responses.py,sha256=qbirSB4d7mBRKcJKqGLrJxRAPaLHqObf9t-VMAq6UKA,7020
|
6
|
+
auth/scopes.py,sha256=kMRdFN0wLyipFkp7IitTHs-M6zhZD-oieVd7fylueBc,3320
|
7
|
+
auth/service_decorator.py,sha256=jux1Fboa_ncau_5XcGekSpuD1USPvmIpDu9hNBA15Vk,15267
|
8
|
+
core/__init__.py,sha256=AHVKdPl6v4lUFm2R-KuGuAgEmCyfxseMeLGtntMcqCs,43
|
9
|
+
core/server.py,sha256=s89ptSJ9f1MFZ6pxoA0f0Ue3yHVZl6Ukndx4ZcQ0Y1M,9682
|
10
|
+
core/utils.py,sha256=Xsc2E0LSQl-tjcC95CtnmNFTJi8K8iwJvTUIjbRtu4o,8830
|
11
|
+
gcalendar/__init__.py,sha256=D5fSdAwbeomoaj7XAdxSnIy-NVKNkpExs67175bOtfc,46
|
12
|
+
gcalendar/calendar_tools.py,sha256=ptS_iyi6JBPhstbdPnRD5ruZtKzkAEbfaimiPabYLTo,20982
|
13
|
+
gchat/__init__.py,sha256=XBjH4SbtULfZHgFCxk3moel5XqG599HCgZWl_veIncg,88
|
14
|
+
gchat/chat_tools.py,sha256=jm2trdYhfqNfsMfoAGuhzkEvjA8tSOL1d4QwSPv9K1w,8832
|
15
|
+
gdocs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
+
gdocs/docs_tools.py,sha256=3530NcSFkWemMrJ6JRrTQuFUaZLv6gYSU8BIWdOPgak,9571
|
17
|
+
gdrive/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
|
+
gdrive/drive_tools.py,sha256=yBpV5DKwyhd83M2gSdwr3X-oWmAyBtpbl9L2JIdwNNU,15708
|
19
|
+
gforms/__init__.py,sha256=pL91XixrEp9YjpM-AYwONIEfeCP2OumkEG0Io5V4boE,37
|
20
|
+
gforms/forms_tools.py,sha256=8dX3JVFWqQUESXTi7WgynqZh7FIufcXZI1c3ph_F3bk,12248
|
21
|
+
gmail/__init__.py,sha256=l8PZ4_7Oet6ZE7tVu9oQ3-BaRAmI4YzAO86kf9uu6pU,60
|
22
|
+
gmail/gmail_tools.py,sha256=sit06sqietJ4fNbr1S1WfucxAsf3wWmoFte-fBHzfPk,29057
|
23
|
+
gsheets/__init__.py,sha256=jFfhD52w_EOVw6N5guf_dIc9eP2khW_eS9UAPJg_K3k,446
|
24
|
+
gsheets/sheets_tools.py,sha256=mZU7kf3kRRhjz406rDBlVtwNMAjye2F81W1cveXlBFY,14481
|
25
|
+
gslides/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
|
+
gslides/slides_tools.py,sha256=NAkApdjkX7GtZRksy7fLmjmbsa1khkeAJKNN6NZe0t8,11846
|
27
|
+
workspace_mcp-0.2.0.dist-info/licenses/LICENSE,sha256=bB8L7rIyRy5o-WHxGgvRuY8hUTzNu4h3DTkvyV8XFJo,1070
|
28
|
+
workspace_mcp-0.2.0.dist-info/METADATA,sha256=G9XQNqZbqoQegiamRh9wsIbpAbLkl7Jxx1D7xElAFA0,1287
|
29
|
+
workspace_mcp-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
30
|
+
workspace_mcp-0.2.0.dist-info/entry_points.txt,sha256=FMg6KOfvwWgrLe_qDKr1Cm8NFdZ_44JnrF9FMw1EE1Q,51
|
31
|
+
workspace_mcp-0.2.0.dist-info/top_level.txt,sha256=Y8mAkTitLNE2zZEJ-DbqR9R7Cs1V1MMf-UploVdOvlw,73
|
32
|
+
workspace_mcp-0.2.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Taylor Wilsdon
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|