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-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' ]