claude-mpm 5.6.1__py3-none-any.whl → 5.6.76__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.
Files changed (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
  3. claude_mpm/auth/__init__.py +35 -0
  4. claude_mpm/auth/callback_server.py +328 -0
  5. claude_mpm/auth/models.py +104 -0
  6. claude_mpm/auth/oauth_manager.py +266 -0
  7. claude_mpm/auth/providers/__init__.py +12 -0
  8. claude_mpm/auth/providers/base.py +165 -0
  9. claude_mpm/auth/providers/google.py +261 -0
  10. claude_mpm/auth/token_storage.py +252 -0
  11. claude_mpm/cli/commands/commander.py +174 -4
  12. claude_mpm/cli/commands/mcp.py +29 -17
  13. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  14. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  15. claude_mpm/cli/commands/oauth.py +481 -0
  16. claude_mpm/cli/commands/skill_source.py +51 -2
  17. claude_mpm/cli/commands/skills.py +5 -3
  18. claude_mpm/cli/executor.py +9 -0
  19. claude_mpm/cli/helpers.py +1 -1
  20. claude_mpm/cli/parsers/base_parser.py +13 -0
  21. claude_mpm/cli/parsers/commander_parser.py +43 -10
  22. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  23. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  24. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  25. claude_mpm/cli/parsers/skills_parser.py +5 -0
  26. claude_mpm/cli/startup.py +300 -33
  27. claude_mpm/cli/startup_display.py +4 -2
  28. claude_mpm/cli/startup_migrations.py +236 -0
  29. claude_mpm/commander/__init__.py +6 -0
  30. claude_mpm/commander/adapters/__init__.py +32 -3
  31. claude_mpm/commander/adapters/auggie.py +260 -0
  32. claude_mpm/commander/adapters/base.py +98 -1
  33. claude_mpm/commander/adapters/claude_code.py +32 -1
  34. claude_mpm/commander/adapters/codex.py +237 -0
  35. claude_mpm/commander/adapters/example_usage.py +310 -0
  36. claude_mpm/commander/adapters/mpm.py +389 -0
  37. claude_mpm/commander/adapters/registry.py +204 -0
  38. claude_mpm/commander/api/app.py +32 -16
  39. claude_mpm/commander/api/errors.py +21 -0
  40. claude_mpm/commander/api/routes/messages.py +11 -11
  41. claude_mpm/commander/api/routes/projects.py +20 -20
  42. claude_mpm/commander/api/routes/sessions.py +37 -26
  43. claude_mpm/commander/api/routes/work.py +86 -50
  44. claude_mpm/commander/api/schemas.py +4 -0
  45. claude_mpm/commander/chat/cli.py +47 -5
  46. claude_mpm/commander/chat/commands.py +44 -16
  47. claude_mpm/commander/chat/repl.py +1729 -82
  48. claude_mpm/commander/config.py +5 -3
  49. claude_mpm/commander/core/__init__.py +10 -0
  50. claude_mpm/commander/core/block_manager.py +325 -0
  51. claude_mpm/commander/core/response_manager.py +323 -0
  52. claude_mpm/commander/daemon.py +215 -10
  53. claude_mpm/commander/env_loader.py +59 -0
  54. claude_mpm/commander/events/manager.py +61 -1
  55. claude_mpm/commander/frameworks/base.py +91 -1
  56. claude_mpm/commander/frameworks/mpm.py +9 -14
  57. claude_mpm/commander/git/__init__.py +5 -0
  58. claude_mpm/commander/git/worktree_manager.py +212 -0
  59. claude_mpm/commander/instance_manager.py +546 -15
  60. claude_mpm/commander/memory/__init__.py +45 -0
  61. claude_mpm/commander/memory/compression.py +347 -0
  62. claude_mpm/commander/memory/embeddings.py +230 -0
  63. claude_mpm/commander/memory/entities.py +310 -0
  64. claude_mpm/commander/memory/example_usage.py +290 -0
  65. claude_mpm/commander/memory/integration.py +325 -0
  66. claude_mpm/commander/memory/search.py +381 -0
  67. claude_mpm/commander/memory/store.py +657 -0
  68. claude_mpm/commander/models/events.py +6 -0
  69. claude_mpm/commander/persistence/state_store.py +95 -1
  70. claude_mpm/commander/registry.py +10 -4
  71. claude_mpm/commander/runtime/monitor.py +32 -2
  72. claude_mpm/commander/tmux_orchestrator.py +3 -2
  73. claude_mpm/commander/work/executor.py +38 -20
  74. claude_mpm/commander/workflow/event_handler.py +25 -3
  75. claude_mpm/config/skill_sources.py +16 -0
  76. claude_mpm/constants.py +5 -0
  77. claude_mpm/core/claude_runner.py +152 -0
  78. claude_mpm/core/config.py +30 -22
  79. claude_mpm/core/config_constants.py +74 -9
  80. claude_mpm/core/constants.py +56 -12
  81. claude_mpm/core/hook_manager.py +2 -1
  82. claude_mpm/core/interactive_session.py +5 -4
  83. claude_mpm/core/logger.py +16 -2
  84. claude_mpm/core/logging_utils.py +40 -16
  85. claude_mpm/core/network_config.py +148 -0
  86. claude_mpm/core/oneshot_session.py +7 -6
  87. claude_mpm/core/output_style_manager.py +37 -7
  88. claude_mpm/core/socketio_pool.py +47 -15
  89. claude_mpm/core/unified_paths.py +68 -80
  90. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
  91. claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
  92. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  93. claude_mpm/hooks/claude_hooks/installer.py +222 -54
  94. claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
  95. claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
  96. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  97. claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
  98. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
  99. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  100. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  101. claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
  102. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
  103. claude_mpm/hooks/session_resume_hook.py +22 -18
  104. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  105. claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
  106. claude_mpm/init.py +21 -14
  107. claude_mpm/mcp/__init__.py +9 -0
  108. claude_mpm/mcp/google_workspace_server.py +610 -0
  109. claude_mpm/scripts/claude-hook-handler.sh +10 -9
  110. claude_mpm/services/agents/agent_selection_service.py +2 -2
  111. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  112. claude_mpm/services/command_deployment_service.py +44 -26
  113. claude_mpm/services/hook_installer_service.py +77 -8
  114. claude_mpm/services/mcp_config_manager.py +99 -19
  115. claude_mpm/services/mcp_service_registry.py +294 -0
  116. claude_mpm/services/monitor/server.py +6 -1
  117. claude_mpm/services/pm_skills_deployer.py +5 -3
  118. claude_mpm/services/skills/git_skill_source_manager.py +79 -8
  119. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  120. claude_mpm/services/skills/skill_discovery_service.py +17 -1
  121. claude_mpm/services/skills_deployer.py +31 -5
  122. claude_mpm/skills/__init__.py +2 -1
  123. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  124. claude_mpm/skills/registry.py +295 -90
  125. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
  126. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
  127. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
  128. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
  129. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
  130. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  131. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.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()
@@ -125,7 +125,7 @@ find_python_command() {
125
125
  # 1. Check for UV project first (uv.lock or pyproject.toml with uv)
126
126
  if [ -f "$CLAUDE_MPM_ROOT/uv.lock" ]; then
127
127
  if command -v uv &> /dev/null; then
128
- echo "uv run python"
128
+ echo "uv run --directory \"$CLAUDE_MPM_ROOT\" python"
129
129
  return
130
130
  fi
131
131
  fi
@@ -219,16 +219,16 @@ log_debug() {
219
219
 
220
220
  # Test Python works and module exists
221
221
  # Handle UV's multi-word command specially
222
- if [[ "$PYTHON_CMD" == "uv run python" ]]; then
223
- if ! uv run python -c "import claude_mpm" 2>/dev/null; then
222
+ if [[ "$PYTHON_CMD" == "uv run"* ]]; then
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 '{"action": "continue"}'
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 '{"action": "continue"}'
231
+ echo '{"continue": true}'
232
232
  exit 0
233
233
  fi
234
234
  fi
@@ -236,10 +236,11 @@ fi
236
236
  # Run the Python hook handler with all input
237
237
  # Use exec to replace the shell process with Python
238
238
  # Handle UV's multi-word command specially
239
- if [[ "$PYTHON_CMD" == "uv run python" ]]; then
240
- exec uv run python -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/claude-mpm-hook-error.log
239
+ # Suppress RuntimeWarning to prevent stderr output (which causes hook errors)
240
+ if [[ "$PYTHON_CMD" == "uv run"* ]]; then
241
+ exec uv run --directory "$CLAUDE_MPM_ROOT" python -W ignore::RuntimeWarning -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/claude-mpm-hook-error.log
241
242
  else
242
- exec "$PYTHON_CMD" -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/claude-mpm-hook-error.log
243
+ exec "$PYTHON_CMD" -W ignore::RuntimeWarning -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/claude-mpm-hook-error.log
243
244
  fi
244
245
 
245
246
  # Note: exec replaces the shell process, so code below only runs if exec fails
@@ -249,5 +250,5 @@ if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
249
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
250
251
  fi
251
252
  # Return continue action to prevent blocking Claude Code
252
- echo '{"action": "continue"}'
253
+ echo '{"continue": true}'
253
254
  exit 0
@@ -39,10 +39,10 @@ import logging
39
39
  from pathlib import Path
40
40
  from typing import Any, Dict, List, Optional, Set, Tuple
41
41
 
42
- from src.claude_mpm.services.agents.single_tier_deployment_service import (
42
+ from claude_mpm.services.agents.single_tier_deployment_service import (
43
43
  SingleTierDeploymentService,
44
44
  )
45
- from src.claude_mpm.services.agents.toolchain_detector import ToolchainDetector
45
+ from claude_mpm.services.agents.toolchain_detector import ToolchainDetector
46
46
 
47
47
  logger = logging.getLogger(__name__)
48
48
 
@@ -30,12 +30,12 @@ from datetime import datetime, timezone
30
30
  from pathlib import Path
31
31
  from typing import Any, Dict, List, Optional
32
32
 
33
- from src.claude_mpm.config.agent_sources import AgentSourceConfiguration
34
- from src.claude_mpm.models.git_repository import GitRepository
35
- from src.claude_mpm.services.agents.deployment.remote_agent_discovery_service import (
33
+ from claude_mpm.config.agent_sources import AgentSourceConfiguration
34
+ from claude_mpm.models.git_repository import GitRepository
35
+ from claude_mpm.services.agents.deployment.remote_agent_discovery_service import (
36
36
  RemoteAgentDiscoveryService,
37
37
  )
38
- from src.claude_mpm.services.agents.git_source_manager import GitSourceManager
38
+ from claude_mpm.services.agents.git_source_manager import GitSourceManager
39
39
 
40
40
  logger = logging.getLogger(__name__)
41
41