2025-06-06 12:45:15 -04:00
"""
Google Sheets MCP Tools
This module provides MCP tools for interacting with Google Sheets API.
"""
import logging
import asyncio
2025-08-10 12:00:36 +03:00
import json
from typing import List , Optional , Union
2025-06-06 12:45:15 -04:00
2025-06-07 10:21:12 -04:00
from auth . service_decorator import require_google_service
2025-06-06 12:45:15 -04:00
from core . server import server
2025-07-17 13:57:21 -04:00
from core . utils import handle_http_errors
2025-07-01 17:14:30 -07:00
from core . comments import create_comment_tools
2025-06-06 12:45:15 -04:00
# Configure module logger
logger = logging . getLogger ( __name__ )
@server.tool ( )
2025-07-28 11:49:01 -04:00
@handle_http_errors ( " list_spreadsheets " , is_read_only = True , service_type = " sheets " )
2025-06-07 10:21:12 -04:00
@require_google_service ( " drive " , " drive_read " )
2025-06-06 12:45:15 -04:00
async def list_spreadsheets (
2025-06-07 10:21:12 -04:00
service ,
2025-06-06 12:45:15 -04:00
user_google_email : str ,
max_results : int = 25 ,
2025-06-06 17:32:09 -04:00
) - > str :
2025-06-06 12:45:15 -04:00
"""
Lists spreadsheets from Google Drive that the user has access to.
2025-06-06 12:50:32 -04:00
2025-06-06 12:45:15 -04:00
Args:
user_google_email (str): The user ' s Google email address. Required.
max_results (int): Maximum number of spreadsheets to return. Defaults to 25.
2025-06-06 12:50:32 -04:00
2025-06-06 12:45:15 -04:00
Returns:
2025-06-06 17:32:09 -04:00
str: A formatted list of spreadsheet files (name, ID, modified time).
2025-06-06 12:45:15 -04:00
"""
2025-06-07 10:21:12 -04:00
logger . info ( f " [list_spreadsheets] Invoked. Email: ' { user_google_email } ' " )
2025-06-06 12:45:15 -04:00
2025-06-18 16:29:35 -04:00
files_response = await asyncio . to_thread (
service . files ( )
. list (
q = " mimeType= ' application/vnd.google-apps.spreadsheet ' " ,
pageSize = max_results ,
fields = " files(id,name,modifiedTime,webViewLink) " ,
orderBy = " modifiedTime desc " ,
2025-11-04 10:19:37 -05:00
supportsAllDrives = True ,
includeItemsFromAllDrives = True
2025-06-06 12:45:15 -04:00
)
2025-06-18 16:29:35 -04:00
. execute
)
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
files = files_response . get ( " files " , [ ] )
if not files :
return f " No spreadsheets found for { user_google_email } . "
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
spreadsheets_list = [
f " - \" { file [ ' name ' ] } \" (ID: { file [ ' id ' ] } ) | Modified: { file . get ( ' modifiedTime ' , ' Unknown ' ) } | Link: { file . get ( ' webViewLink ' , ' No link ' ) } "
for file in files
]
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
text_output = (
f " Successfully listed { len ( files ) } spreadsheets for { user_google_email } : \n "
+ " \n " . join ( spreadsheets_list )
)
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
logger . info ( f " Successfully listed { len ( files ) } spreadsheets for { user_google_email } . " )
return text_output
2025-06-06 12:45:15 -04:00
@server.tool ( )
2025-07-28 11:49:01 -04:00
@handle_http_errors ( " get_spreadsheet_info " , is_read_only = True , service_type = " sheets " )
2025-06-07 10:21:12 -04:00
@require_google_service ( " sheets " , " sheets_read " )
2025-06-06 12:45:15 -04:00
async def get_spreadsheet_info (
2025-06-07 10:21:12 -04:00
service ,
2025-06-06 12:45:15 -04:00
user_google_email : str ,
spreadsheet_id : str ,
2025-06-06 17:32:09 -04:00
) - > str :
2025-06-06 12:45:15 -04:00
"""
Gets information about a specific spreadsheet including its sheets.
2025-06-06 12:50:32 -04:00
2025-06-06 12:45:15 -04:00
Args:
user_google_email (str): The user ' s Google email address. Required.
spreadsheet_id (str): The ID of the spreadsheet to get info for. Required.
2025-06-06 12:50:32 -04:00
2025-06-06 12:45:15 -04:00
Returns:
2025-06-06 17:32:09 -04:00
str: Formatted spreadsheet information including title and sheets list.
2025-06-06 12:45:15 -04:00
"""
2025-06-07 10:21:12 -04:00
logger . info ( f " [get_spreadsheet_info] Invoked. Email: ' { user_google_email } ' , Spreadsheet ID: { spreadsheet_id } " )
2025-06-06 12:45:15 -04:00
2025-06-18 16:29:35 -04:00
spreadsheet = await asyncio . to_thread (
service . spreadsheets ( ) . get ( spreadsheetId = spreadsheet_id ) . execute
)
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
title = spreadsheet . get ( " properties " , { } ) . get ( " title " , " Unknown " )
sheets = spreadsheet . get ( " sheets " , [ ] )
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
sheets_info = [ ]
for sheet in sheets :
sheet_props = sheet . get ( " properties " , { } )
sheet_name = sheet_props . get ( " title " , " Unknown " )
sheet_id = sheet_props . get ( " sheetId " , " Unknown " )
grid_props = sheet_props . get ( " gridProperties " , { } )
rows = grid_props . get ( " rowCount " , " Unknown " )
cols = grid_props . get ( " columnCount " , " Unknown " )
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
sheets_info . append (
f " - \" { sheet_name } \" (ID: { sheet_id } ) | Size: { rows } x { cols } "
2025-06-06 12:45:15 -04:00
)
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
text_output = (
f " Spreadsheet: \" { title } \" (ID: { spreadsheet_id } ) \n "
f " Sheets ( { len ( sheets ) } ): \n "
+ " \n " . join ( sheets_info ) if sheets_info else " No sheets found "
)
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
logger . info ( f " Successfully retrieved info for spreadsheet { spreadsheet_id } for { user_google_email } . " )
return text_output
2025-06-06 12:45:15 -04:00
@server.tool ( )
2025-07-28 11:49:01 -04:00
@handle_http_errors ( " read_sheet_values " , is_read_only = True , service_type = " sheets " )
2025-06-07 10:21:12 -04:00
@require_google_service ( " sheets " , " sheets_read " )
2025-06-06 12:45:15 -04:00
async def read_sheet_values (
2025-06-07 10:21:12 -04:00
service ,
2025-06-06 12:45:15 -04:00
user_google_email : str ,
spreadsheet_id : str ,
range_name : str = " A1:Z1000 " ,
2025-06-06 17:32:09 -04:00
) - > str :
2025-06-06 12:45:15 -04:00
"""
Reads values from a specific range in a Google Sheet.
2025-06-06 12:50:32 -04:00
2025-06-06 12:45:15 -04:00
Args:
user_google_email (str): The user ' s Google email address. Required.
spreadsheet_id (str): The ID of the spreadsheet. Required.
range_name (str): The range to read (e.g., " Sheet1!A1:D10 " , " A1:D10 " ). Defaults to " A1:Z1000 " .
2025-06-06 12:50:32 -04:00
2025-06-06 12:45:15 -04:00
Returns:
2025-06-06 17:32:09 -04:00
str: The formatted values from the specified range.
2025-06-06 12:45:15 -04:00
"""
2025-06-07 10:21:12 -04:00
logger . info ( f " [read_sheet_values] Invoked. Email: ' { user_google_email } ' , Spreadsheet: { spreadsheet_id } , Range: { range_name } " )
2025-06-06 12:45:15 -04:00
2025-06-18 16:29:35 -04:00
result = await asyncio . to_thread (
service . spreadsheets ( )
. values ( )
. get ( spreadsheetId = spreadsheet_id , range = range_name )
. execute
)
2025-06-06 12:45:15 -04:00
2025-06-18 16:29:35 -04:00
values = result . get ( " values " , [ ] )
if not values :
return f " No data found in range ' { range_name } ' for { user_google_email } . "
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
# Format the output as a readable table
formatted_rows = [ ]
for i , row in enumerate ( values , 1 ) :
# Pad row with empty strings to show structure
padded_row = row + [ " " ] * max ( 0 , len ( values [ 0 ] ) - len ( row ) ) if values else row
formatted_rows . append ( f " Row { i : 2d } : { padded_row } " )
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
text_output = (
f " Successfully read { len ( values ) } rows from range ' { range_name } ' in spreadsheet { spreadsheet_id } for { user_google_email } : \n "
+ " \n " . join ( formatted_rows [ : 50 ] ) # Limit to first 50 rows for readability
+ ( f " \n ... and { len ( values ) - 50 } more rows " if len ( values ) > 50 else " " )
)
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
logger . info ( f " Successfully read { len ( values ) } rows for { user_google_email } . " )
return text_output
2025-06-06 12:45:15 -04:00
@server.tool ( )
2025-07-28 11:49:01 -04:00
@handle_http_errors ( " modify_sheet_values " , service_type = " sheets " )
2025-07-17 13:57:21 -04:00
@require_google_service ( " sheets " , " sheets_write " )
2025-06-06 12:45:15 -04:00
async def modify_sheet_values (
2025-06-07 10:21:12 -04:00
service ,
2025-06-06 12:45:15 -04:00
user_google_email : str ,
spreadsheet_id : str ,
range_name : str ,
2025-08-10 12:00:36 +03:00
values : Optional [ Union [ str , List [ List [ str ] ] ] ] = None ,
2025-06-06 12:45:15 -04:00
value_input_option : str = " USER_ENTERED " ,
clear_values : bool = False ,
2025-06-06 17:32:09 -04:00
) - > str :
2025-06-06 12:45:15 -04:00
"""
Modifies values in a specific range of a Google Sheet - can write, update, or clear values.
2025-06-06 12:50:32 -04:00
2025-06-06 12:45:15 -04:00
Args:
user_google_email (str): The user ' s Google email address. Required.
spreadsheet_id (str): The ID of the spreadsheet. Required.
range_name (str): The range to modify (e.g., " Sheet1!A1:D10 " , " A1:D10 " ). Required.
2025-08-10 12:00:36 +03:00
values (Optional[Union[str, List[List[str]]]]): 2D array of values to write/update. Can be a JSON string or Python list. Required unless clear_values=True.
2025-06-06 12:45:15 -04:00
value_input_option (str): How to interpret input values ( " RAW " or " USER_ENTERED " ). Defaults to " USER_ENTERED " .
clear_values (bool): If True, clears the range instead of writing values. Defaults to False.
2025-06-06 12:50:32 -04:00
2025-06-06 12:45:15 -04:00
Returns:
2025-06-06 17:32:09 -04:00
str: Confirmation message of the successful modification operation.
2025-06-06 12:45:15 -04:00
"""
operation = " clear " if clear_values else " write "
2025-06-07 10:21:12 -04:00
logger . info ( f " [modify_sheet_values] Invoked. Operation: { operation } , Email: ' { user_google_email } ' , Spreadsheet: { spreadsheet_id } , Range: { range_name } " )
2025-06-06 12:45:15 -04:00
2025-08-10 12:00:36 +03:00
# Parse values if it's a JSON string (MCP passes parameters as JSON strings)
if values is not None and isinstance ( values , str ) :
try :
parsed_values = json . loads ( values )
if not isinstance ( parsed_values , list ) :
raise ValueError ( f " Values must be a list, got { type ( parsed_values ) . __name__ } " )
# Validate it's a list of lists
for i , row in enumerate ( parsed_values ) :
if not isinstance ( row , list ) :
raise ValueError ( f " Row { i } must be a list, got { type ( row ) . __name__ } " )
values = parsed_values
logger . info ( f " [modify_sheet_values] Parsed JSON string to Python list with { len ( values ) } rows " )
except json . JSONDecodeError as e :
raise Exception ( f " Invalid JSON format for values: { e } " )
except ValueError as e :
raise Exception ( f " Invalid values structure: { e } " )
2025-06-06 12:45:15 -04:00
if not clear_values and not values :
2025-06-06 17:32:09 -04:00
raise Exception ( " Either ' values ' must be provided or ' clear_values ' must be True. " )
2025-06-06 12:45:15 -04:00
2025-06-18 16:29:35 -04:00
if clear_values :
result = await asyncio . to_thread (
service . spreadsheets ( )
. values ( )
. clear ( spreadsheetId = spreadsheet_id , range = range_name )
. execute
)
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
cleared_range = result . get ( " clearedRange " , range_name )
text_output = f " Successfully cleared range ' { cleared_range } ' in spreadsheet { spreadsheet_id } for { user_google_email } . "
logger . info ( f " Successfully cleared range ' { cleared_range } ' for { user_google_email } . " )
else :
body = { " values " : values }
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
result = await asyncio . to_thread (
service . spreadsheets ( )
. values ( )
. update (
spreadsheetId = spreadsheet_id ,
range = range_name ,
valueInputOption = value_input_option ,
body = body ,
2025-06-06 12:45:15 -04:00
)
2025-06-18 16:29:35 -04:00
. execute
)
updated_cells = result . get ( " updatedCells " , 0 )
updated_rows = result . get ( " updatedRows " , 0 )
updated_columns = result . get ( " updatedColumns " , 0 )
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
text_output = (
f " Successfully updated range ' { range_name } ' in spreadsheet { spreadsheet_id } for { user_google_email } . "
f " Updated: { updated_cells } cells, { updated_rows } rows, { updated_columns } columns. "
)
logger . info ( f " Successfully updated { updated_cells } cells for { user_google_email } . " )
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
return text_output
2025-06-06 12:45:15 -04:00
@server.tool ( )
2025-07-28 11:49:01 -04:00
@handle_http_errors ( " create_spreadsheet " , service_type = " sheets " )
2025-07-17 13:57:21 -04:00
@require_google_service ( " sheets " , " sheets_write " )
2025-06-06 12:45:15 -04:00
async def create_spreadsheet (
2025-06-07 10:21:12 -04:00
service ,
2025-06-06 12:45:15 -04:00
user_google_email : str ,
title : str ,
sheet_names : Optional [ List [ str ] ] = None ,
2025-06-06 17:32:09 -04:00
) - > str :
2025-06-06 12:45:15 -04:00
"""
Creates a new Google Spreadsheet.
2025-06-06 12:50:32 -04:00
2025-06-06 12:45:15 -04:00
Args:
user_google_email (str): The user ' s Google email address. Required.
title (str): The title of the new spreadsheet. Required.
sheet_names (Optional[List[str]]): List of sheet names to create. If not provided, creates one sheet with default name.
2025-06-06 12:50:32 -04:00
2025-06-06 12:45:15 -04:00
Returns:
2025-06-06 17:32:09 -04:00
str: Information about the newly created spreadsheet including ID and URL.
2025-06-06 12:45:15 -04:00
"""
2025-06-07 10:21:12 -04:00
logger . info ( f " [create_spreadsheet] Invoked. Email: ' { user_google_email } ' , Title: { title } " )
2025-06-06 12:45:15 -04:00
2025-06-18 16:29:35 -04:00
spreadsheet_body = {
" properties " : {
" title " : title
2025-06-06 12:45:15 -04:00
}
2025-06-18 16:29:35 -04:00
}
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
if sheet_names :
spreadsheet_body [ " sheets " ] = [
{ " properties " : { " title " : sheet_name } } for sheet_name in sheet_names
]
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
spreadsheet = await asyncio . to_thread (
service . spreadsheets ( ) . create ( body = spreadsheet_body ) . execute
)
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
spreadsheet_id = spreadsheet . get ( " spreadsheetId " )
spreadsheet_url = spreadsheet . get ( " spreadsheetUrl " )
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
text_output = (
f " Successfully created spreadsheet ' { title } ' for { user_google_email } . "
f " ID: { spreadsheet_id } | URL: { spreadsheet_url } "
)
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
logger . info ( f " Successfully created spreadsheet for { user_google_email } . ID: { spreadsheet_id } " )
return text_output
2025-06-06 12:45:15 -04:00
@server.tool ( )
2025-07-28 11:49:01 -04:00
@handle_http_errors ( " create_sheet " , service_type = " sheets " )
2025-07-17 13:57:21 -04:00
@require_google_service ( " sheets " , " sheets_write " )
2025-06-06 12:45:15 -04:00
async def create_sheet (
2025-06-07 10:21:12 -04:00
service ,
2025-06-06 12:45:15 -04:00
user_google_email : str ,
spreadsheet_id : str ,
sheet_name : str ,
2025-06-06 17:32:09 -04:00
) - > str :
2025-06-06 12:45:15 -04:00
"""
Creates a new sheet within an existing spreadsheet.
2025-06-06 12:50:32 -04:00
2025-06-06 12:45:15 -04:00
Args:
user_google_email (str): The user ' s Google email address. Required.
spreadsheet_id (str): The ID of the spreadsheet. Required.
sheet_name (str): The name of the new sheet. Required.
2025-06-06 12:50:32 -04:00
2025-06-06 12:45:15 -04:00
Returns:
2025-06-06 17:32:09 -04:00
str: Confirmation message of the successful sheet creation.
2025-06-06 12:45:15 -04:00
"""
2025-06-07 10:21:12 -04:00
logger . info ( f " [create_sheet] Invoked. Email: ' { user_google_email } ' , Spreadsheet: { spreadsheet_id } , Sheet: { sheet_name } " )
2025-06-06 12:45:15 -04:00
2025-06-18 16:29:35 -04:00
request_body = {
" requests " : [
{
" addSheet " : {
" properties " : {
" title " : sheet_name
2025-06-06 12:45:15 -04:00
}
}
2025-06-18 16:29:35 -04:00
}
]
}
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
response = await asyncio . to_thread (
service . spreadsheets ( )
. batchUpdate ( spreadsheetId = spreadsheet_id , body = request_body )
. execute
)
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
sheet_id = response [ " replies " ] [ 0 ] [ " addSheet " ] [ " properties " ] [ " sheetId " ]
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
text_output = (
f " Successfully created sheet ' { sheet_name } ' (ID: { sheet_id } ) in spreadsheet { spreadsheet_id } for { user_google_email } . "
)
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
logger . info ( f " Successfully created sheet for { user_google_email } . Sheet ID: { sheet_id } " )
return text_output
2025-06-06 12:45:15 -04:00
2025-07-01 17:14:30 -07:00
# Create comment management tools for sheets
2025-07-01 18:56:53 -07:00
_comment_tools = create_comment_tools ( " spreadsheet " , " spreadsheet_id " )
2025-07-03 19:37:45 -04:00
# Extract and register the functions
2025-07-01 17:14:30 -07:00
read_sheet_comments = _comment_tools [ ' read_comments ' ]
2025-07-17 13:57:21 -04:00
create_sheet_comment = _comment_tools [ ' create_comment ' ]
2025-07-01 17:14:30 -07:00
reply_to_sheet_comment = _comment_tools [ ' reply_to_comment ' ]
resolve_sheet_comment = _comment_tools [ ' resolve_comment ' ]