Skip to content

Commit 725b772

Browse files
authored
Merge pull request #3 from cfdude/dev
feat(drive): Add update_drive_file function with OAuth fixes and comprehensive file management
2 parents e58ca9e + f0e10a5 commit 725b772

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+4124
-2794
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ mcp_server_debug.log
2929
# ---- Local development files -------------------------------------------
3030
/.credentials
3131
/.claude
32+
.serena/

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,8 +698,9 @@ cp .env.oauth21 .env
698698
|------|------|-------------|
699699
| `search_drive_files` | **Core** | Search files with query syntax |
700700
| `get_drive_file_content` | **Core** | Read file content (Office formats) |
701-
| `list_drive_items` | Extended | List folder contents |
702701
| `create_drive_file` | **Core** | Create files or fetch from URLs |
702+
| `list_drive_items` | Extended | List folder contents |
703+
| `update_drive_file` | Extended | Update file metadata, move between folders |
703704

704705
</td>
705706
</tr>

auth/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
# Make the auth directory a Python package
1+
# Make the auth directory a Python package

auth/auth_info_middleware.py

Lines changed: 205 additions & 96 deletions
Large diffs are not rendered by default.

auth/fastmcp_google_auth.py

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -27,116 +27,126 @@
2727
class GoogleWorkspaceAuthProvider(AuthProvider):
2828
"""
2929
Authentication provider for Google Workspace integration.
30-
30+
3131
This provider implements the Remote Authentication pattern where:
3232
- Google acts as the Authorization Server (AS)
3333
- This MCP server acts as a Resource Server (RS)
3434
- Tokens are verified using Google's public keys
3535
"""
36-
36+
3737
def __init__(self):
3838
"""Initialize the Google Workspace auth provider."""
3939
super().__init__()
40-
40+
4141
# Get configuration from OAuth config
4242
from auth.oauth_config import get_oauth_config
43+
4344
config = get_oauth_config()
44-
45+
4546
self.client_id = config.client_id
4647
self.client_secret = config.client_secret
4748
self.base_url = config.get_oauth_base_url()
4849
self.port = config.port
49-
50+
5051
if not self.client_id:
51-
logger.warning("GOOGLE_OAUTH_CLIENT_ID not set - OAuth 2.1 authentication will not work")
52+
logger.warning(
53+
"GOOGLE_OAUTH_CLIENT_ID not set - OAuth 2.1 authentication will not work"
54+
)
5255
return
53-
56+
5457
# Initialize JWT verifier for Google tokens
5558
self.jwt_verifier = JWTVerifier(
5659
jwks_uri="https://www.googleapis.com/oauth2/v3/certs",
5760
issuer="https://accounts.google.com",
5861
audience=self.client_id,
59-
algorithm="RS256"
62+
algorithm="RS256",
6063
)
61-
64+
6265
# Session bridging now handled by OAuth21SessionStore
63-
66+
6467
async def verify_token(self, token: str) -> Optional[AccessToken]:
6568
"""
6669
Verify a bearer token issued by Google.
67-
70+
6871
Args:
6972
token: The bearer token to verify
70-
73+
7174
Returns:
7275
AccessToken object if valid, None otherwise
7376
"""
7477
if not self.client_id:
7578
return None
76-
79+
7780
try:
7881
# Use FastMCP's JWT verifier
7982
access_token = await self.jwt_verifier.verify_token(token)
80-
83+
8184
if access_token:
8285
# Store session info in OAuth21SessionStore for credential bridging
8386
user_email = access_token.claims.get("email")
8487
if user_email:
8588
from auth.oauth21_session_store import get_oauth21_session_store
89+
8690
store = get_oauth21_session_store()
8791
session_id = f"google_{access_token.claims.get('sub', 'unknown')}"
88-
92+
8993
# Try to get FastMCP session ID for binding
9094
mcp_session_id = None
9195
try:
9296
from fastmcp.server.dependencies import get_context
97+
9398
ctx = get_context()
94-
if ctx and hasattr(ctx, 'session_id'):
99+
if ctx and hasattr(ctx, "session_id"):
95100
mcp_session_id = ctx.session_id
96-
logger.debug(f"Binding MCP session {mcp_session_id} to user {user_email}")
101+
logger.debug(
102+
f"Binding MCP session {mcp_session_id} to user {user_email}"
103+
)
97104
except Exception:
98105
pass
99-
106+
100107
store.store_session(
101108
user_email=user_email,
102109
access_token=token,
103110
scopes=access_token.scopes or [],
104111
session_id=session_id,
105-
mcp_session_id=mcp_session_id
112+
mcp_session_id=mcp_session_id,
106113
)
107-
108-
logger.debug(f"Successfully verified Google token for user: {user_email}")
109-
114+
115+
logger.debug(
116+
f"Successfully verified Google token for user: {user_email}"
117+
)
118+
110119
return access_token
111-
120+
112121
except Exception as e:
113122
logger.error(f"Failed to verify Google token: {e}")
114123
return None
115-
124+
116125
def customize_auth_routes(self, routes: List[Route]) -> List[Route]:
117126
"""
118-
NOTE: This method is not currently used. All OAuth 2.1 routes are implemented
127+
NOTE: This method is not currently used. All OAuth 2.1 routes are implemented
119128
directly in core/server.py using @server.custom_route decorators.
120-
129+
121130
This method exists for compatibility with FastMCP's AuthProvider interface
122131
but the routes it would define are handled elsewhere.
123132
"""
124133
# Routes are implemented directly in core/server.py
125134
return routes
126-
135+
127136
def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
128137
"""
129138
Get session information for credential bridging from OAuth21SessionStore.
130-
139+
131140
Args:
132141
session_id: The session identifier
133-
142+
134143
Returns:
135144
Session information if found
136145
"""
137146
from auth.oauth21_session_store import get_oauth21_session_store
147+
138148
store = get_oauth21_session_store()
139-
149+
140150
# Try to get user by session_id (assuming it's the MCP session ID)
141151
user_email = store.get_user_by_mcp_session(session_id)
142152
if user_email:
@@ -145,28 +155,27 @@ def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
145155
return {
146156
"access_token": credentials.token,
147157
"user_email": user_email,
148-
"scopes": credentials.scopes or []
158+
"scopes": credentials.scopes or [],
149159
}
150160
return None
151-
161+
152162
def create_session_from_token(self, token: str, user_email: str) -> str:
153163
"""
154164
Create a session from an access token for credential bridging using OAuth21SessionStore.
155-
165+
156166
Args:
157167
token: The access token
158168
user_email: The user's email address
159-
169+
160170
Returns:
161171
Session ID
162172
"""
163173
from auth.oauth21_session_store import get_oauth21_session_store
174+
164175
store = get_oauth21_session_store()
165176
session_id = f"google_{user_email}"
166-
177+
167178
store.store_session(
168-
user_email=user_email,
169-
access_token=token,
170-
session_id=session_id
179+
user_email=user_email, access_token=token, session_id=session_id
171180
)
172-
return session_id
181+
return session_id

auth/google_auth.py

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from google.auth.exceptions import RefreshError
1515
from googleapiclient.discovery import build
1616
from googleapiclient.errors import HttpError
17-
from auth.scopes import SCOPES, get_current_scopes # noqa
17+
from auth.scopes import SCOPES, get_current_scopes # noqa
1818
from auth.oauth21_session_store import get_oauth21_session_store
1919
from auth.credential_store import get_credential_store
2020
from auth.oauth_config import get_oauth_config, is_stateless_mode
@@ -135,11 +135,15 @@ def save_credentials_to_session(session_id: str, credentials: Credentials):
135135
client_secret=credentials.client_secret,
136136
scopes=credentials.scopes,
137137
expiry=credentials.expiry,
138-
mcp_session_id=session_id
138+
mcp_session_id=session_id,
139+
)
140+
logger.debug(
141+
f"Credentials saved to OAuth21SessionStore for session_id: {session_id}, user: {user_email}"
139142
)
140-
logger.debug(f"Credentials saved to OAuth21SessionStore for session_id: {session_id}, user: {user_email}")
141143
else:
142-
logger.warning(f"Could not save credentials to session store - no user email found for session: {session_id}")
144+
logger.warning(
145+
f"Could not save credentials to session store - no user email found for session: {session_id}"
146+
)
143147

144148

145149
def load_credentials_from_session(session_id: str) -> Optional[Credentials]:
@@ -477,7 +481,7 @@ def handle_auth_callback(
477481
scopes=credentials.scopes,
478482
expiry=credentials.expiry,
479483
mcp_session_id=session_id,
480-
issuer="https://accounts.google.com" # Add issuer for Google tokens
484+
issuer="https://accounts.google.com", # Add issuer for Google tokens
481485
)
482486

483487
# If session_id is provided, also save to session cache for compatibility
@@ -521,7 +525,9 @@ def get_credentials(
521525
# Try to get credentials by MCP session
522526
credentials = store.get_credentials_by_mcp_session(session_id)
523527
if credentials:
524-
logger.info(f"[get_credentials] Found OAuth 2.1 credentials for MCP session {session_id}")
528+
logger.info(
529+
f"[get_credentials] Found OAuth 2.1 credentials for MCP session {session_id}"
530+
)
525531

526532
# Check scopes
527533
if not all(scope in credentials.scopes for scope in required_scopes):
@@ -537,7 +543,9 @@ def get_credentials(
537543
# Try to refresh
538544
try:
539545
credentials.refresh(Request())
540-
logger.info(f"[get_credentials] Refreshed OAuth 2.1 credentials for session {session_id}")
546+
logger.info(
547+
f"[get_credentials] Refreshed OAuth 2.1 credentials for session {session_id}"
548+
)
541549
# Update stored credentials
542550
user_email = store.get_user_by_mcp_session(session_id)
543551
if user_email:
@@ -547,11 +555,13 @@ def get_credentials(
547555
refresh_token=credentials.refresh_token,
548556
scopes=credentials.scopes,
549557
expiry=credentials.expiry,
550-
mcp_session_id=session_id
558+
mcp_session_id=session_id,
551559
)
552560
return credentials
553561
except Exception as e:
554-
logger.error(f"[get_credentials] Failed to refresh OAuth 2.1 credentials: {e}")
562+
logger.error(
563+
f"[get_credentials] Failed to refresh OAuth 2.1 credentials: {e}"
564+
)
555565
return None
556566
except ImportError:
557567
pass # OAuth 2.1 store not available
@@ -672,7 +682,9 @@ def get_credentials(
672682
credential_store = get_credential_store()
673683
credential_store.store_credential(user_google_email, credentials)
674684
else:
675-
logger.info(f"Skipping credential file save in stateless mode for {user_google_email}")
685+
logger.info(
686+
f"Skipping credential file save in stateless mode for {user_google_email}"
687+
)
676688

677689
# Also update OAuth21SessionStore
678690
store = get_oauth21_session_store()
@@ -686,7 +698,7 @@ def get_credentials(
686698
scopes=credentials.scopes,
687699
expiry=credentials.expiry,
688700
mcp_session_id=session_id,
689-
issuer="https://accounts.google.com" # Add issuer for Google tokens
701+
issuer="https://accounts.google.com", # Add issuer for Google tokens
690702
)
691703

692704
if session_id: # Update session cache if it was the source or is active
@@ -775,9 +787,13 @@ async def get_authenticated_google_service(
775787
# First try context variable (works in async context)
776788
session_id = get_fastmcp_session_id()
777789
if session_id:
778-
logger.debug(f"[{tool_name}] Got FastMCP session ID from context: {session_id}")
790+
logger.debug(
791+
f"[{tool_name}] Got FastMCP session ID from context: {session_id}"
792+
)
779793
else:
780-
logger.debug(f"[{tool_name}] Context variable returned None/empty session ID")
794+
logger.debug(
795+
f"[{tool_name}] Context variable returned None/empty session ID"
796+
)
781797
except Exception as e:
782798
logger.debug(
783799
f"[{tool_name}] Could not get FastMCP session from context: {e}"
@@ -787,17 +803,25 @@ async def get_authenticated_google_service(
787803
if not session_id and get_fastmcp_context:
788804
try:
789805
fastmcp_ctx = get_fastmcp_context()
790-
if fastmcp_ctx and hasattr(fastmcp_ctx, 'session_id'):
806+
if fastmcp_ctx and hasattr(fastmcp_ctx, "session_id"):
791807
session_id = fastmcp_ctx.session_id
792-
logger.debug(f"[{tool_name}] Got FastMCP session ID directly: {session_id}")
808+
logger.debug(
809+
f"[{tool_name}] Got FastMCP session ID directly: {session_id}"
810+
)
793811
else:
794-
logger.debug(f"[{tool_name}] FastMCP context exists but no session_id attribute")
812+
logger.debug(
813+
f"[{tool_name}] FastMCP context exists but no session_id attribute"
814+
)
795815
except Exception as e:
796-
logger.debug(f"[{tool_name}] Could not get FastMCP context directly: {e}")
816+
logger.debug(
817+
f"[{tool_name}] Could not get FastMCP context directly: {e}"
818+
)
797819

798820
# Final fallback: log if we still don't have session_id
799821
if not session_id:
800-
logger.warning(f"[{tool_name}] Unable to obtain FastMCP session ID from any source")
822+
logger.warning(
823+
f"[{tool_name}] Unable to obtain FastMCP session ID from any source"
824+
)
801825

802826
logger.info(
803827
f"[{tool_name}] Attempting to get authenticated {service_name} service. Email: '{user_google_email}', Session: '{session_id}'"
@@ -818,8 +842,12 @@ async def get_authenticated_google_service(
818842
)
819843

820844
if not credentials or not credentials.valid:
821-
logger.warning(f"[{tool_name}] No valid credentials. Email: '{user_google_email}'.")
822-
logger.info(f"[{tool_name}] Valid email '{user_google_email}' provided, initiating auth flow.")
845+
logger.warning(
846+
f"[{tool_name}] No valid credentials. Email: '{user_google_email}'."
847+
)
848+
logger.info(
849+
f"[{tool_name}] Valid email '{user_google_email}' provided, initiating auth flow."
850+
)
823851

824852
# Ensure OAuth callback is available
825853
from auth.oauth_callback_server import ensure_oauth_callback_available

auth/google_remote_auth_provider.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ def __init__(self):
6161

6262
# Get configuration from OAuth config
6363
from auth.oauth_config import get_oauth_config
64+
6465
config = get_oauth_config()
65-
66+
6667
self.client_id = config.client_id
6768
self.client_secret = config.client_secret
6869
self.base_url = config.get_oauth_base_url()

0 commit comments

Comments
 (0)