workspace-mcp 1.1.3__py3-none-any.whl → 1.1.5__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 CHANGED
@@ -1,14 +1,16 @@
1
1
  # auth/google_auth.py
2
2
 
3
- import os
3
+ import asyncio
4
4
  import json
5
+ import jwt
5
6
  import logging
6
- import asyncio
7
- from typing import List, Optional, Tuple, Dict, Any, Callable
8
7
  import os
9
8
 
9
+ from datetime import datetime
10
+ from typing import List, Optional, Tuple, Dict, Any
11
+
10
12
  from google.oauth2.credentials import Credentials
11
- from google_auth_oauthlib.flow import Flow, InstalledAppFlow
13
+ from google_auth_oauthlib.flow import Flow
12
14
  from google.auth.transport.requests import Request
13
15
  from google.auth.exceptions import RefreshError
14
16
  from googleapiclient.discovery import build
@@ -161,8 +163,6 @@ def load_credentials_from_file(
161
163
  expiry = None
162
164
  if creds_data.get("expiry"):
163
165
  try:
164
- from datetime import datetime
165
-
166
166
  expiry = datetime.fromisoformat(creds_data["expiry"])
167
167
  except (ValueError, TypeError) as e:
168
168
  logger.warning(
@@ -789,8 +789,6 @@ async def get_authenticated_google_service(
789
789
  # Try to get email from credentials if needed for validation
790
790
  if credentials and credentials.id_token:
791
791
  try:
792
- import jwt
793
-
794
792
  # Decode without verification (just to get email for logging)
795
793
  decoded_token = jwt.decode(
796
794
  credentials.id_token, options={"verify_signature": False}
@@ -5,15 +5,17 @@ In streamable-http mode: Uses the existing FastAPI server
5
5
  In stdio mode: Starts a minimal HTTP server just for OAuth callbacks
6
6
  """
7
7
 
8
+ import os
8
9
  import asyncio
9
10
  import logging
10
11
  import threading
11
12
  import time
12
- from typing import Optional, Dict, Any
13
13
  import socket
14
+ import uvicorn
14
15
 
15
16
  from fastapi import FastAPI, Request
16
- import uvicorn
17
+ from typing import Optional
18
+ from urllib.parse import urlparse
17
19
 
18
20
  from auth.google_auth import handle_auth_callback, check_client_secrets
19
21
  from auth.scopes import OAUTH_STATE_TO_SESSION_ID_MAP, SCOPES
@@ -73,10 +75,11 @@ class MinimalOAuthServer:
73
75
  logger.warning(f"OAuth callback: No MCP session ID found for state '{state}'. Auth will not be tied to a specific session.")
74
76
 
75
77
  # Exchange code for credentials
78
+ redirect_uri = get_oauth_redirect_uri(port=self.port, base_uri=self.base_uri)
76
79
  verified_user_id, credentials = handle_auth_callback(
77
80
  scopes=SCOPES,
78
81
  authorization_response=str(request.url),
79
- redirect_uri=f"{self.base_uri}:{self.port}/oauth2callback",
82
+ redirect_uri=redirect_uri,
80
83
  session_id=mcp_session_id
81
84
  )
82
85
 
@@ -105,7 +108,6 @@ class MinimalOAuthServer:
105
108
  # Check if port is available
106
109
  # Extract hostname from base_uri (e.g., "http://localhost" -> "localhost")
107
110
  try:
108
- from urllib.parse import urlparse
109
111
  parsed_uri = urlparse(self.base_uri)
110
112
  hostname = parsed_uri.hostname or 'localhost'
111
113
  except Exception:
@@ -179,19 +181,31 @@ class MinimalOAuthServer:
179
181
  # Global instance for stdio mode
180
182
  _minimal_oauth_server: Optional[MinimalOAuthServer] = None
181
183
 
182
- def get_oauth_redirect_uri(transport_mode: str = "stdio", port: int = 8000, base_uri: str = "http://localhost") -> str:
184
+ def get_oauth_redirect_uri(port: int = 8000, base_uri: str = "http://localhost") -> str:
183
185
  """
184
- Get the appropriate OAuth redirect URI based on transport mode.
186
+ Get the appropriate OAuth redirect URI.
187
+
188
+ Priority:
189
+ 1. GOOGLE_OAUTH_REDIRECT_URI environment variable
190
+ 2. Constructed from port and base URI
185
191
 
186
192
  Args:
187
- transport_mode: "stdio" or "streamable-http"
188
193
  port: Port number (default 8000)
189
194
  base_uri: Base URI (default "http://localhost")
190
195
 
191
196
  Returns:
192
197
  OAuth redirect URI
193
198
  """
194
- return f"{base_uri}:{port}/oauth2callback"
199
+ # Highest priority: Use the environment variable if it's set
200
+ env_redirect_uri = os.getenv("GOOGLE_OAUTH_REDIRECT_URI")
201
+ if env_redirect_uri:
202
+ logger.info(f"Using redirect URI from GOOGLE_OAUTH_REDIRECT_URI: {env_redirect_uri}")
203
+ return env_redirect_uri
204
+
205
+ # Fallback to constructing the URI based on server settings
206
+ constructed_uri = f"{base_uri}:{port}/oauth2callback"
207
+ logger.info(f"Constructed redirect URI: {constructed_uri}")
208
+ return constructed_uri
195
209
 
196
210
  def ensure_oauth_callback_available(transport_mode: str = "stdio", port: int = 8000, base_uri: str = "http://localhost") -> bool:
197
211
  """
auth/scopes.py CHANGED
@@ -52,6 +52,10 @@ FORMS_RESPONSES_READONLY_SCOPE = 'https://www.googleapis.com/auth/forms.response
52
52
  SLIDES_SCOPE = 'https://www.googleapis.com/auth/presentations'
53
53
  SLIDES_READONLY_SCOPE = 'https://www.googleapis.com/auth/presentations.readonly'
54
54
 
55
+ # Google Tasks API scopes
56
+ TASKS_SCOPE = 'https://www.googleapis.com/auth/tasks'
57
+ TASKS_READONLY_SCOPE = 'https://www.googleapis.com/auth/tasks.readonly'
58
+
55
59
  # Base OAuth scopes required for user identification
56
60
  BASE_SCOPES = [
57
61
  USERINFO_EMAIL_SCOPE,
@@ -104,5 +108,10 @@ SLIDES_SCOPES = [
104
108
  SLIDES_READONLY_SCOPE
105
109
  ]
106
110
 
111
+ TASKS_SCOPES = [
112
+ TASKS_SCOPE,
113
+ TASKS_READONLY_SCOPE
114
+ ]
115
+
107
116
  # Combined scopes for all supported Google Workspace operations
108
- SCOPES = list(set(BASE_SCOPES + CALENDAR_SCOPES + DRIVE_SCOPES + GMAIL_SCOPES + DOCS_SCOPES + CHAT_SCOPES + SHEETS_SCOPES + FORMS_SCOPES + SLIDES_SCOPES))
117
+ SCOPES = list(set(BASE_SCOPES + CALENDAR_SCOPES + DRIVE_SCOPES + GMAIL_SCOPES + DOCS_SCOPES + CHAT_SCOPES + SHEETS_SCOPES + FORMS_SCOPES + SLIDES_SCOPES + TASKS_SCOPES))
auth/service_decorator.py CHANGED
@@ -18,7 +18,8 @@ from auth.scopes import (
18
18
  SHEETS_READONLY_SCOPE, SHEETS_WRITE_SCOPE,
19
19
  CHAT_READONLY_SCOPE, CHAT_WRITE_SCOPE, CHAT_SPACES_SCOPE,
20
20
  FORMS_BODY_SCOPE, FORMS_BODY_READONLY_SCOPE, FORMS_RESPONSES_READONLY_SCOPE,
21
- SLIDES_SCOPE, SLIDES_READONLY_SCOPE
21
+ SLIDES_SCOPE, SLIDES_READONLY_SCOPE,
22
+ TASKS_SCOPE, TASKS_READONLY_SCOPE
22
23
  )
23
24
 
24
25
  # Service configuration mapping
@@ -30,7 +31,8 @@ SERVICE_CONFIGS = {
30
31
  "sheets": {"service": "sheets", "version": "v4"},
31
32
  "chat": {"service": "chat", "version": "v1"},
32
33
  "forms": {"service": "forms", "version": "v1"},
33
- "slides": {"service": "slides", "version": "v1"}
34
+ "slides": {"service": "slides", "version": "v1"},
35
+ "tasks": {"service": "tasks", "version": "v1"}
34
36
  }
35
37
 
36
38
 
@@ -72,6 +74,10 @@ SCOPE_GROUPS = {
72
74
  # Slides scopes
73
75
  "slides": SLIDES_SCOPE,
74
76
  "slides_read": SLIDES_READONLY_SCOPE,
77
+
78
+ # Tasks scopes
79
+ "tasks": TASKS_SCOPE,
80
+ "tasks_read": TASKS_READONLY_SCOPE,
75
81
  }
76
82
 
77
83
  # Service cache: {cache_key: (service, cached_time, user_email)}
core/server.py CHANGED
@@ -49,6 +49,9 @@ from auth.scopes import (
49
49
  SLIDES_SCOPE,
50
50
  SLIDES_READONLY_SCOPE,
51
51
  SLIDES_SCOPES,
52
+ TASKS_SCOPE,
53
+ TASKS_READONLY_SCOPE,
54
+ TASKS_SCOPES,
52
55
  SCOPES
53
56
  )
54
57
 
@@ -79,7 +82,7 @@ def set_transport_mode(mode: str):
79
82
 
80
83
  def get_oauth_redirect_uri_for_current_mode() -> str:
81
84
  """Get OAuth redirect URI based on current transport mode."""
82
- return get_oauth_redirect_uri(_current_transport_mode, WORKSPACE_MCP_PORT, WORKSPACE_MCP_BASE_URI)
85
+ return get_oauth_redirect_uri(WORKSPACE_MCP_PORT, WORKSPACE_MCP_BASE_URI)
83
86
 
84
87
  # Health check endpoint
85
88
  @server.custom_route("/health", methods=["GET"])
@@ -12,6 +12,7 @@ from typing import List, Optional, Dict, Any
12
12
 
13
13
  from mcp import types
14
14
  from googleapiclient.errors import HttpError
15
+ from googleapiclient.discovery import build
15
16
 
16
17
  from auth.service_decorator import require_google_service
17
18
  from core.utils import handle_http_errors
@@ -268,7 +269,6 @@ async def create_event(
268
269
  if attachments:
269
270
  # Accept both file URLs and file IDs. If a URL, extract the fileId.
270
271
  event_body["attachments"] = []
271
- from googleapiclient.discovery import build
272
272
  drive_service = None
273
273
  try:
274
274
  drive_service = service._http and build("drive", "v3", http=service._http)
gdocs/docs_tools.py CHANGED
@@ -9,7 +9,6 @@ import io
9
9
  from typing import List
10
10
 
11
11
  from mcp import types
12
- from googleapiclient.errors import HttpError
13
12
  from googleapiclient.http import MediaIoBaseDownload
14
13
 
15
14
  # Auth & server utilities
gforms/forms_tools.py CHANGED
@@ -6,10 +6,9 @@ This module provides MCP tools for interacting with Google Forms API.
6
6
 
7
7
  import logging
8
8
  import asyncio
9
- from typing import List, Optional, Dict, Any
9
+ from typing import Optional, Dict, Any
10
10
 
11
11
  from mcp import types
12
- from googleapiclient.errors import HttpError
13
12
 
14
13
  from auth.service_decorator import require_google_service
15
14
  from core.server import server
@@ -47,10 +46,10 @@ async def create_form(
47
46
  "title": title
48
47
  }
49
48
  }
50
-
49
+
51
50
  if description:
52
51
  form_body["info"]["description"] = description
53
-
52
+
54
53
  if document_title:
55
54
  form_body["info"]["document_title"] = document_title
56
55
 
@@ -61,7 +60,7 @@ async def create_form(
61
60
  form_id = created_form.get("formId")
62
61
  edit_url = f"https://docs.google.com/forms/d/{form_id}/edit"
63
62
  responder_url = created_form.get("responderUri", f"https://docs.google.com/forms/d/{form_id}/viewform")
64
-
63
+
65
64
  confirmation_message = f"Successfully created form '{created_form.get('info', {}).get('title', title)}' for {user_google_email}. Form ID: {form_id}. Edit URL: {edit_url}. Responder URL: {responder_url}"
66
65
  logger.info(f"Form created successfully for {user_google_email}. ID: {form_id}")
67
66
  return confirmation_message
@@ -95,10 +94,10 @@ async def get_form(
95
94
  title = form_info.get("title", "No Title")
96
95
  description = form_info.get("description", "No Description")
97
96
  document_title = form_info.get("documentTitle", title)
98
-
97
+
99
98
  edit_url = f"https://docs.google.com/forms/d/{form_id}/edit"
100
99
  responder_url = form.get("responderUri", f"https://docs.google.com/forms/d/{form_id}/viewform")
101
-
100
+
102
101
  items = form.get("items", [])
103
102
  questions_summary = []
104
103
  for i, item in enumerate(items, 1):
@@ -106,9 +105,9 @@ async def get_form(
106
105
  item_type = item.get("questionItem", {}).get("question", {}).get("required", False)
107
106
  required_text = " (Required)" if item_type else ""
108
107
  questions_summary.append(f" {i}. {item_title}{required_text}")
109
-
108
+
110
109
  questions_text = "\n".join(questions_summary) if questions_summary else " No questions found"
111
-
110
+
112
111
  result = f"""Form Details for {user_google_email}:
113
112
  - Title: "{title}"
114
113
  - Description: "{description}"
@@ -118,7 +117,7 @@ async def get_form(
118
117
  - Responder URL: {responder_url}
119
118
  - Questions ({len(items)} total):
120
119
  {questions_text}"""
121
-
120
+
122
121
  logger.info(f"Successfully retrieved form for {user_google_email}. ID: {form_id}")
123
122
  return result
124
123
 
@@ -190,7 +189,7 @@ async def get_form_response(
190
189
  response_id = response.get("responseId", "Unknown")
191
190
  create_time = response.get("createTime", "Unknown")
192
191
  last_submitted_time = response.get("lastSubmittedTime", "Unknown")
193
-
192
+
194
193
  answers = response.get("answers", {})
195
194
  answer_details = []
196
195
  for question_id, answer_data in answers.items():
@@ -200,9 +199,9 @@ async def get_form_response(
200
199
  answer_details.append(f" Question ID {question_id}: {answer_text}")
201
200
  else:
202
201
  answer_details.append(f" Question ID {question_id}: No answer provided")
203
-
202
+
204
203
  answers_text = "\n".join(answer_details) if answer_details else " No answers found"
205
-
204
+
206
205
  result = f"""Form Response Details for {user_google_email}:
207
206
  - Form ID: {form_id}
208
207
  - Response ID: {response_id}
@@ -210,7 +209,7 @@ async def get_form_response(
210
209
  - Last Submitted: {last_submitted_time}
211
210
  - Answers:
212
211
  {answers_text}"""
213
-
212
+
214
213
  logger.info(f"Successfully retrieved response for {user_google_email}. Response ID: {response_id}")
215
214
  return result
216
215
 
@@ -252,7 +251,7 @@ async def list_form_responses(
252
251
 
253
252
  responses = responses_result.get("responses", [])
254
253
  next_page_token = responses_result.get("nextPageToken")
255
-
254
+
256
255
  if not responses:
257
256
  return f"No responses found for form {form_id} for {user_google_email}."
258
257
 
@@ -261,19 +260,19 @@ async def list_form_responses(
261
260
  response_id = response.get("responseId", "Unknown")
262
261
  create_time = response.get("createTime", "Unknown")
263
262
  last_submitted_time = response.get("lastSubmittedTime", "Unknown")
264
-
263
+
265
264
  answers_count = len(response.get("answers", {}))
266
265
  response_details.append(
267
266
  f" {i}. Response ID: {response_id} | Created: {create_time} | Last Submitted: {last_submitted_time} | Answers: {answers_count}"
268
267
  )
269
268
 
270
269
  pagination_info = f"\nNext page token: {next_page_token}" if next_page_token else "\nNo more pages."
271
-
270
+
272
271
  result = f"""Form Responses for {user_google_email}:
273
272
  - Form ID: {form_id}
274
273
  - Total responses returned: {len(responses)}
275
274
  - Responses:
276
275
  {chr(10).join(response_details)}{pagination_info}"""
277
-
276
+
278
277
  logger.info(f"Successfully retrieved {len(responses)} responses for {user_google_email}. Form ID: {form_id}")
279
278
  return result
gmail/gmail_tools.py CHANGED
@@ -13,7 +13,6 @@ from email.mime.text import MIMEText
13
13
 
14
14
  from mcp import types
15
15
  from fastapi import Body
16
- from googleapiclient.errors import HttpError
17
16
 
18
17
  from auth.service_decorator import require_google_service
19
18
  from core.utils import handle_http_errors
gtasks/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """
2
+ Google Tasks MCP Integration
3
+
4
+ This module provides MCP tools for interacting with Google Tasks API.
5
+ """