99
1010import json
1111import logging
12+ import re
1213import time
1314from typing import Any
1415
1516import httpx
1617
17- from everyrow_mcp .redis_store import build_key , get_redis_client
18+ from everyrow_mcp .redis_store import (
19+ build_key ,
20+ decrypt_value ,
21+ encrypt_value ,
22+ get_redis_client ,
23+ )
1824
1925logger = logging .getLogger (__name__ )
2026
2127SHEETS_API_BASE = "https://sheets.googleapis.com/v4/spreadsheets"
2228DRIVE_API_BASE = "https://www.googleapis.com/drive/v3"
2329
2430# Google token TTL and refresh buffer
25- GOOGLE_TOKEN_TTL = 3600 # 1 hour
31+ GOOGLE_TOKEN_TTL_DEFAULT = 3600 # 1 hour
2632GOOGLE_TOKEN_REFRESH_BUFFER = 300 # refresh 5 min before expiry
27- GOOGLE_TOKEN_REDIS_TTL = 3600 # store for 1 hour in Redis
2833
2934
3035# ── Token resolution ──────────────────────────────────────────────────
3136
3237
33- async def get_google_token () -> str :
38+ async def get_google_token (user_id : str | None = None ) -> str :
3439 """Resolve a valid Google access token from Redis.
3540
3641 The token is stored during the OAuth callback when the user logs in
3742 via Google through Supabase. Auto-refreshes if near expiry.
3843
3944 Only available in HTTP mode — sheets tools are removed in stdio mode.
4045 """
46+ if user_id is None :
47+ from mcp .server .auth .middleware .auth_context import ( # noqa: PLC0415
48+ get_access_token ,
49+ )
50+
51+ access_token = get_access_token ()
52+ user_id = access_token .client_id if access_token else None
53+ if not user_id :
54+ raise RuntimeError (
55+ "No authenticated user. The user must log in with Google "
56+ "(with Sheets scopes) to use Google Sheets tools."
57+ )
4158
4259 redis = get_redis_client ()
4360
44- # NOTE: single-tenant — "current" key is shared. If multi-tenancy is
45- # needed, key by session/user ID instead.
46- token_key = build_key ("google_token" , "current" )
47- token_data = await redis .get (token_key )
48- if token_data :
49- data = json .loads (token_data )
61+ token_key = build_key ("google_token" , user_id )
62+ raw = await redis .get (token_key )
63+ if raw :
64+ data = json .loads (decrypt_value (raw ))
5065 expires_at = data .get ("expires_at" , 0 )
5166 if time .time () < expires_at - GOOGLE_TOKEN_REFRESH_BUFFER :
5267 return data ["access_token" ]
@@ -55,11 +70,9 @@ async def get_google_token() -> str:
5570 refresh_token = data .get ("refresh_token" )
5671 if refresh_token :
5772 try :
58- return await _refresh_google_token_http (refresh_token )
59- except Exception :
60- logger .warning (
61- "Failed to refresh Google token, using existing" , exc_info = True
62- )
73+ return await _refresh_google_token_http (refresh_token , user_id )
74+ except Exception as e :
75+ logger .warning ("Failed to refresh Google token: %s" , type (e ).__name__ )
6376 if time .time () < expires_at :
6477 return data ["access_token" ]
6578
@@ -69,7 +82,7 @@ async def get_google_token() -> str:
6982 )
7083
7184
72- async def _refresh_google_token_http (refresh_token : str ) -> str :
85+ async def _refresh_google_token_http (refresh_token : str , user_id : str ) -> str :
7386 """Refresh a Google access token using the Supabase-stored refresh token."""
7487 from everyrow_mcp .config import settings # noqa: PLC0415
7588
@@ -88,38 +101,46 @@ async def _refresh_google_token_http(refresh_token: str) -> str:
88101
89102 provider_token = data .get ("provider_token" , "" )
90103 provider_refresh_token = data .get ("provider_refresh_token" , refresh_token )
104+ expires_in = data .get ("expires_in" )
91105
92106 if not provider_token :
93107 raise RuntimeError ("Supabase refresh did not return a Google provider_token" )
94108
95- await store_google_token ("current" , provider_token , provider_refresh_token )
109+ await store_google_token (
110+ user_id , provider_token , provider_refresh_token , expires_in = expires_in
111+ )
96112 return provider_token
97113
98114
99115async def store_google_token (
100116 user_id : str ,
101117 access_token : str ,
102118 refresh_token : str | None = None ,
119+ * ,
120+ expires_in : int | None = None ,
103121) -> None :
104122 """Store Google access token in Redis with TTL."""
105123 try :
106124 redis = get_redis_client ()
107125 except Exception :
108- return
126+ logger .error ("Failed to obtain Redis client for Google token storage" )
127+ raise
128+ ttl = expires_in if expires_in and expires_in > 0 else GOOGLE_TOKEN_TTL_DEFAULT
109129 try :
110- data = {
130+ data : dict [ str , Any ] = {
111131 "access_token" : access_token ,
112- "expires_at" : time .time () + GOOGLE_TOKEN_TTL ,
132+ "expires_at" : time .time () + ttl ,
113133 }
114134 if refresh_token :
115135 data ["refresh_token" ] = refresh_token
116136 await redis .setex (
117137 build_key ("google_token" , user_id ),
118- GOOGLE_TOKEN_REDIS_TTL ,
119- json .dumps (data ),
138+ ttl ,
139+ encrypt_value ( json .dumps (data ) ),
120140 )
121141 except Exception :
122- logger .warning ("Failed to store Google token in Redis for %s" , user_id )
142+ logger .error ("Failed to store Google token in Redis for %s" , user_id )
143+ raise
123144
124145
125146# ── Sheets API client ─────────────────────────────────────────────────
@@ -224,8 +245,7 @@ async def list_spreadsheets(
224245 """
225246 q = "mimeType='application/vnd.google-apps.spreadsheet' and trashed=false"
226247 if query :
227- # Escape single quotes in the user's query
228- safe_query = query .replace ("'" , "\\ '" )
248+ safe_query = re .sub (r"[^a-zA-Z0-9 ]" , "" , query )
229249 q += f" and name contains '{ safe_query } '"
230250
231251 resp = await self ._client .get (
0 commit comments