claude-mpm 5.6.23__py3-none-any.whl → 5.6.73__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.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/VERSION +1 -1
- claude_mpm/auth/__init__.py +35 -0
- claude_mpm/auth/callback_server.py +328 -0
- claude_mpm/auth/models.py +104 -0
- claude_mpm/auth/oauth_manager.py +266 -0
- claude_mpm/auth/providers/__init__.py +12 -0
- claude_mpm/auth/providers/base.py +165 -0
- claude_mpm/auth/providers/google.py +261 -0
- claude_mpm/auth/token_storage.py +252 -0
- claude_mpm/cli/commands/commander.py +6 -6
- claude_mpm/cli/commands/mcp.py +29 -17
- claude_mpm/cli/commands/mcp_command_router.py +39 -0
- claude_mpm/cli/commands/mcp_service_commands.py +304 -0
- claude_mpm/cli/commands/oauth.py +481 -0
- claude_mpm/cli/executor.py +9 -0
- claude_mpm/cli/helpers.py +1 -1
- claude_mpm/cli/parsers/base_parser.py +13 -0
- claude_mpm/cli/parsers/mcp_parser.py +79 -0
- claude_mpm/cli/parsers/oauth_parser.py +165 -0
- claude_mpm/cli/startup.py +150 -33
- claude_mpm/cli/startup_display.py +3 -2
- claude_mpm/commander/chat/cli.py +5 -2
- claude_mpm/commander/chat/commands.py +42 -16
- claude_mpm/commander/chat/repl.py +1581 -70
- claude_mpm/commander/events/manager.py +61 -1
- claude_mpm/commander/frameworks/base.py +87 -0
- claude_mpm/commander/frameworks/mpm.py +9 -14
- claude_mpm/commander/git/__init__.py +5 -0
- claude_mpm/commander/git/worktree_manager.py +212 -0
- claude_mpm/commander/instance_manager.py +428 -13
- claude_mpm/commander/models/events.py +6 -0
- claude_mpm/commander/persistence/state_store.py +95 -1
- claude_mpm/commander/tmux_orchestrator.py +3 -2
- claude_mpm/constants.py +5 -0
- claude_mpm/core/hook_manager.py +2 -1
- claude_mpm/core/logging_utils.py +4 -2
- claude_mpm/core/output_style_manager.py +5 -2
- claude_mpm/core/socketio_pool.py +34 -10
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
- claude_mpm/hooks/claude_hooks/event_handlers.py +206 -94
- claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
- claude_mpm/hooks/claude_hooks/installer.py +175 -51
- claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
- claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +2 -2
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +2 -2
- claude_mpm/hooks/claude_hooks/services/container.py +326 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
- claude_mpm/init.py +21 -14
- claude_mpm/mcp/__init__.py +9 -0
- claude_mpm/mcp/google_workspace_server.py +610 -0
- claude_mpm/scripts/claude-hook-handler.sh +3 -3
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/mcp_config_manager.py +99 -19
- claude_mpm/services/mcp_service_registry.py +294 -0
- claude_mpm/services/monitor/server.py +6 -1
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/METADATA +24 -1
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/RECORD +69 -64
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/WHEEL +1 -1
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/entry_points.txt +2 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
"""Google Workspace MCP server integrated with claude-mpm OAuth storage.
|
|
2
|
+
|
|
3
|
+
This MCP server provides tools for interacting with Google Workspace APIs
|
|
4
|
+
(Calendar, Gmail, Drive) using OAuth tokens managed by claude-mpm's
|
|
5
|
+
TokenStorage system.
|
|
6
|
+
|
|
7
|
+
The server automatically handles token refresh when tokens expire,
|
|
8
|
+
using the OAuthManager for seamless re-authentication.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
from mcp.server import Server
|
|
18
|
+
from mcp.server.stdio import stdio_server
|
|
19
|
+
from mcp.types import TextContent, Tool
|
|
20
|
+
|
|
21
|
+
from claude_mpm.auth import OAuthManager, TokenStatus, TokenStorage
|
|
22
|
+
|
|
23
|
+
# Configure logging
|
|
24
|
+
logging.basicConfig(level=logging.INFO)
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Service name for token storage - matches workspace-mcp convention
|
|
28
|
+
SERVICE_NAME = "workspace-mcp"
|
|
29
|
+
|
|
30
|
+
# Google API base URLs
|
|
31
|
+
CALENDAR_API_BASE = "https://www.googleapis.com/calendar/v3"
|
|
32
|
+
GMAIL_API_BASE = "https://gmail.googleapis.com/gmail/v1"
|
|
33
|
+
DRIVE_API_BASE = "https://www.googleapis.com/drive/v3"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GoogleWorkspaceServer:
|
|
37
|
+
"""MCP server for Google Workspace APIs.
|
|
38
|
+
|
|
39
|
+
Integrates with claude-mpm's TokenStorage for credential management
|
|
40
|
+
and provides tools for Calendar, Gmail, and Drive operations.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
server: MCP Server instance.
|
|
44
|
+
storage: TokenStorage for retrieving OAuth tokens.
|
|
45
|
+
manager: OAuthManager for token refresh operations.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self) -> None:
|
|
49
|
+
"""Initialize the Google Workspace MCP server."""
|
|
50
|
+
self.server = Server("google-workspace-mpm")
|
|
51
|
+
self.storage = TokenStorage()
|
|
52
|
+
self.manager = OAuthManager(storage=self.storage)
|
|
53
|
+
self._setup_handlers()
|
|
54
|
+
|
|
55
|
+
def _setup_handlers(self) -> None:
|
|
56
|
+
"""Register MCP tool handlers."""
|
|
57
|
+
|
|
58
|
+
@self.server.list_tools()
|
|
59
|
+
async def list_tools() -> list[Tool]:
|
|
60
|
+
"""Return list of available tools."""
|
|
61
|
+
return [
|
|
62
|
+
Tool(
|
|
63
|
+
name="list_calendars",
|
|
64
|
+
description="List all calendars accessible by the authenticated user",
|
|
65
|
+
inputSchema={
|
|
66
|
+
"type": "object",
|
|
67
|
+
"properties": {},
|
|
68
|
+
"required": [],
|
|
69
|
+
},
|
|
70
|
+
),
|
|
71
|
+
Tool(
|
|
72
|
+
name="get_events",
|
|
73
|
+
description="Get events from a calendar within a time range",
|
|
74
|
+
inputSchema={
|
|
75
|
+
"type": "object",
|
|
76
|
+
"properties": {
|
|
77
|
+
"calendar_id": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"description": "Calendar ID (default: 'primary')",
|
|
80
|
+
"default": "primary",
|
|
81
|
+
},
|
|
82
|
+
"time_min": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"description": "Start time in RFC3339 format (e.g., '2024-01-01T00:00:00Z')",
|
|
85
|
+
},
|
|
86
|
+
"time_max": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"description": "End time in RFC3339 format",
|
|
89
|
+
},
|
|
90
|
+
"max_results": {
|
|
91
|
+
"type": "integer",
|
|
92
|
+
"description": "Maximum number of events to return (default: 10)",
|
|
93
|
+
"default": 10,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
"required": [],
|
|
97
|
+
},
|
|
98
|
+
),
|
|
99
|
+
Tool(
|
|
100
|
+
name="search_gmail_messages",
|
|
101
|
+
description="Search Gmail messages using a query string",
|
|
102
|
+
inputSchema={
|
|
103
|
+
"type": "object",
|
|
104
|
+
"properties": {
|
|
105
|
+
"query": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"description": "Gmail search query (e.g., 'from:user@example.com subject:meeting')",
|
|
108
|
+
},
|
|
109
|
+
"max_results": {
|
|
110
|
+
"type": "integer",
|
|
111
|
+
"description": "Maximum number of messages to return (default: 10)",
|
|
112
|
+
"default": 10,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
"required": ["query"],
|
|
116
|
+
},
|
|
117
|
+
),
|
|
118
|
+
Tool(
|
|
119
|
+
name="get_gmail_message_content",
|
|
120
|
+
description="Get the full content of a Gmail message by ID",
|
|
121
|
+
inputSchema={
|
|
122
|
+
"type": "object",
|
|
123
|
+
"properties": {
|
|
124
|
+
"message_id": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"description": "Gmail message ID",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
"required": ["message_id"],
|
|
130
|
+
},
|
|
131
|
+
),
|
|
132
|
+
Tool(
|
|
133
|
+
name="search_drive_files",
|
|
134
|
+
description="Search Google Drive files using a query string",
|
|
135
|
+
inputSchema={
|
|
136
|
+
"type": "object",
|
|
137
|
+
"properties": {
|
|
138
|
+
"query": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"description": "Drive search query (e.g., 'name contains \"report\"')",
|
|
141
|
+
},
|
|
142
|
+
"max_results": {
|
|
143
|
+
"type": "integer",
|
|
144
|
+
"description": "Maximum number of files to return (default: 10)",
|
|
145
|
+
"default": 10,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
"required": ["query"],
|
|
149
|
+
},
|
|
150
|
+
),
|
|
151
|
+
Tool(
|
|
152
|
+
name="get_drive_file_content",
|
|
153
|
+
description="Get the content of a Google Drive file by ID (text files only)",
|
|
154
|
+
inputSchema={
|
|
155
|
+
"type": "object",
|
|
156
|
+
"properties": {
|
|
157
|
+
"file_id": {
|
|
158
|
+
"type": "string",
|
|
159
|
+
"description": "Google Drive file ID",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
"required": ["file_id"],
|
|
163
|
+
},
|
|
164
|
+
),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
@self.server.call_tool()
|
|
168
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
|
169
|
+
"""Handle tool calls."""
|
|
170
|
+
try:
|
|
171
|
+
result = await self._dispatch_tool(name, arguments)
|
|
172
|
+
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.exception(f"Error calling tool {name}")
|
|
175
|
+
return [
|
|
176
|
+
TextContent(
|
|
177
|
+
type="text",
|
|
178
|
+
text=json.dumps({"error": str(e)}, indent=2),
|
|
179
|
+
)
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
async def _get_access_token(self) -> str:
|
|
183
|
+
"""Get a valid access token, refreshing if necessary.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Valid access token string.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
RuntimeError: If no token is available or refresh fails.
|
|
190
|
+
"""
|
|
191
|
+
status = self.storage.get_status(SERVICE_NAME)
|
|
192
|
+
|
|
193
|
+
if status == TokenStatus.MISSING:
|
|
194
|
+
raise RuntimeError(
|
|
195
|
+
f"No OAuth token found for service '{SERVICE_NAME}'. "
|
|
196
|
+
"Please authenticate first using: claude-mpm auth login google"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if status == TokenStatus.INVALID:
|
|
200
|
+
raise RuntimeError(
|
|
201
|
+
f"OAuth token for service '{SERVICE_NAME}' is invalid or corrupted. "
|
|
202
|
+
"Please re-authenticate using: claude-mpm auth login google"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Try to refresh if expired
|
|
206
|
+
if status == TokenStatus.EXPIRED:
|
|
207
|
+
logger.info("Token expired, attempting refresh...")
|
|
208
|
+
token = await self.manager.refresh_if_needed(SERVICE_NAME)
|
|
209
|
+
if token is None:
|
|
210
|
+
raise RuntimeError(
|
|
211
|
+
"Token refresh failed. Please re-authenticate using: "
|
|
212
|
+
"claude-mpm auth login google"
|
|
213
|
+
)
|
|
214
|
+
return token.access_token
|
|
215
|
+
|
|
216
|
+
# Token is valid
|
|
217
|
+
stored = self.storage.retrieve(SERVICE_NAME)
|
|
218
|
+
if stored is None:
|
|
219
|
+
raise RuntimeError("Unexpected error: token retrieval failed")
|
|
220
|
+
|
|
221
|
+
return stored.token.access_token
|
|
222
|
+
|
|
223
|
+
async def _make_request(
|
|
224
|
+
self,
|
|
225
|
+
method: str,
|
|
226
|
+
url: str,
|
|
227
|
+
params: Optional[dict[str, Any]] = None,
|
|
228
|
+
json_data: Optional[dict[str, Any]] = None,
|
|
229
|
+
) -> dict[str, Any]:
|
|
230
|
+
"""Make an authenticated HTTP request to Google APIs.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
method: HTTP method (GET, POST, etc.).
|
|
234
|
+
url: Full URL to request.
|
|
235
|
+
params: Optional query parameters.
|
|
236
|
+
json_data: Optional JSON body data.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
JSON response as a dictionary.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
httpx.HTTPStatusError: If the request fails.
|
|
243
|
+
"""
|
|
244
|
+
access_token = await self._get_access_token()
|
|
245
|
+
|
|
246
|
+
async with httpx.AsyncClient() as client:
|
|
247
|
+
response = await client.request(
|
|
248
|
+
method=method,
|
|
249
|
+
url=url,
|
|
250
|
+
params=params,
|
|
251
|
+
json=json_data,
|
|
252
|
+
headers={
|
|
253
|
+
"Authorization": f"Bearer {access_token}",
|
|
254
|
+
"Accept": "application/json",
|
|
255
|
+
},
|
|
256
|
+
timeout=30.0,
|
|
257
|
+
)
|
|
258
|
+
response.raise_for_status()
|
|
259
|
+
result: dict[str, Any] = response.json()
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
async def _dispatch_tool(
|
|
263
|
+
self, name: str, arguments: dict[str, Any]
|
|
264
|
+
) -> dict[str, Any]:
|
|
265
|
+
"""Dispatch tool call to appropriate handler.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
name: Tool name.
|
|
269
|
+
arguments: Tool arguments.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Tool result as dictionary.
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
ValueError: If tool name is not recognized.
|
|
276
|
+
"""
|
|
277
|
+
handlers = {
|
|
278
|
+
"list_calendars": self._list_calendars,
|
|
279
|
+
"get_events": self._get_events,
|
|
280
|
+
"search_gmail_messages": self._search_gmail_messages,
|
|
281
|
+
"get_gmail_message_content": self._get_gmail_message_content,
|
|
282
|
+
"search_drive_files": self._search_drive_files,
|
|
283
|
+
"get_drive_file_content": self._get_drive_file_content,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
handler = handlers.get(name)
|
|
287
|
+
if handler is None:
|
|
288
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
289
|
+
|
|
290
|
+
return await handler(arguments)
|
|
291
|
+
|
|
292
|
+
async def _list_calendars(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
293
|
+
"""List all calendars accessible by the user.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
arguments: Tool arguments (not used).
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
List of calendars with id, summary, and access role.
|
|
300
|
+
"""
|
|
301
|
+
url = f"{CALENDAR_API_BASE}/users/me/calendarList"
|
|
302
|
+
response = await self._make_request("GET", url)
|
|
303
|
+
|
|
304
|
+
calendars = []
|
|
305
|
+
for item in response.get("items", []):
|
|
306
|
+
calendars.append(
|
|
307
|
+
{
|
|
308
|
+
"id": item.get("id"),
|
|
309
|
+
"summary": item.get("summary"),
|
|
310
|
+
"description": item.get("description"),
|
|
311
|
+
"access_role": item.get("accessRole"),
|
|
312
|
+
"primary": item.get("primary", False),
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return {"calendars": calendars, "count": len(calendars)}
|
|
317
|
+
|
|
318
|
+
async def _get_events(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
319
|
+
"""Get events from a calendar.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
arguments: Tool arguments with calendar_id, time_min, time_max, max_results.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
List of events with summary, start, end times.
|
|
326
|
+
"""
|
|
327
|
+
calendar_id = arguments.get("calendar_id", "primary")
|
|
328
|
+
time_min = arguments.get("time_min")
|
|
329
|
+
time_max = arguments.get("time_max")
|
|
330
|
+
max_results = arguments.get("max_results", 10)
|
|
331
|
+
|
|
332
|
+
url = f"{CALENDAR_API_BASE}/calendars/{calendar_id}/events"
|
|
333
|
+
params: dict[str, Any] = {
|
|
334
|
+
"maxResults": max_results,
|
|
335
|
+
"singleEvents": True,
|
|
336
|
+
"orderBy": "startTime",
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if time_min:
|
|
340
|
+
params["timeMin"] = time_min
|
|
341
|
+
if time_max:
|
|
342
|
+
params["timeMax"] = time_max
|
|
343
|
+
|
|
344
|
+
response = await self._make_request("GET", url, params=params)
|
|
345
|
+
|
|
346
|
+
events = []
|
|
347
|
+
for item in response.get("items", []):
|
|
348
|
+
start = item.get("start", {})
|
|
349
|
+
end = item.get("end", {})
|
|
350
|
+
events.append(
|
|
351
|
+
{
|
|
352
|
+
"id": item.get("id"),
|
|
353
|
+
"summary": item.get("summary"),
|
|
354
|
+
"description": item.get("description"),
|
|
355
|
+
"start": start.get("dateTime") or start.get("date"),
|
|
356
|
+
"end": end.get("dateTime") or end.get("date"),
|
|
357
|
+
"location": item.get("location"),
|
|
358
|
+
"attendees": [a.get("email") for a in item.get("attendees", [])],
|
|
359
|
+
}
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
return {"events": events, "count": len(events)}
|
|
363
|
+
|
|
364
|
+
async def _search_gmail_messages(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
365
|
+
"""Search Gmail messages.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
arguments: Tool arguments with query and max_results.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
List of message snippets with id, thread_id, subject, from, date.
|
|
372
|
+
"""
|
|
373
|
+
query = arguments.get("query", "")
|
|
374
|
+
max_results = arguments.get("max_results", 10)
|
|
375
|
+
|
|
376
|
+
url = f"{GMAIL_API_BASE}/users/me/messages"
|
|
377
|
+
params = {"q": query, "maxResults": max_results}
|
|
378
|
+
|
|
379
|
+
response = await self._make_request("GET", url, params=params)
|
|
380
|
+
|
|
381
|
+
messages = []
|
|
382
|
+
for msg in response.get("messages", []):
|
|
383
|
+
# Get message metadata
|
|
384
|
+
msg_url = f"{GMAIL_API_BASE}/users/me/messages/{msg['id']}"
|
|
385
|
+
msg_detail = await self._make_request(
|
|
386
|
+
"GET", msg_url, params={"format": "metadata"}
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
headers = {
|
|
390
|
+
h["name"]: h["value"]
|
|
391
|
+
for h in msg_detail.get("payload", {}).get("headers", [])
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
messages.append(
|
|
395
|
+
{
|
|
396
|
+
"id": msg["id"],
|
|
397
|
+
"thread_id": msg.get("threadId"),
|
|
398
|
+
"subject": headers.get("Subject"),
|
|
399
|
+
"from": headers.get("From"),
|
|
400
|
+
"to": headers.get("To"),
|
|
401
|
+
"date": headers.get("Date"),
|
|
402
|
+
"snippet": msg_detail.get("snippet"),
|
|
403
|
+
}
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
return {"messages": messages, "count": len(messages)}
|
|
407
|
+
|
|
408
|
+
async def _get_gmail_message_content(
|
|
409
|
+
self, arguments: dict[str, Any]
|
|
410
|
+
) -> dict[str, Any]:
|
|
411
|
+
"""Get full content of a Gmail message.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
arguments: Tool arguments with message_id.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Message content including headers and body.
|
|
418
|
+
"""
|
|
419
|
+
message_id = arguments["message_id"]
|
|
420
|
+
|
|
421
|
+
url = f"{GMAIL_API_BASE}/users/me/messages/{message_id}"
|
|
422
|
+
response = await self._make_request("GET", url, params={"format": "full"})
|
|
423
|
+
|
|
424
|
+
headers = {
|
|
425
|
+
h["name"]: h["value"]
|
|
426
|
+
for h in response.get("payload", {}).get("headers", [])
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
# Extract body content
|
|
430
|
+
body = self._extract_message_body(response.get("payload", {}))
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
"id": response.get("id"),
|
|
434
|
+
"thread_id": response.get("threadId"),
|
|
435
|
+
"subject": headers.get("Subject"),
|
|
436
|
+
"from": headers.get("From"),
|
|
437
|
+
"to": headers.get("To"),
|
|
438
|
+
"cc": headers.get("Cc"),
|
|
439
|
+
"date": headers.get("Date"),
|
|
440
|
+
"body": body,
|
|
441
|
+
"labels": response.get("labelIds", []),
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
def _extract_message_body(self, payload: dict[str, Any]) -> str:
|
|
445
|
+
"""Extract message body from Gmail payload.
|
|
446
|
+
|
|
447
|
+
Handles both simple and multipart messages.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
payload: Gmail message payload.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Decoded message body text.
|
|
454
|
+
"""
|
|
455
|
+
import base64
|
|
456
|
+
|
|
457
|
+
# Simple message with body data
|
|
458
|
+
if "body" in payload and payload["body"].get("data"):
|
|
459
|
+
data = payload["body"]["data"]
|
|
460
|
+
return base64.urlsafe_b64decode(data).decode("utf-8", errors="replace")
|
|
461
|
+
|
|
462
|
+
# Multipart message
|
|
463
|
+
parts = payload.get("parts", [])
|
|
464
|
+
for part in parts:
|
|
465
|
+
mime_type = part.get("mimeType", "")
|
|
466
|
+
if mime_type == "text/plain":
|
|
467
|
+
data = part.get("body", {}).get("data", "")
|
|
468
|
+
if data:
|
|
469
|
+
return base64.urlsafe_b64decode(data).decode(
|
|
470
|
+
"utf-8", errors="replace"
|
|
471
|
+
)
|
|
472
|
+
elif mime_type.startswith("multipart/"):
|
|
473
|
+
# Recursively extract from nested parts
|
|
474
|
+
result = self._extract_message_body(part)
|
|
475
|
+
if result:
|
|
476
|
+
return result
|
|
477
|
+
|
|
478
|
+
# Fallback to HTML if no plain text
|
|
479
|
+
for part in parts:
|
|
480
|
+
if part.get("mimeType") == "text/html":
|
|
481
|
+
data = part.get("body", {}).get("data", "")
|
|
482
|
+
if data:
|
|
483
|
+
return base64.urlsafe_b64decode(data).decode(
|
|
484
|
+
"utf-8", errors="replace"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
return ""
|
|
488
|
+
|
|
489
|
+
async def _search_drive_files(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
490
|
+
"""Search Google Drive files.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
arguments: Tool arguments with query and max_results.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
List of files with id, name, mimeType, modifiedTime.
|
|
497
|
+
"""
|
|
498
|
+
query = arguments.get("query", "")
|
|
499
|
+
max_results = arguments.get("max_results", 10)
|
|
500
|
+
|
|
501
|
+
url = f"{DRIVE_API_BASE}/files"
|
|
502
|
+
params = {
|
|
503
|
+
"q": query,
|
|
504
|
+
"pageSize": max_results,
|
|
505
|
+
"fields": "files(id,name,mimeType,modifiedTime,size,webViewLink,owners)",
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
response = await self._make_request("GET", url, params=params)
|
|
509
|
+
|
|
510
|
+
files = []
|
|
511
|
+
for item in response.get("files", []):
|
|
512
|
+
files.append(
|
|
513
|
+
{
|
|
514
|
+
"id": item.get("id"),
|
|
515
|
+
"name": item.get("name"),
|
|
516
|
+
"mimeType": item.get("mimeType"),
|
|
517
|
+
"modifiedTime": item.get("modifiedTime"),
|
|
518
|
+
"size": item.get("size"),
|
|
519
|
+
"webViewLink": item.get("webViewLink"),
|
|
520
|
+
"owners": [o.get("emailAddress") for o in item.get("owners", [])],
|
|
521
|
+
}
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
return {"files": files, "count": len(files)}
|
|
525
|
+
|
|
526
|
+
async def _get_drive_file_content(
|
|
527
|
+
self, arguments: dict[str, Any]
|
|
528
|
+
) -> dict[str, Any]:
|
|
529
|
+
"""Get content of a Google Drive file.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
arguments: Tool arguments with file_id.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
File metadata and content (for exportable types).
|
|
536
|
+
"""
|
|
537
|
+
file_id = arguments["file_id"]
|
|
538
|
+
|
|
539
|
+
# First get file metadata
|
|
540
|
+
meta_url = f"{DRIVE_API_BASE}/files/{file_id}"
|
|
541
|
+
metadata = await self._make_request(
|
|
542
|
+
"GET", meta_url, params={"fields": "id,name,mimeType,size"}
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
mime_type = metadata.get("mimeType", "")
|
|
546
|
+
|
|
547
|
+
# Google Docs types need export
|
|
548
|
+
export_map = {
|
|
549
|
+
"application/vnd.google-apps.document": "text/plain",
|
|
550
|
+
"application/vnd.google-apps.spreadsheet": "text/csv",
|
|
551
|
+
"application/vnd.google-apps.presentation": "text/plain",
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
access_token = await self._get_access_token()
|
|
555
|
+
|
|
556
|
+
if mime_type in export_map:
|
|
557
|
+
# Export Google Workspace files
|
|
558
|
+
export_url = f"{DRIVE_API_BASE}/files/{file_id}/export"
|
|
559
|
+
async with httpx.AsyncClient() as client:
|
|
560
|
+
response = await client.get(
|
|
561
|
+
export_url,
|
|
562
|
+
params={"mimeType": export_map[mime_type]},
|
|
563
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
|
564
|
+
timeout=30.0,
|
|
565
|
+
)
|
|
566
|
+
response.raise_for_status()
|
|
567
|
+
content = response.text
|
|
568
|
+
else:
|
|
569
|
+
# Download regular files
|
|
570
|
+
download_url = f"{DRIVE_API_BASE}/files/{file_id}"
|
|
571
|
+
async with httpx.AsyncClient() as client:
|
|
572
|
+
response = await client.get(
|
|
573
|
+
download_url,
|
|
574
|
+
params={"alt": "media"},
|
|
575
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
|
576
|
+
timeout=30.0,
|
|
577
|
+
)
|
|
578
|
+
response.raise_for_status()
|
|
579
|
+
|
|
580
|
+
# Try to decode as text, otherwise indicate binary
|
|
581
|
+
try:
|
|
582
|
+
content = response.text
|
|
583
|
+
except UnicodeDecodeError:
|
|
584
|
+
content = f"[Binary file: {metadata.get('size', 'unknown')} bytes]"
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
"id": metadata.get("id"),
|
|
588
|
+
"name": metadata.get("name"),
|
|
589
|
+
"mimeType": mime_type,
|
|
590
|
+
"content": content,
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async def run(self) -> None:
|
|
594
|
+
"""Run the MCP server using stdio transport."""
|
|
595
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
596
|
+
await self.server.run(
|
|
597
|
+
read_stream,
|
|
598
|
+
write_stream,
|
|
599
|
+
self.server.create_initialization_options(),
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def main() -> None:
|
|
604
|
+
"""Entry point for the Google Workspace MCP server."""
|
|
605
|
+
server = GoogleWorkspaceServer()
|
|
606
|
+
asyncio.run(server.run())
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
if __name__ == "__main__":
|
|
610
|
+
main()
|
|
@@ -222,13 +222,13 @@ log_debug() {
|
|
|
222
222
|
if [[ "$PYTHON_CMD" == "uv run"* ]]; then
|
|
223
223
|
if ! uv run --directory "$CLAUDE_MPM_ROOT" python -c "import claude_mpm" 2>/dev/null; then
|
|
224
224
|
log_debug "claude_mpm module not available, continuing without hook"
|
|
225
|
-
echo '{"
|
|
225
|
+
echo '{"continue": true}'
|
|
226
226
|
exit 0
|
|
227
227
|
fi
|
|
228
228
|
else
|
|
229
229
|
if ! $PYTHON_CMD -c "import claude_mpm" 2>/dev/null; then
|
|
230
230
|
log_debug "claude_mpm module not available, continuing without hook"
|
|
231
|
-
echo '{"
|
|
231
|
+
echo '{"continue": true}'
|
|
232
232
|
exit 0
|
|
233
233
|
fi
|
|
234
234
|
fi
|
|
@@ -250,5 +250,5 @@ if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
|
250
250
|
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Error: $(cat /tmp/claude-mpm-hook-error.log 2>/dev/null | head -5)" >> /tmp/claude-mpm-hook.log
|
|
251
251
|
fi
|
|
252
252
|
# Return continue action to prevent blocking Claude Code
|
|
253
|
-
echo '{"
|
|
253
|
+
echo '{"continue": true}'
|
|
254
254
|
exit 0
|