Skip to content

Commit 9ea5aac

Browse files
authored
feat: replace google drive tool bundle with Google Drive MCP server (#708)
* init google drive conversion * wip: add tools to server.py * fix: example client * fix: readme * feat: add tests * fix: read credential from env thus support single user usage * fix: get access token from custom http header, based on #3279 * wip: adding all necessary pieces * fix: optional params * fix: pytest * fix: ruff and remove code not needed
1 parent be34e6f commit 9ea5aac

40 files changed

+2579
-701
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Build release google-drive
2+
3+
permissions:
4+
id-token: write
5+
contents: read
6+
packages: write
7+
8+
on:
9+
workflow_dispatch:
10+
push:
11+
paths:
12+
- 'google/drive/**'
13+
branches:
14+
- main
15+
16+
jobs:
17+
oss-tools-build:
18+
runs-on: depot-ubuntu-22.04
19+
concurrency:
20+
group: google-drive-build
21+
cancel-in-progress: true
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Log in to GitHub Container Registry
26+
uses: docker/login-action@v3
27+
with:
28+
registry: ghcr.io
29+
username: ${{ secrets.GHCR_USERNAME }}
30+
password: ${{ secrets.GHCR_TOKEN }}
31+
32+
- name: Build and Push Docker image
33+
uses: depot/build-push-action@v1
34+
id: build-and-push
35+
with:
36+
project: bbqjs4tj1g
37+
context: ./google/drive
38+
push: true
39+
pull: true
40+
platforms: linux/amd64,linux/arm64
41+
tags: |
42+
ghcr.io/${{ github.repository }}/google-drive:latest
43+
44+
- name: Install Cosign
45+
uses: sigstore/[email protected]
46+
with:
47+
cosign-release: 'v2.4.3'
48+
49+
- name: Sign Images
50+
env:
51+
DIGEST: ${{ steps.build-and-push.outputs.digest }}
52+
TAGS: ghcr.io/${{ github.repository }}/google-drive:latest
53+
run: |
54+
images=""
55+
for tag in ${TAGS}; do
56+
images+="${tag}@${DIGEST} "
57+
done
58+
cosign sign --yes ${images}

google/drive/.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

google/drive/Dockerfile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
2+
3+
# Prevents Python from writing .pyc files and buffers stdout/stderr
4+
ENV PYTHONDONTWRITEBYTECODE=1 \
5+
PYTHONUNBUFFERED=1
6+
7+
WORKDIR /app
8+
9+
# Install system deps (if any needed) - using apt for Debian
10+
RUN apt-get update && apt-get install -y --no-install-recommends \
11+
build-essential \
12+
git \
13+
&& rm -rf /var/lib/apt/lists/*
14+
15+
# Copy uv project files first for better Docker layer caching
16+
COPY pyproject.toml uv.lock ./
17+
18+
# Copy your application code (needed for local package build)
19+
COPY . .
20+
21+
# Install dependencies using uv sync (faster and more reliable)
22+
RUN uv sync --frozen --no-dev --no-editable
23+
24+
EXPOSE 9000
25+
26+
# Start the server using uv run (ensures proper virtual environment)
27+
CMD ["uv", "run", "python", "-m", "app.server"]

google/drive/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Obot Google Drive MCP Server
2+
3+
## Installation & Running
4+
5+
### Docker-compose
6+
Export (Google's) Oauth CLient ID and Secret for Oauth Proxy
7+
```bash
8+
export OAUTH_CLIENT_ID=xxx
9+
export OAUTH_CLIENT_SECRET=xxx
10+
```
11+
12+
then:
13+
```bash
14+
docker-compose up
15+
```
16+
17+
### Using uvx
18+
install from local directory:
19+
```bash
20+
uvx --from . obot-google-drive-mcp
21+
```
22+
23+
### Option 2: Using uv (Development)
24+
Install dependencies:
25+
```bash
26+
uv pip install
27+
```
28+
29+
Run the server:
30+
```bash
31+
uv run server.py
32+
```
33+
34+
## Testing
35+
36+
### Unit-test with pytest
37+
```
38+
uv run python -m pytest
39+
```
40+
41+
### Integration Testing
42+
43+
#### Get Your Access Token
44+
This MCP server assumes Obot will take care of the Oauth2.0 flow and supply an access token. To test locally or without Obot, you need to get an access token by yourself. I use [postman workspace](https://blog.postman.com/how-to-access-google-apis-using-oauth-in-postman/) to create and manage my tokens.

google/drive/apis/workspace_file.py

Lines changed: 0 additions & 127 deletions
This file was deleted.

google/drive/app/__init__.py

Whitespace-only changes.

google/drive/app/apis/__init__.py

Whitespace-only changes.
File renamed without changes.

google/drive/apis/helper.py renamed to google/drive/app/apis/helper.py

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from google.oauth2.credentials import Credentials
33
from googleapiclient.discovery import build
44
from googleapiclient.errors import HttpError
5-
import os
5+
from fastmcp.exceptions import ToolError
66
import logging
77

88

@@ -32,39 +32,26 @@ def setup_logger(name, tool_name: str = "Google Drive Tool"):
3232
logger = setup_logger(__name__)
3333

3434

35-
def str_to_bool(value):
36-
"""Convert a string to a boolean."""
37-
return str(value).lower() in ("true", "1", "yes")
35+
def get_client(cred_token: str, service_name: str = "drive", version: str = "v3"):
3836

39-
40-
def get_client(service_name: str = "drive", version: str = "v3"):
41-
token = os.getenv("GOOGLE_OAUTH_TOKEN")
42-
if token is None:
43-
raise ValueError("GOOGLE_OAUTH_TOKEN environment variable is not set")
44-
45-
creds = Credentials(token=token)
37+
creds = Credentials(token=cred_token)
4638
try:
4739
service = build(serviceName=service_name, version=version, credentials=creds)
4840
return service
4941
except HttpError as err:
50-
print(err)
51-
exit(1)
52-
53-
54-
def get_obot_user_timezone():
55-
return os.getenv("OBOT_USER_TIMEZONE", "UTC").strip()
42+
raise ToolError(f"HttpError retrieving google {service_name} client: {err}")
5643

5744

5845
def get_user_timezone(service):
5946
"""Fetches the authenticated user's time zone from User's Google Calendar settings."""
6047
try:
6148
settings = service.settings().get(setting="timezone").execute()
6249
return settings.get(
63-
"value", get_obot_user_timezone()
64-
) # Default to Obot's user timezone if not found
50+
"value", "UTC"
51+
) # Default to UTC if not found
6552
except HttpError as err:
6653
if err.status_code == 403:
67-
raise Exception(f"HttpError retrieving user timezone: {err}")
54+
raise ToolError(f"HttpError retrieving user timezone: {err}")
6855
logger.error(f"HttpError retrieving user timezone: {err}")
6956
return "UTC"
7057
except Exception as e:

0 commit comments

Comments
 (0)