workspace-mcp 1.1.4__tar.gz → 1.1.5__tar.gz
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.
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/PKG-INFO +51 -9
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/README.md +50 -8
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/auth/google_auth.py +6 -8
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/auth/oauth_callback_server.py +22 -8
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/core/server.py +1 -1
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gcalendar/calendar_tools.py +1 -1
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gdocs/docs_tools.py +0 -1
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gforms/forms_tools.py +17 -18
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gmail/gmail_tools.py +0 -1
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/pyproject.toml +1 -1
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/tests/test_oauth_callback_server.py +0 -2
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/workspace_mcp.egg-info/PKG-INFO +51 -9
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/LICENSE +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/auth/__init__.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/auth/oauth_responses.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/auth/scopes.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/auth/service_decorator.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/core/__init__.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/core/comments.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/core/context.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/core/utils.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gTasks/__init__.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gTasks/tasks_tools.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gcalendar/__init__.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gchat/__init__.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gchat/chat_tools.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gdocs/__init__.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gdrive/__init__.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gdrive/drive_tools.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gforms/__init__.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gmail/__init__.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gsheets/__init__.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gsheets/sheets_tools.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gslides/__init__.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/gslides/slides_tools.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/main.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/setup.cfg +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/tests/test_auth.py +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/workspace_mcp.egg-info/SOURCES.txt +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/workspace_mcp.egg-info/dependency_links.txt +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/workspace_mcp.egg-info/entry_points.txt +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/workspace_mcp.egg-info/requires.txt +0 -0
- {workspace_mcp-1.1.4 → workspace_mcp-1.1.5}/workspace_mcp.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: workspace-mcp
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.5
|
4
4
|
Summary: Comprehensive, highly performant Google Workspace Streamable HTTP & SSE MCP Server for Calendar, Gmail, Docs, Sheets, Slides & Drive
|
5
5
|
Author-email: Taylor Wilsdon <taylor@taylorwilsdon.com>
|
6
6
|
License: MIT
|
@@ -79,13 +79,15 @@ Dynamic: license-file
|
|
79
79
|
---
|
80
80
|
|
81
81
|
### A quick plug for AI-Enhanced Docs
|
82
|
+
<details>
|
83
|
+
<summary>But why?</summary>
|
82
84
|
|
83
|
-
|
85
|
+
**This README was written with AI assistance, and here's why that matters**
|
84
86
|
>
|
85
|
-
> As a solo
|
87
|
+
> As a solo dev building open source tools that many never see outside use, comprehensive documentation often wouldn't happen without AI help. Using agentic dev tools like **Roo** & **Claude Code** that understand the entire codebase, AI doesn't just regurgitate generic content - it extracts real implementation details and creates accurate, specific documentation.
|
86
88
|
>
|
87
|
-
> In this case, Sonnet 4 took a pass & a human (me) verified them
|
88
|
-
|
89
|
+
> In this case, Sonnet 4 took a pass & a human (me) verified them 7/10/25.
|
90
|
+
</details>
|
89
91
|
|
90
92
|
## Overview
|
91
93
|
|
@@ -103,7 +105,7 @@ A production-ready MCP server that integrates all major Google Workspace service
|
|
103
105
|
- **📝 Google Forms**: Form creation, retrieval, publish settings, and response management
|
104
106
|
- **✓ Google Tasks**: Complete task and task list management with hierarchy, due dates, and status tracking
|
105
107
|
- **💬 Google Chat**: Space management and messaging capabilities
|
106
|
-
- **🔄
|
108
|
+
- **🔄 All Transports**: Stdio, Streamable HTTP & SSE, OpenAPI compatibility via `mcpo`
|
107
109
|
- **⚡ High Performance**: Service caching, thread-safe sessions, FastMCP integration
|
108
110
|
- **🧩 Developer Friendly**: Minimal boilerplate, automatic service injection, centralized configuration
|
109
111
|
|
@@ -111,7 +113,45 @@ A production-ready MCP server that integrates all major Google Workspace service
|
|
111
113
|
|
112
114
|
## 🚀 Quick Start
|
113
115
|
|
114
|
-
###
|
116
|
+
### 1. One-Click Claude Desktop Install (Recommended)
|
117
|
+
|
118
|
+
1. **Download:** Grab the latest `google_workspace_mcp.dxt` from the “Releases” page
|
119
|
+
2. **Install:** Double-click the file – Claude Desktop opens and prompts you to **Install**
|
120
|
+
3. **Configure:** In Claude Desktop → **Settings → Extensions → Google Workspace MCP**, paste your Google OAuth credentials
|
121
|
+
4. **Use it:** Start a new Claude chat and call any Google Workspace tool 🎉
|
122
|
+
|
123
|
+
>
|
124
|
+
**Why DXT?**
|
125
|
+
> Desktop Extensions (`.dxt`) bundle the server, dependencies, and manifest so users go from download → working MCP in **three clicks** – no terminal, no JSON editing, no version conflicts.
|
126
|
+
|
127
|
+
#### Required Configuration
|
128
|
+
<details>
|
129
|
+
<summary>Environment - you will configure these in Claude itself, see screenshot:</summary>
|
130
|
+
| Variable | Purpose |
|
131
|
+
|----------|---------|
|
132
|
+
| `GOOGLE_OAUTH_CLIENT_ID` | OAuth client ID from Google Cloud |
|
133
|
+
| `GOOGLE_OAUTH_CLIENT_SECRET` | OAuth client secret |
|
134
|
+
| `USER_GOOGLE_EMAIL` *(optional)* | Default email for single-user auth |
|
135
|
+
| `OAUTHLIB_INSECURE_TRANSPORT=1` | Development only (allows `http://` redirect) |
|
136
|
+
|
137
|
+
Claude Desktop stores these securely in the OS keychain; set them once in the extension pane.
|
138
|
+
</details>
|
139
|
+
Screenshot here
|
140
|
+
|
141
|
+
---
|
142
|
+
|
143
|
+
### 2. Advanced / Cross-Platform Installation
|
144
|
+
|
145
|
+
If you’re developing, deploying to servers, or using another MCP-capable client, keep reading.
|
146
|
+
|
147
|
+
#### Instant CLI (uvx)
|
148
|
+
|
149
|
+
```bash
|
150
|
+
# Requires Python 3.11+ and uvx
|
151
|
+
export GOOGLE_OAUTH_CLIENT_ID="xxx"
|
152
|
+
export GOOGLE_OAUTH_CLIENT_SECRET="yyy"
|
153
|
+
uvx workspace-mcp --tools gmail drive calendar
|
154
|
+
```
|
115
155
|
|
116
156
|
> Run instantly without manual installation - you must configure OAuth credentials when using uvx. You can use either environment variables (recommended for production) or set the `GOOGLE_CLIENT_SECRET_PATH` (or legacy `GOOGLE_CLIENT_SECRETS`) environment variable to point to your `client_secret.json` file.
|
117
157
|
|
@@ -187,9 +227,10 @@ uv run main.py
|
|
187
227
|
|
188
228
|
3. **Server Configuration**:
|
189
229
|
The server's base URL and port can be customized using environment variables:
|
190
|
-
- `WORKSPACE_MCP_BASE_URI`: Sets the base URI for the server (default: http://localhost). This affects the server_url used
|
230
|
+
- `WORKSPACE_MCP_BASE_URI`: Sets the base URI for the server (default: http://localhost). This affects the `server_url` used to construct the default `OAUTH_REDIRECT_URI` if `GOOGLE_OAUTH_REDIRECT_URI` is not set.
|
191
231
|
- `WORKSPACE_MCP_PORT`: Sets the port the server listens on (default: 8000). This affects the server_url, port, and OAUTH_REDIRECT_URI.
|
192
232
|
- `USER_GOOGLE_EMAIL`: Optional default email for authentication flows. If set, the LLM won't need to specify your email when calling `start_google_auth`.
|
233
|
+
- `GOOGLE_OAUTH_REDIRECT_URI`: Sets an override for OAuth redirect specifically, must include a full address (i.e. include port if necessary). Use this if you want to run your OAuth redirect separately from the MCP. This is not recommended outside of very specific cases
|
193
234
|
|
194
235
|
### Start the Server
|
195
236
|
|
@@ -221,7 +262,8 @@ The server supports two transport modes:
|
|
221
262
|
|
222
263
|
#### Stdio Mode (Default - Recommended for Claude Desktop)
|
223
264
|
|
224
|
-
**
|
265
|
+
**Guided Setup (Recommended if not using DXT)**
|
266
|
+
|
225
267
|
```bash
|
226
268
|
python install_claude.py
|
227
269
|
```
|
@@ -37,13 +37,15 @@
|
|
37
37
|
---
|
38
38
|
|
39
39
|
### A quick plug for AI-Enhanced Docs
|
40
|
+
<details>
|
41
|
+
<summary>But why?</summary>
|
40
42
|
|
41
|
-
|
43
|
+
**This README was written with AI assistance, and here's why that matters**
|
42
44
|
>
|
43
|
-
> As a solo
|
45
|
+
> As a solo dev building open source tools that many never see outside use, comprehensive documentation often wouldn't happen without AI help. Using agentic dev tools like **Roo** & **Claude Code** that understand the entire codebase, AI doesn't just regurgitate generic content - it extracts real implementation details and creates accurate, specific documentation.
|
44
46
|
>
|
45
|
-
> In this case, Sonnet 4 took a pass & a human (me) verified them
|
46
|
-
|
47
|
+
> In this case, Sonnet 4 took a pass & a human (me) verified them 7/10/25.
|
48
|
+
</details>
|
47
49
|
|
48
50
|
## Overview
|
49
51
|
|
@@ -61,7 +63,7 @@ A production-ready MCP server that integrates all major Google Workspace service
|
|
61
63
|
- **📝 Google Forms**: Form creation, retrieval, publish settings, and response management
|
62
64
|
- **✓ Google Tasks**: Complete task and task list management with hierarchy, due dates, and status tracking
|
63
65
|
- **💬 Google Chat**: Space management and messaging capabilities
|
64
|
-
- **🔄
|
66
|
+
- **🔄 All Transports**: Stdio, Streamable HTTP & SSE, OpenAPI compatibility via `mcpo`
|
65
67
|
- **⚡ High Performance**: Service caching, thread-safe sessions, FastMCP integration
|
66
68
|
- **🧩 Developer Friendly**: Minimal boilerplate, automatic service injection, centralized configuration
|
67
69
|
|
@@ -69,7 +71,45 @@ A production-ready MCP server that integrates all major Google Workspace service
|
|
69
71
|
|
70
72
|
## 🚀 Quick Start
|
71
73
|
|
72
|
-
###
|
74
|
+
### 1. One-Click Claude Desktop Install (Recommended)
|
75
|
+
|
76
|
+
1. **Download:** Grab the latest `google_workspace_mcp.dxt` from the “Releases” page
|
77
|
+
2. **Install:** Double-click the file – Claude Desktop opens and prompts you to **Install**
|
78
|
+
3. **Configure:** In Claude Desktop → **Settings → Extensions → Google Workspace MCP**, paste your Google OAuth credentials
|
79
|
+
4. **Use it:** Start a new Claude chat and call any Google Workspace tool 🎉
|
80
|
+
|
81
|
+
>
|
82
|
+
**Why DXT?**
|
83
|
+
> Desktop Extensions (`.dxt`) bundle the server, dependencies, and manifest so users go from download → working MCP in **three clicks** – no terminal, no JSON editing, no version conflicts.
|
84
|
+
|
85
|
+
#### Required Configuration
|
86
|
+
<details>
|
87
|
+
<summary>Environment - you will configure these in Claude itself, see screenshot:</summary>
|
88
|
+
| Variable | Purpose |
|
89
|
+
|----------|---------|
|
90
|
+
| `GOOGLE_OAUTH_CLIENT_ID` | OAuth client ID from Google Cloud |
|
91
|
+
| `GOOGLE_OAUTH_CLIENT_SECRET` | OAuth client secret |
|
92
|
+
| `USER_GOOGLE_EMAIL` *(optional)* | Default email for single-user auth |
|
93
|
+
| `OAUTHLIB_INSECURE_TRANSPORT=1` | Development only (allows `http://` redirect) |
|
94
|
+
|
95
|
+
Claude Desktop stores these securely in the OS keychain; set them once in the extension pane.
|
96
|
+
</details>
|
97
|
+
Screenshot here
|
98
|
+
|
99
|
+
---
|
100
|
+
|
101
|
+
### 2. Advanced / Cross-Platform Installation
|
102
|
+
|
103
|
+
If you’re developing, deploying to servers, or using another MCP-capable client, keep reading.
|
104
|
+
|
105
|
+
#### Instant CLI (uvx)
|
106
|
+
|
107
|
+
```bash
|
108
|
+
# Requires Python 3.11+ and uvx
|
109
|
+
export GOOGLE_OAUTH_CLIENT_ID="xxx"
|
110
|
+
export GOOGLE_OAUTH_CLIENT_SECRET="yyy"
|
111
|
+
uvx workspace-mcp --tools gmail drive calendar
|
112
|
+
```
|
73
113
|
|
74
114
|
> Run instantly without manual installation - you must configure OAuth credentials when using uvx. You can use either environment variables (recommended for production) or set the `GOOGLE_CLIENT_SECRET_PATH` (or legacy `GOOGLE_CLIENT_SECRETS`) environment variable to point to your `client_secret.json` file.
|
75
115
|
|
@@ -145,9 +185,10 @@ uv run main.py
|
|
145
185
|
|
146
186
|
3. **Server Configuration**:
|
147
187
|
The server's base URL and port can be customized using environment variables:
|
148
|
-
- `WORKSPACE_MCP_BASE_URI`: Sets the base URI for the server (default: http://localhost). This affects the server_url used
|
188
|
+
- `WORKSPACE_MCP_BASE_URI`: Sets the base URI for the server (default: http://localhost). This affects the `server_url` used to construct the default `OAUTH_REDIRECT_URI` if `GOOGLE_OAUTH_REDIRECT_URI` is not set.
|
149
189
|
- `WORKSPACE_MCP_PORT`: Sets the port the server listens on (default: 8000). This affects the server_url, port, and OAUTH_REDIRECT_URI.
|
150
190
|
- `USER_GOOGLE_EMAIL`: Optional default email for authentication flows. If set, the LLM won't need to specify your email when calling `start_google_auth`.
|
191
|
+
- `GOOGLE_OAUTH_REDIRECT_URI`: Sets an override for OAuth redirect specifically, must include a full address (i.e. include port if necessary). Use this if you want to run your OAuth redirect separately from the MCP. This is not recommended outside of very specific cases
|
151
192
|
|
152
193
|
### Start the Server
|
153
194
|
|
@@ -179,7 +220,8 @@ The server supports two transport modes:
|
|
179
220
|
|
180
221
|
#### Stdio Mode (Default - Recommended for Claude Desktop)
|
181
222
|
|
182
|
-
**
|
223
|
+
**Guided Setup (Recommended if not using DXT)**
|
224
|
+
|
183
225
|
```bash
|
184
226
|
python install_claude.py
|
185
227
|
```
|
@@ -1,14 +1,16 @@
|
|
1
1
|
# auth/google_auth.py
|
2
2
|
|
3
|
-
import
|
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
|
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
|
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=
|
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(
|
184
|
+
def get_oauth_redirect_uri(port: int = 8000, base_uri: str = "http://localhost") -> str:
|
183
185
|
"""
|
184
|
-
Get the appropriate OAuth redirect URI
|
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
|
-
|
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
|
"""
|
@@ -82,7 +82,7 @@ def set_transport_mode(mode: str):
|
|
82
82
|
|
83
83
|
def get_oauth_redirect_uri_for_current_mode() -> str:
|
84
84
|
"""Get OAuth redirect URI based on current transport mode."""
|
85
|
-
return get_oauth_redirect_uri(
|
85
|
+
return get_oauth_redirect_uri(WORKSPACE_MCP_PORT, WORKSPACE_MCP_BASE_URI)
|
86
86
|
|
87
87
|
# Health check endpoint
|
88
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)
|
@@ -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
|
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
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "workspace-mcp"
|
7
|
-
version = "1.1.
|
7
|
+
version = "1.1.5"
|
8
8
|
description = "Comprehensive, highly performant Google Workspace Streamable HTTP & SSE MCP Server for Calendar, Gmail, Docs, Sheets, Slides & Drive"
|
9
9
|
readme = "README.md"
|
10
10
|
keywords = [ "mcp", "google", "workspace", "llm", "ai", "claude", "model", "context", "protocol", "server"]
|
@@ -13,7 +13,6 @@ def test_get_oauth_redirect_uri_prioritizes_env_var(monkeypatch):
|
|
13
13
|
|
14
14
|
# Call the function with parameters that would otherwise construct a different URI
|
15
15
|
redirect_uri = get_oauth_redirect_uri(
|
16
|
-
transport_mode="stdio",
|
17
16
|
port=8888,
|
18
17
|
base_uri="http://should-be-ignored"
|
19
18
|
)
|
@@ -31,7 +30,6 @@ def test_get_oauth_redirect_uri_constructs_uri_when_env_var_is_missing(monkeypat
|
|
31
30
|
|
32
31
|
# Call the function with specific parameters
|
33
32
|
redirect_uri = get_oauth_redirect_uri(
|
34
|
-
transport_mode="stdio",
|
35
33
|
port=9999,
|
36
34
|
base_uri="http://localhost-test"
|
37
35
|
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: workspace-mcp
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.5
|
4
4
|
Summary: Comprehensive, highly performant Google Workspace Streamable HTTP & SSE MCP Server for Calendar, Gmail, Docs, Sheets, Slides & Drive
|
5
5
|
Author-email: Taylor Wilsdon <taylor@taylorwilsdon.com>
|
6
6
|
License: MIT
|
@@ -79,13 +79,15 @@ Dynamic: license-file
|
|
79
79
|
---
|
80
80
|
|
81
81
|
### A quick plug for AI-Enhanced Docs
|
82
|
+
<details>
|
83
|
+
<summary>But why?</summary>
|
82
84
|
|
83
|
-
|
85
|
+
**This README was written with AI assistance, and here's why that matters**
|
84
86
|
>
|
85
|
-
> As a solo
|
87
|
+
> As a solo dev building open source tools that many never see outside use, comprehensive documentation often wouldn't happen without AI help. Using agentic dev tools like **Roo** & **Claude Code** that understand the entire codebase, AI doesn't just regurgitate generic content - it extracts real implementation details and creates accurate, specific documentation.
|
86
88
|
>
|
87
|
-
> In this case, Sonnet 4 took a pass & a human (me) verified them
|
88
|
-
|
89
|
+
> In this case, Sonnet 4 took a pass & a human (me) verified them 7/10/25.
|
90
|
+
</details>
|
89
91
|
|
90
92
|
## Overview
|
91
93
|
|
@@ -103,7 +105,7 @@ A production-ready MCP server that integrates all major Google Workspace service
|
|
103
105
|
- **📝 Google Forms**: Form creation, retrieval, publish settings, and response management
|
104
106
|
- **✓ Google Tasks**: Complete task and task list management with hierarchy, due dates, and status tracking
|
105
107
|
- **💬 Google Chat**: Space management and messaging capabilities
|
106
|
-
- **🔄
|
108
|
+
- **🔄 All Transports**: Stdio, Streamable HTTP & SSE, OpenAPI compatibility via `mcpo`
|
107
109
|
- **⚡ High Performance**: Service caching, thread-safe sessions, FastMCP integration
|
108
110
|
- **🧩 Developer Friendly**: Minimal boilerplate, automatic service injection, centralized configuration
|
109
111
|
|
@@ -111,7 +113,45 @@ A production-ready MCP server that integrates all major Google Workspace service
|
|
111
113
|
|
112
114
|
## 🚀 Quick Start
|
113
115
|
|
114
|
-
###
|
116
|
+
### 1. One-Click Claude Desktop Install (Recommended)
|
117
|
+
|
118
|
+
1. **Download:** Grab the latest `google_workspace_mcp.dxt` from the “Releases” page
|
119
|
+
2. **Install:** Double-click the file – Claude Desktop opens and prompts you to **Install**
|
120
|
+
3. **Configure:** In Claude Desktop → **Settings → Extensions → Google Workspace MCP**, paste your Google OAuth credentials
|
121
|
+
4. **Use it:** Start a new Claude chat and call any Google Workspace tool 🎉
|
122
|
+
|
123
|
+
>
|
124
|
+
**Why DXT?**
|
125
|
+
> Desktop Extensions (`.dxt`) bundle the server, dependencies, and manifest so users go from download → working MCP in **three clicks** – no terminal, no JSON editing, no version conflicts.
|
126
|
+
|
127
|
+
#### Required Configuration
|
128
|
+
<details>
|
129
|
+
<summary>Environment - you will configure these in Claude itself, see screenshot:</summary>
|
130
|
+
| Variable | Purpose |
|
131
|
+
|----------|---------|
|
132
|
+
| `GOOGLE_OAUTH_CLIENT_ID` | OAuth client ID from Google Cloud |
|
133
|
+
| `GOOGLE_OAUTH_CLIENT_SECRET` | OAuth client secret |
|
134
|
+
| `USER_GOOGLE_EMAIL` *(optional)* | Default email for single-user auth |
|
135
|
+
| `OAUTHLIB_INSECURE_TRANSPORT=1` | Development only (allows `http://` redirect) |
|
136
|
+
|
137
|
+
Claude Desktop stores these securely in the OS keychain; set them once in the extension pane.
|
138
|
+
</details>
|
139
|
+
Screenshot here
|
140
|
+
|
141
|
+
---
|
142
|
+
|
143
|
+
### 2. Advanced / Cross-Platform Installation
|
144
|
+
|
145
|
+
If you’re developing, deploying to servers, or using another MCP-capable client, keep reading.
|
146
|
+
|
147
|
+
#### Instant CLI (uvx)
|
148
|
+
|
149
|
+
```bash
|
150
|
+
# Requires Python 3.11+ and uvx
|
151
|
+
export GOOGLE_OAUTH_CLIENT_ID="xxx"
|
152
|
+
export GOOGLE_OAUTH_CLIENT_SECRET="yyy"
|
153
|
+
uvx workspace-mcp --tools gmail drive calendar
|
154
|
+
```
|
115
155
|
|
116
156
|
> Run instantly without manual installation - you must configure OAuth credentials when using uvx. You can use either environment variables (recommended for production) or set the `GOOGLE_CLIENT_SECRET_PATH` (or legacy `GOOGLE_CLIENT_SECRETS`) environment variable to point to your `client_secret.json` file.
|
117
157
|
|
@@ -187,9 +227,10 @@ uv run main.py
|
|
187
227
|
|
188
228
|
3. **Server Configuration**:
|
189
229
|
The server's base URL and port can be customized using environment variables:
|
190
|
-
- `WORKSPACE_MCP_BASE_URI`: Sets the base URI for the server (default: http://localhost). This affects the server_url used
|
230
|
+
- `WORKSPACE_MCP_BASE_URI`: Sets the base URI for the server (default: http://localhost). This affects the `server_url` used to construct the default `OAUTH_REDIRECT_URI` if `GOOGLE_OAUTH_REDIRECT_URI` is not set.
|
191
231
|
- `WORKSPACE_MCP_PORT`: Sets the port the server listens on (default: 8000). This affects the server_url, port, and OAUTH_REDIRECT_URI.
|
192
232
|
- `USER_GOOGLE_EMAIL`: Optional default email for authentication flows. If set, the LLM won't need to specify your email when calling `start_google_auth`.
|
233
|
+
- `GOOGLE_OAUTH_REDIRECT_URI`: Sets an override for OAuth redirect specifically, must include a full address (i.e. include port if necessary). Use this if you want to run your OAuth redirect separately from the MCP. This is not recommended outside of very specific cases
|
193
234
|
|
194
235
|
### Start the Server
|
195
236
|
|
@@ -221,7 +262,8 @@ The server supports two transport modes:
|
|
221
262
|
|
222
263
|
#### Stdio Mode (Default - Recommended for Claude Desktop)
|
223
264
|
|
224
|
-
**
|
265
|
+
**Guided Setup (Recommended if not using DXT)**
|
266
|
+
|
225
267
|
```bash
|
226
268
|
python install_claude.py
|
227
269
|
```
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|