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
2025-11-27 17:19:56 +01:00
import copy
2025-08-10 12:00:36 +03:00
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-11-27 17:19:56 +01:00
from core.utils import handle_http_errors , UserInputError
2025-07-01 17:14:30 -07:00
from core.comments import create_comment_tools
2025-12-12 18:55:16 +01:00
from gsheets.sheets_helpers import (
2026-02-08 18:53:47 -05:00
_a1_range_cell_count ,
2025-12-12 18:55:16 +01:00
CONDITION_TYPES ,
2025-12-15 09:57:45 +01:00
_a1_range_for_values ,
2025-12-12 18:55:16 +01:00
_build_boolean_rule ,
_build_gradient_rule ,
2025-12-15 09:57:45 +01:00
_fetch_detailed_sheet_errors ,
2026-02-08 17:41:15 -05:00
_fetch_sheet_hyperlinks ,
2025-12-12 18:55:16 +01:00
_fetch_sheets_with_rules ,
_format_conditional_rules_section ,
2026-02-08 17:41:15 -05:00
_format_sheet_hyperlink_section ,
2025-12-15 09:57:45 +01:00
_format_sheet_error_section ,
2025-12-12 18:55:16 +01:00
_parse_a1_range ,
_parse_condition_values ,
_parse_gradient_points ,
_parse_hex_color ,
_select_sheet ,
2025-12-15 09:57:45 +01:00
_values_contain_sheets_errors ,
2025-12-12 18:55:16 +01:00
)
2025-06-06 12:45:15 -04:00
# Configure module logger
logger = logging . getLogger ( __name__ )
2026-02-08 18:15:36 -05:00
MAX_HYPERLINK_FETCH_CELLS = 5000
2025-06-06 12:45:15 -04:00
2025-12-13 13:49:28 -08:00
2025-06-06 12:45:15 -04:00
@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 ,
2025-11-04 10:39:53 -05:00
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 = [
2025-12-13 13:49:28 -08:00
f '- " { file [ "name" ] } " (ID: { file [ "id" ] } ) | Modified: { file . get ( "modifiedTime" , "Unknown" ) } | Link: { file . get ( "webViewLink" , "No link" ) } '
2025-06-18 16:29:35 -04:00
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-12-13 13:49:28 -08:00
logger . info (
f "Successfully listed { len ( files ) } spreadsheets for { user_google_email } ."
)
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 ( "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-11-27 17:19:56 +01:00
str: Formatted spreadsheet information including title, locale, and sheets list.
2025-06-06 12:45:15 -04:00
"""
2025-12-13 13:49:28 -08: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 (
2025-11-27 17:19:56 +01:00
service . spreadsheets ()
. get (
spreadsheetId = spreadsheet_id ,
fields = "spreadsheetId,properties(title,locale),sheets(properties(title,sheetId,gridProperties(rowCount,columnCount)),conditionalFormats)" ,
)
. execute
2025-06-18 16:29:35 -04:00
)
2025-06-06 12:50:32 -04:00
2025-11-27 17:19:56 +01:00
properties = spreadsheet . get ( "properties" , {})
title = properties . get ( "title" , "Unknown" )
locale = properties . get ( "locale" , "Unknown" )
2025-06-18 16:29:35 -04:00
sheets = spreadsheet . get ( "sheets" , [])
2025-06-06 12:50:32 -04:00
2025-11-27 17:19:56 +01:00
sheet_titles = {}
for sheet in sheets :
sheet_props = sheet . get ( "properties" , {})
sid = sheet_props . get ( "sheetId" )
if sid is not None :
sheet_titles [ sid ] = sheet_props . get ( "title" , f "Sheet { sid } " )
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-11-27 17:19:56 +01:00
rules = sheet . get ( "conditionalFormats" , []) or []
2025-06-06 12:50:32 -04:00
2025-06-18 16:29:35 -04:00
sheets_info . append (
2025-12-13 13:49:28 -08:00
f ' - " { sheet_name } " (ID: { sheet_id } ) | Size: { rows } x { cols } | Conditional formats: { len ( rules ) } '
2025-06-06 12:45:15 -04:00
)
2025-11-27 17:19:56 +01:00
if rules :
sheets_info . append (
_format_conditional_rules_section (
sheet_name , rules , sheet_titles , indent = " "
)
)
2025-06-06 12:50:32 -04:00
2025-11-27 17:19:56 +01:00
sheets_section = " \n " . join ( sheets_info ) if sheets_info else " No sheets found"
text_output = " \n " . join (
[
2025-12-13 13:49:28 -08:00
f 'Spreadsheet: " { title } " (ID: { spreadsheet_id } ) | Locale: { locale } ' ,
2025-11-27 17:19:56 +01:00
f "Sheets ( { len ( sheets ) } ):" ,
sheets_section ,
]
2025-06-18 16:29:35 -04:00
)
2025-06-06 12:50:32 -04:00
2025-12-13 13:49:28 -08:00
logger . info (
f "Successfully retrieved info for spreadsheet { spreadsheet_id } for { user_google_email } ."
)
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 ( "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" ,
2026-02-08 18:15:36 -05:00
include_hyperlinks : bool = False ,
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".
2026-02-08 18:15:36 -05:00
include_hyperlinks (bool): If True, also fetch hyperlink metadata for the range.
Defaults to False to avoid expensive includeGridData requests.
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-12-13 13:49:28 -08: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
2026-02-08 17:41:15 -05:00
resolved_range = result . get ( "range" , range_name )
detailed_range = _a1_range_for_values ( resolved_range , values ) or resolved_range
hyperlink_section = ""
2026-02-08 18:15:36 -05:00
if include_hyperlinks :
2026-02-08 19:00:52 -05:00
# Use a tight A1 range for includeGridData fetches to avoid expensive
# open-ended requests (e.g., A:Z).
hyperlink_range = _a1_range_for_values ( resolved_range , values )
if not hyperlink_range :
2026-02-08 18:15:36 -05:00
logger . info (
2026-02-08 19:00:52 -05:00
"[read_sheet_values] Skipping hyperlink fetch for range ' %s ': unable to determine tight bounds" ,
resolved_range ,
)
else :
cell_count = _a1_range_cell_count ( hyperlink_range ) or sum (
len ( row ) for row in values
2026-02-08 18:15:36 -05:00
)
2026-02-08 19:00:52 -05:00
if cell_count <= MAX_HYPERLINK_FETCH_CELLS :
try :
hyperlinks = await _fetch_sheet_hyperlinks (
service , spreadsheet_id , hyperlink_range
)
hyperlink_section = _format_sheet_hyperlink_section (
hyperlinks = hyperlinks , range_label = hyperlink_range
)
except Exception as exc :
logger . warning (
"[read_sheet_values] Failed fetching hyperlinks for range ' %s ': %s " ,
hyperlink_range ,
exc ,
)
else :
logger . info (
"[read_sheet_values] Skipping hyperlink fetch for large range ' %s ' ( %d cells > %d limit)" ,
hyperlink_range ,
cell_count ,
MAX_HYPERLINK_FETCH_CELLS ,
)
2026-02-08 17:41:15 -05:00
2025-12-15 09:57:45 +01:00
detailed_errors_section = ""
if _values_contain_sheets_errors ( values ):
try :
errors = await _fetch_detailed_sheet_errors (
service , spreadsheet_id , detailed_range
)
detailed_errors_section = _format_sheet_error_section (
errors = errors , range_label = detailed_range
)
except Exception as exc :
logger . warning (
"[read_sheet_values] Failed fetching detailed error messages for range ' %s ': %s " ,
detailed_range ,
exc ,
)
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 } ." )
2026-02-08 17:41:15 -05:00
return text_output + hyperlink_section + detailed_errors_section
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-12-13 13:49:28 -08: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 ):
2025-12-13 13:49:28 -08:00
raise ValueError (
f "Values must be a list, got { type ( parsed_values ) . __name__ } "
)
2025-08-10 12:00:36 +03:00
# Validate it's a list of lists
for i , row in enumerate ( parsed_values ):
if not isinstance ( row , list ):
2025-12-13 13:49:28 -08:00
raise ValueError (
f "Row { i } must be a list, got { type ( row ) . __name__ } "
)
2025-08-10 12:00:36 +03:00
values = parsed_values
2025-12-13 13:49:28 -08:00
logger . info (
f "[modify_sheet_values] Parsed JSON string to Python list with { len ( values ) } rows"
)
2025-08-10 12:00:36 +03:00
except json . JSONDecodeError as e :
2025-11-27 17:19:56 +01:00
raise UserInputError ( f "Invalid JSON format for values: { e } " )
2025-08-10 12:00:36 +03:00
except ValueError as e :
2025-11-27 17:19:56 +01:00
raise UserInputError ( f "Invalid values structure: { e } " )
2025-08-10 12:00:36 +03:00
2025-06-06 12:45:15 -04:00
if not clear_values and not values :
2025-12-13 13:49:28 -08:00
raise UserInputError (
"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 } ."
2025-12-13 13:49:28 -08:00
logger . info (
f "Successfully cleared range ' { cleared_range } ' for { user_google_email } ."
)
2025-06-18 16:29:35 -04:00
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 ,
2025-12-15 09:57:45 +01:00
# NOTE: This increases response payload/shape by including `updatedData`, but lets
# us detect Sheets error tokens (e.g. "#VALUE!", "#REF!") without an extra read.
includeValuesInResponse = True ,
responseValueRenderOption = "FORMATTED_VALUE" ,
2025-06-18 16:29:35 -04:00
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-12-15 09:57:45 +01:00
detailed_errors_section = ""
updated_data = result . get ( "updatedData" ) or {}
updated_values = updated_data . get ( "values" , []) or []
if updated_values and _values_contain_sheets_errors ( updated_values ):
updated_range = result . get ( "updatedRange" , range_name )
detailed_range = (
_a1_range_for_values ( updated_range , updated_values ) or updated_range
)
try :
errors = await _fetch_detailed_sheet_errors (
service , spreadsheet_id , detailed_range
)
detailed_errors_section = _format_sheet_error_section (
errors = errors , range_label = detailed_range
)
except Exception as exc :
logger . warning (
"[modify_sheet_values] Failed fetching detailed error messages for range ' %s ': %s " ,
detailed_range ,
exc ,
)
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."
)
2025-12-15 09:57:45 +01:00
text_output += detailed_errors_section
2025-12-13 13:49:28 -08:00
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
2026-02-02 14:18:53 +00:00
# Internal implementation function for testing
async def _format_sheet_range_impl (
2025-11-27 17:19:56 +01:00
service ,
spreadsheet_id : str ,
range_name : str ,
background_color : Optional [ str ] = None ,
text_color : Optional [ str ] = None ,
number_format_type : Optional [ str ] = None ,
number_format_pattern : Optional [ str ] = None ,
2026-02-02 14:18:53 +00:00
wrap_strategy : Optional [ str ] = None ,
horizontal_alignment : Optional [ str ] = None ,
vertical_alignment : Optional [ str ] = None ,
bold : Optional [ bool ] = None ,
italic : Optional [ bool ] = None ,
font_size : Optional [ int ] = None ,
2025-11-27 17:19:56 +01:00
) -> str :
2026-02-02 14:18:53 +00:00
"""Internal implementation for format_sheet_range.
2025-11-27 17:19:56 +01:00
2026-02-02 14:18:53 +00:00
Applies formatting to a Google Sheets range including colors, number formats,
text wrapping, alignment, and text styling.
2025-11-27 17:19:56 +01:00
Args:
2026-02-02 14:18:53 +00:00
service: Google Sheets API service client.
spreadsheet_id: The ID of the spreadsheet.
range_name: A1-style range (optionally with sheet name).
background_color: Hex background color (e.g., "#FFEECC").
text_color: Hex text color (e.g., "#000000").
number_format_type: Sheets number format type (e.g., "DATE").
number_format_pattern: Optional custom pattern for the number format.
wrap_strategy: Text wrap strategy (WRAP, CLIP, OVERFLOW_CELL).
horizontal_alignment: Horizontal alignment (LEFT, CENTER, RIGHT).
vertical_alignment: Vertical alignment (TOP, MIDDLE, BOTTOM).
bold: Whether to apply bold formatting.
italic: Whether to apply italic formatting.
font_size: Font size in points.
2025-11-27 17:19:56 +01:00
Returns:
2026-02-05 14:30:57 -05:00
Dictionary with keys: range_name, spreadsheet_id, summary.
2025-11-27 17:19:56 +01:00
"""
2026-02-02 14:18:53 +00:00
# Validate at least one formatting option is provided
2026-02-04 09:16:10 +00:00
has_any_format = any (
[
background_color ,
text_color ,
number_format_type ,
wrap_strategy ,
horizontal_alignment ,
vertical_alignment ,
bold is not None ,
italic is not None ,
font_size is not None ,
]
)
2026-02-02 14:18:53 +00:00
if not has_any_format :
2025-11-27 17:19:56 +01:00
raise UserInputError (
2026-02-02 14:18:53 +00:00
"Provide at least one formatting option (background_color, text_color, "
"number_format_type, wrap_strategy, horizontal_alignment, vertical_alignment, "
"bold, italic, or font_size)."
2025-11-27 17:19:56 +01:00
)
2026-02-02 14:18:53 +00:00
# Parse colors
2025-11-27 17:19:56 +01:00
bg_color_parsed = _parse_hex_color ( background_color )
text_color_parsed = _parse_hex_color ( text_color )
2026-02-02 14:18:53 +00:00
# Validate and normalize number format
2025-11-27 17:19:56 +01:00
number_format = None
if number_format_type :
allowed_number_formats = {
"NUMBER" ,
"NUMBER_WITH_GROUPING" ,
"CURRENCY" ,
"PERCENT" ,
"SCIENTIFIC" ,
"DATE" ,
"TIME" ,
"DATE_TIME" ,
"TEXT" ,
}
normalized_type = number_format_type . upper ()
if normalized_type not in allowed_number_formats :
raise UserInputError (
f "number_format_type must be one of { sorted ( allowed_number_formats ) } ."
)
number_format = { "type" : normalized_type }
if number_format_pattern :
number_format [ "pattern" ] = number_format_pattern
2026-02-02 14:18:53 +00:00
# Validate and normalize wrap_strategy
wrap_strategy_normalized = None
if wrap_strategy :
allowed_wrap_strategies = { "WRAP" , "CLIP" , "OVERFLOW_CELL" }
wrap_strategy_normalized = wrap_strategy . upper ()
if wrap_strategy_normalized not in allowed_wrap_strategies :
raise UserInputError (
f "wrap_strategy must be one of { sorted ( allowed_wrap_strategies ) } ."
)
# Validate and normalize horizontal_alignment
h_align_normalized = None
if horizontal_alignment :
allowed_h_alignments = { "LEFT" , "CENTER" , "RIGHT" }
h_align_normalized = horizontal_alignment . upper ()
if h_align_normalized not in allowed_h_alignments :
raise UserInputError (
f "horizontal_alignment must be one of { sorted ( allowed_h_alignments ) } ."
)
# Validate and normalize vertical_alignment
v_align_normalized = None
if vertical_alignment :
allowed_v_alignments = { "TOP" , "MIDDLE" , "BOTTOM" }
v_align_normalized = vertical_alignment . upper ()
if v_align_normalized not in allowed_v_alignments :
raise UserInputError (
f "vertical_alignment must be one of { sorted ( allowed_v_alignments ) } ."
)
# Get sheet metadata for range parsing
2025-11-27 17:19:56 +01:00
metadata = await asyncio . to_thread (
service . spreadsheets ()
. get (
spreadsheetId = spreadsheet_id ,
fields = "sheets(properties(sheetId,title))" ,
)
. execute
)
sheets = metadata . get ( "sheets" , [])
grid_range = _parse_a1_range ( range_name , sheets )
2026-02-02 14:18:53 +00:00
# Build userEnteredFormat and fields list
2025-11-27 17:19:56 +01:00
user_entered_format = {}
fields = []
2026-02-02 14:18:53 +00:00
# Background color
2025-11-27 17:19:56 +01:00
if bg_color_parsed :
user_entered_format [ "backgroundColor" ] = bg_color_parsed
fields . append ( "userEnteredFormat.backgroundColor" )
2026-02-02 14:18:53 +00:00
# Text format (color, bold, italic, fontSize)
text_format = {}
text_format_fields = []
2025-11-27 17:19:56 +01:00
if text_color_parsed :
2026-02-02 14:18:53 +00:00
text_format [ "foregroundColor" ] = text_color_parsed
text_format_fields . append ( "userEnteredFormat.textFormat.foregroundColor" )
if bold is not None :
text_format [ "bold" ] = bold
text_format_fields . append ( "userEnteredFormat.textFormat.bold" )
if italic is not None :
text_format [ "italic" ] = italic
text_format_fields . append ( "userEnteredFormat.textFormat.italic" )
if font_size is not None :
text_format [ "fontSize" ] = font_size
text_format_fields . append ( "userEnteredFormat.textFormat.fontSize" )
if text_format :
user_entered_format [ "textFormat" ] = text_format
fields . extend ( text_format_fields )
# Number format
2025-11-27 17:19:56 +01:00
if number_format :
user_entered_format [ "numberFormat" ] = number_format
fields . append ( "userEnteredFormat.numberFormat" )
2026-02-02 14:18:53 +00:00
# Wrap strategy
if wrap_strategy_normalized :
user_entered_format [ "wrapStrategy" ] = wrap_strategy_normalized
fields . append ( "userEnteredFormat.wrapStrategy" )
# Horizontal alignment
if h_align_normalized :
user_entered_format [ "horizontalAlignment" ] = h_align_normalized
fields . append ( "userEnteredFormat.horizontalAlignment" )
# Vertical alignment
if v_align_normalized :
user_entered_format [ "verticalAlignment" ] = v_align_normalized
fields . append ( "userEnteredFormat.verticalAlignment" )
2025-11-27 17:19:56 +01:00
if not user_entered_format :
raise UserInputError (
2026-02-02 14:18:53 +00:00
"No formatting applied. Verify provided formatting options."
2025-11-27 17:19:56 +01:00
)
2026-02-02 14:18:53 +00:00
# Build and execute request
2025-11-27 17:19:56 +01:00
request_body = {
"requests" : [
{
"repeatCell" : {
"range" : grid_range ,
"cell" : { "userEnteredFormat" : user_entered_format },
"fields" : "," . join ( fields ),
}
}
]
}
await asyncio . to_thread (
service . spreadsheets ()
. batchUpdate ( spreadsheetId = spreadsheet_id , body = request_body )
. execute
)
2026-02-02 14:18:53 +00:00
# Build confirmation message
2025-11-27 17:19:56 +01:00
applied_parts = []
if bg_color_parsed :
2026-02-06 10:54:44 -05:00
applied_parts . append ( f "background { background_color } " )
2025-11-27 17:19:56 +01:00
if text_color_parsed :
2026-02-02 14:18:53 +00:00
applied_parts . append ( f "text color { text_color } " )
2025-11-27 17:19:56 +01:00
if number_format :
nf_desc = number_format [ "type" ]
if number_format_pattern :
nf_desc += f " (pattern: { number_format_pattern } )"
2026-02-02 14:18:53 +00:00
applied_parts . append ( f "number format { nf_desc } " )
if wrap_strategy_normalized :
applied_parts . append ( f "wrap { wrap_strategy_normalized } " )
if h_align_normalized :
applied_parts . append ( f "horizontal align { h_align_normalized } " )
if v_align_normalized :
applied_parts . append ( f "vertical align { v_align_normalized } " )
if bold is not None :
applied_parts . append ( "bold" if bold else "not bold" )
if italic is not None :
applied_parts . append ( "italic" if italic else "not italic" )
if font_size is not None :
applied_parts . append ( f "font size { font_size } " )
2025-11-27 17:19:56 +01:00
summary = ", " . join ( applied_parts )
2026-02-05 14:30:57 -05:00
# Return structured data for the wrapper to format
return {
"range_name" : range_name ,
"spreadsheet_id" : spreadsheet_id ,
2026-02-06 10:59:46 -05:00
"summary" : summary ,
2026-02-05 14:30:57 -05:00
}
2026-02-02 14:18:53 +00:00
@server.tool ()
@handle_http_errors ( "format_sheet_range" , service_type = "sheets" )
@require_google_service ( "sheets" , "sheets_write" )
async def format_sheet_range (
service ,
user_google_email : str ,
spreadsheet_id : str ,
range_name : str ,
background_color : Optional [ str ] = None ,
text_color : Optional [ str ] = None ,
number_format_type : Optional [ str ] = None ,
number_format_pattern : Optional [ str ] = None ,
wrap_strategy : Optional [ str ] = None ,
horizontal_alignment : Optional [ str ] = None ,
vertical_alignment : Optional [ str ] = None ,
bold : Optional [ bool ] = None ,
italic : Optional [ bool ] = None ,
font_size : Optional [ int ] = None ,
) -> str :
"""
Applies formatting to a range: colors, number formats, text wrapping,
alignment, and text styling.
Colors accept hex strings (#RRGGBB). Number formats follow Sheets types
(e.g., NUMBER, CURRENCY, DATE, PERCENT). If no sheet name is provided,
the first sheet is used.
Args:
user_google_email (str): The user's Google email address. Required.
spreadsheet_id (str): The ID of the spreadsheet. Required.
range_name (str): A1-style range (optionally with sheet name). Required.
background_color (Optional[str]): Hex background color (e.g., "#FFEECC").
text_color (Optional[str]): Hex text color (e.g., "#000000").
number_format_type (Optional[str]): Sheets number format type (e.g., "DATE").
number_format_pattern (Optional[str]): Custom pattern for the number format.
wrap_strategy (Optional[str]): Text wrap strategy - WRAP (wrap text within
cell), CLIP (clip text at cell boundary), or OVERFLOW_CELL (allow text
to overflow into adjacent empty cells).
horizontal_alignment (Optional[str]): Horizontal text alignment - LEFT,
CENTER, or RIGHT.
vertical_alignment (Optional[str]): Vertical text alignment - TOP, MIDDLE,
or BOTTOM.
bold (Optional[bool]): Whether to apply bold formatting.
italic (Optional[bool]): Whether to apply italic formatting.
font_size (Optional[int]): Font size in points.
Returns:
str: Confirmation of the applied formatting.
"""
logger . info (
"[format_sheet_range] Invoked. Email: ' %s ', Spreadsheet: %s , Range: %s " ,
user_google_email ,
spreadsheet_id ,
range_name ,
)
result = await _format_sheet_range_impl (
service = service ,
spreadsheet_id = spreadsheet_id ,
range_name = range_name ,
background_color = background_color ,
text_color = text_color ,
number_format_type = number_format_type ,
number_format_pattern = number_format_pattern ,
wrap_strategy = wrap_strategy ,
horizontal_alignment = horizontal_alignment ,
vertical_alignment = vertical_alignment ,
bold = bold ,
italic = italic ,
font_size = font_size ,
)
2026-02-05 14:30:57 -05:00
# Build confirmation message with user email
return (
f "Applied formatting to range ' { result [ 'range_name' ] } ' in spreadsheet "
f " { result [ 'spreadsheet_id' ] } for { user_google_email } : { result [ 'summary' ] } ."
2025-11-27 17:19:56 +01:00
)
@server.tool ()
2026-03-01 12:36:09 -05:00
@handle_http_errors ( "manage_conditional_formatting" , service_type = "sheets" )
2025-11-27 17:19:56 +01:00
@require_google_service ( "sheets" , "sheets_write" )
2026-03-01 12:36:09 -05:00
async def manage_conditional_formatting (
2025-11-27 17:19:56 +01:00
service ,
user_google_email : str ,
spreadsheet_id : str ,
2026-03-01 12:36:09 -05:00
action : str ,
range_name : Optional [ str ] = None ,
condition_type : Optional [ str ] = None ,
2025-11-27 17:19:56 +01:00
condition_values : Optional [ Union [ str , List [ Union [ str , int , float ]]]] = None ,
background_color : Optional [ str ] = None ,
text_color : Optional [ str ] = None ,
rule_index : Optional [ int ] = None ,
gradient_points : Optional [ Union [ str , List [ dict ]]] = None ,
2026-03-01 12:36:09 -05:00
sheet_name : Optional [ str ] = None ,
2025-11-27 17:19:56 +01:00
) -> str :
"""
2026-03-01 12:36:09 -05:00
Manages conditional formatting rules on a Google Sheet. Supports adding,
updating, and deleting conditional formatting rules via a single tool.
2025-11-27 17:19:56 +01:00
Args:
user_google_email (str): The user's Google email address. Required.
spreadsheet_id (str): The ID of the spreadsheet. Required.
2026-03-01 12:36:09 -05:00
action (str): The operation to perform. Must be one of "add", "update",
or "delete".
range_name (Optional[str]): A1-style range (optionally with sheet name).
Required for "add". Optional for "update" (preserves existing ranges
if omitted). Not used for "delete".
condition_type (Optional[str]): Sheets condition type (e.g., NUMBER_GREATER,
TEXT_CONTAINS, DATE_BEFORE, CUSTOM_FORMULA). Required for "add".
Optional for "update" (preserves existing type if omitted).
condition_values (Optional[Union[str, List[Union[str, int, float]]]]): Values
for the condition; accepts a list or a JSON string representing a list.
Depends on condition_type. Used by "add" and "update".
background_color (Optional[str]): Hex background color to apply when
condition matches. Used by "add" and "update".
2025-11-27 17:19:56 +01:00
text_color (Optional[str]): Hex text color to apply when condition matches.
2026-03-01 12:36:09 -05:00
Used by "add" and "update".
rule_index (Optional[int]): 0-based index of the rule. For "add", optionally
specifies insertion position. Required for "update" and "delete".
gradient_points (Optional[Union[str, List[dict]]]): List (or JSON list) of
gradient points for a color scale. If provided, a gradient rule is created
and boolean parameters are ignored. Used by "add" and "update".
sheet_name (Optional[str]): Sheet name to locate the rule when range_name is
omitted. Defaults to the first sheet. Used by "update" and "delete".
2025-11-27 17:19:56 +01:00
Returns:
2026-03-01 12:36:09 -05:00
str: Confirmation of the operation and the current rule state.
2025-11-27 17:19:56 +01:00
"""
2026-03-01 12:36:09 -05:00
allowed_actions = { "add" , "update" , "delete" }
action_normalized = action . strip () . lower ()
if action_normalized not in allowed_actions :
raise UserInputError (
f "action must be one of { sorted ( allowed_actions ) } , got ' { action } '."
)
2025-11-27 17:19:56 +01:00
logger . info (
2026-03-01 12:36:09 -05:00
"[manage_conditional_formatting] Invoked. Action: ' %s ', Email: ' %s ', Spreadsheet: %s " ,
action_normalized ,
2025-11-27 17:19:56 +01:00
user_google_email ,
spreadsheet_id ,
)
2026-03-01 12:36:09 -05:00
if action_normalized == "add" :
if not range_name :
raise UserInputError ( "range_name is required for action 'add'." )
if not condition_type and not gradient_points :
raise UserInputError (
"condition_type (or gradient_points) is required for action 'add'."
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
if rule_index is not None and (
not isinstance ( rule_index , int ) or rule_index < 0
):
raise UserInputError (
"rule_index must be a non-negative integer when provided."
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
gradient_points_list = _parse_gradient_points ( gradient_points )
2026-03-01 13:23:40 -05:00
condition_values_list = (
2026-03-01 18:26:42 +00:00
None if gradient_points_list else _parse_condition_values ( condition_values )
2026-03-01 13:23:40 -05:00
)
2025-11-27 17:19:56 +01:00
2026-03-01 17:54:47 +00:00
sheets , sheet_titles = await _fetch_sheets_with_rules ( service , spreadsheet_id )
2026-03-01 12:36:09 -05:00
grid_range = _parse_a1_range ( range_name , sheets )
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
target_sheet = None
for sheet in sheets :
2026-03-01 17:54:47 +00:00
if sheet . get ( "properties" , {}) . get ( "sheetId" ) == grid_range . get ( "sheetId" ):
2026-03-01 12:36:09 -05:00
target_sheet = sheet
break
if target_sheet is None :
raise UserInputError (
"Target sheet not found while adding conditional formatting."
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
current_rules = target_sheet . get ( "conditionalFormats" , []) or []
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
insert_at = rule_index if rule_index is not None else len ( current_rules )
if insert_at > len ( current_rules ):
raise UserInputError (
f "rule_index { insert_at } is out of range for sheet "
f "' { target_sheet . get ( 'properties' , {}) . get ( 'title' , 'Unknown' ) } ' "
f "(current count: { len ( current_rules ) } )."
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
if gradient_points_list :
new_rule = _build_gradient_rule ([ grid_range ], gradient_points_list )
rule_desc = "gradient"
values_desc = ""
applied_parts = [ f "gradient points { len ( gradient_points_list ) } " ]
else :
rule , cond_type_normalized = _build_boolean_rule (
[ grid_range ],
condition_type ,
condition_values_list ,
background_color ,
text_color ,
)
new_rule = rule
rule_desc = cond_type_normalized
values_desc = ""
if condition_values_list :
values_desc = f " with values { condition_values_list } "
applied_parts = []
if background_color :
applied_parts . append ( f "background { background_color } " )
if text_color :
applied_parts . append ( f "text { text_color } " )
new_rules_state = copy . deepcopy ( current_rules )
new_rules_state . insert ( insert_at , new_rule )
add_rule_request = { "rule" : new_rule }
if rule_index is not None :
add_rule_request [ "index" ] = rule_index
2026-03-01 17:54:47 +00:00
request_body = { "requests" : [{ "addConditionalFormatRule" : add_rule_request }]}
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
await asyncio . to_thread (
service . spreadsheets ()
. batchUpdate ( spreadsheetId = spreadsheet_id , body = request_body )
. execute
)
2025-11-27 17:19:56 +01:00
2026-03-01 17:54:47 +00:00
format_desc = ", " . join ( applied_parts ) if applied_parts else "format applied"
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
sheet_title = target_sheet . get ( "properties" , {}) . get ( "title" , "Unknown" )
state_text = _format_conditional_rules_section (
sheet_title , new_rules_state , sheet_titles , indent = ""
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
return " \n " . join (
[
f "Added conditional format on ' { range_name } ' in spreadsheet "
f " { spreadsheet_id } for { user_google_email } : "
f " { rule_desc }{ values_desc } ; format: { format_desc } ." ,
state_text ,
]
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
elif action_normalized == "update" :
if rule_index is None :
raise UserInputError ( "rule_index is required for action 'update'." )
if not isinstance ( rule_index , int ) or rule_index < 0 :
raise UserInputError ( "rule_index must be a non-negative integer." )
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
gradient_points_list = _parse_gradient_points ( gradient_points )
2026-03-01 13:23:40 -05:00
condition_values_list = (
None
if gradient_points_list is not None
else _parse_condition_values ( condition_values )
)
2025-11-27 17:19:56 +01:00
2026-03-01 17:54:47 +00:00
sheets , sheet_titles = await _fetch_sheets_with_rules ( service , spreadsheet_id )
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
target_sheet = None
grid_range = None
if range_name :
grid_range = _parse_a1_range ( range_name , sheets )
for sheet in sheets :
2026-03-01 17:54:47 +00:00
if sheet . get ( "properties" , {}) . get ( "sheetId" ) == grid_range . get (
"sheetId"
2026-03-01 12:36:09 -05:00
):
target_sheet = sheet
break
else :
target_sheet = _select_sheet ( sheets , sheet_name )
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
if target_sheet is None :
2025-11-27 17:19:56 +01:00
raise UserInputError (
2026-03-01 12:36:09 -05:00
"Target sheet not found while updating conditional formatting."
2025-11-27 17:19:56 +01:00
)
2026-03-01 12:36:09 -05:00
sheet_props = target_sheet . get ( "properties" , {})
sheet_id = sheet_props . get ( "sheetId" )
sheet_title = sheet_props . get ( "title" , f "Sheet { sheet_id } " )
rules = target_sheet . get ( "conditionalFormats" , []) or []
if rule_index >= len ( rules ):
2025-12-13 13:49:28 -08:00
raise UserInputError (
2026-03-01 12:36:09 -05:00
f "rule_index { rule_index } is out of range for sheet "
f "' { sheet_title } ' (current count: { len ( rules ) } )."
2025-12-13 13:49:28 -08:00
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
existing_rule = rules [ rule_index ]
ranges_to_use = existing_rule . get ( "ranges" , [])
if range_name :
ranges_to_use = [ grid_range ]
if not ranges_to_use :
ranges_to_use = [{ "sheetId" : sheet_id }]
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
new_rule = None
rule_desc = ""
values_desc = ""
format_desc = ""
if gradient_points_list is not None :
new_rule = _build_gradient_rule ( ranges_to_use , gradient_points_list )
rule_desc = "gradient"
format_desc = f "gradient points { len ( gradient_points_list ) } "
elif "gradientRule" in existing_rule :
if any (
[
background_color ,
text_color ,
condition_type ,
condition_values_list ,
]
):
raise UserInputError (
"Existing rule is a gradient rule. Provide gradient_points "
"to update it, or omit formatting/condition parameters to "
"keep it unchanged."
)
new_rule = {
"ranges" : ranges_to_use ,
"gradientRule" : existing_rule . get ( "gradientRule" , {}),
2025-11-27 17:19:56 +01:00
}
2026-03-01 12:36:09 -05:00
rule_desc = "gradient"
format_desc = "gradient (unchanged)"
else :
existing_boolean = existing_rule . get ( "booleanRule" , {})
existing_condition = existing_boolean . get ( "condition" , {})
2026-03-01 17:54:47 +00:00
existing_format = copy . deepcopy ( existing_boolean . get ( "format" , {}))
2025-11-27 17:19:56 +01:00
2026-03-01 17:54:47 +00:00
cond_type = ( condition_type or existing_condition . get ( "type" , "" )) . upper ()
2026-03-01 12:36:09 -05:00
if not cond_type :
2026-03-01 17:54:47 +00:00
raise UserInputError ( "condition_type is required for boolean rules." )
2026-03-01 12:36:09 -05:00
if cond_type not in CONDITION_TYPES :
raise UserInputError (
f "condition_type must be one of { sorted ( CONDITION_TYPES ) } ."
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
if condition_values_list is not None :
cond_values = [
2026-03-01 17:54:47 +00:00
{ "userEnteredValue" : str ( val )} for val in condition_values_list
2026-03-01 12:36:09 -05:00
]
else :
cond_values = existing_condition . get ( "values" )
2025-11-27 17:19:56 +01:00
2026-03-01 17:54:47 +00:00
new_format = copy . deepcopy ( existing_format ) if existing_format else {}
2026-03-01 12:36:09 -05:00
if background_color is not None :
bg_color_parsed = _parse_hex_color ( background_color )
if bg_color_parsed :
new_format [ "backgroundColor" ] = bg_color_parsed
elif "backgroundColor" in new_format :
del new_format [ "backgroundColor" ]
if text_color is not None :
text_color_parsed = _parse_hex_color ( text_color )
2026-03-01 17:54:47 +00:00
text_format = copy . deepcopy ( new_format . get ( "textFormat" , {}))
2026-03-01 12:36:09 -05:00
if text_color_parsed :
text_format [ "foregroundColor" ] = text_color_parsed
elif "foregroundColor" in text_format :
del text_format [ "foregroundColor" ]
if text_format :
new_format [ "textFormat" ] = text_format
elif "textFormat" in new_format :
del new_format [ "textFormat" ]
if not new_format :
raise UserInputError (
"At least one format option must remain on the rule."
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
new_rule = {
"ranges" : ranges_to_use ,
"booleanRule" : {
"condition" : { "type" : cond_type },
"format" : new_format ,
},
}
if cond_values :
new_rule [ "booleanRule" ][ "condition" ][ "values" ] = cond_values
rule_desc = cond_type
if condition_values_list :
values_desc = f " with values { condition_values_list } "
format_parts = []
if "backgroundColor" in new_format :
format_parts . append ( "background updated" )
if "textFormat" in new_format and new_format [ "textFormat" ] . get (
"foregroundColor"
):
format_parts . append ( "text color updated" )
format_desc = (
", " . join ( format_parts ) if format_parts else "format preserved"
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
new_rules_state = copy . deepcopy ( rules )
new_rules_state [ rule_index ] = new_rule
request_body = {
"requests" : [
{
"updateConditionalFormatRule" : {
"index" : rule_index ,
"sheetId" : sheet_id ,
"rule" : new_rule ,
}
}
]
}
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
await asyncio . to_thread (
service . spreadsheets ()
. batchUpdate ( spreadsheetId = spreadsheet_id , body = request_body )
. execute
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
state_text = _format_conditional_rules_section (
sheet_title , new_rules_state , sheet_titles , indent = ""
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
return " \n " . join (
[
f "Updated conditional format at index { rule_index } on sheet "
f "' { sheet_title } ' in spreadsheet { spreadsheet_id } "
f "for { user_google_email } : "
f " { rule_desc }{ values_desc } ; format: { format_desc } ." ,
state_text ,
]
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
else : # action_normalized == "delete"
if rule_index is None :
raise UserInputError ( "rule_index is required for action 'delete'." )
if not isinstance ( rule_index , int ) or rule_index < 0 :
raise UserInputError ( "rule_index must be a non-negative integer." )
2025-11-27 17:19:56 +01:00
2026-03-01 17:54:47 +00:00
sheets , sheet_titles = await _fetch_sheets_with_rules ( service , spreadsheet_id )
2026-03-01 12:36:09 -05:00
target_sheet = _select_sheet ( sheets , sheet_name )
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
sheet_props = target_sheet . get ( "properties" , {})
sheet_id = sheet_props . get ( "sheetId" )
target_sheet_name = sheet_props . get ( "title" , f "Sheet { sheet_id } " )
rules = target_sheet . get ( "conditionalFormats" , []) or []
if rule_index >= len ( rules ):
raise UserInputError (
f "rule_index { rule_index } is out of range for sheet "
f "' { target_sheet_name } ' (current count: { len ( rules ) } )."
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
new_rules_state = copy . deepcopy ( rules )
del new_rules_state [ rule_index ]
request_body = {
"requests" : [
{
"deleteConditionalFormatRule" : {
"index" : rule_index ,
"sheetId" : sheet_id ,
}
2025-11-27 17:19:56 +01:00
}
2026-03-01 12:36:09 -05:00
]
}
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
await asyncio . to_thread (
service . spreadsheets ()
. batchUpdate ( spreadsheetId = spreadsheet_id , body = request_body )
. execute
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
state_text = _format_conditional_rules_section (
target_sheet_name , new_rules_state , sheet_titles , indent = ""
)
2025-11-27 17:19:56 +01:00
2026-03-01 12:36:09 -05:00
return " \n " . join (
[
f "Deleted conditional format at index { rule_index } on sheet "
f "' { target_sheet_name } ' in spreadsheet { spreadsheet_id } "
f "for { user_google_email } ." ,
state_text ,
]
)
2025-11-27 17:19:56 +01:00
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-11-27 17:19:56 +01:00
str: Information about the newly created spreadsheet including ID, URL, and locale.
2025-06-06 12:45:15 -04:00
"""
2025-12-13 13:49:28 -08:00
logger . info (
f "[create_spreadsheet] Invoked. Email: ' { user_google_email } ', Title: { title } "
)
2025-06-06 12:45:15 -04:00
2025-12-13 13:49:28 -08:00
spreadsheet_body = { "properties" : { "title" : title }}
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 (
2025-11-27 17:19:56 +01:00
service . spreadsheets ()
. create (
body = spreadsheet_body ,
fields = "spreadsheetId,spreadsheetUrl,properties(title,locale)" ,
)
. execute
2025-06-18 16:29:35 -04:00
)
2025-06-06 12:50:32 -04:00
2025-11-27 17:19:56 +01:00
properties = spreadsheet . get ( "properties" , {})
2025-06-18 16:29:35 -04:00
spreadsheet_id = spreadsheet . get ( "spreadsheetId" )
spreadsheet_url = spreadsheet . get ( "spreadsheetUrl" )
2025-11-27 17:19:56 +01:00
locale = properties . get ( "locale" , "Unknown" )
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 } . "
2025-11-27 17:19:56 +01:00
f "ID: { spreadsheet_id } | URL: { spreadsheet_url } | Locale: { locale } "
2025-06-18 16:29:35 -04:00
)
2025-06-06 12:50:32 -04:00
2025-12-13 13:49:28 -08:00
logger . info (
f "Successfully created spreadsheet for { user_google_email } . ID: { spreadsheet_id } "
)
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_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-12-13 13:49:28 -08: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-12-13 13:49:28 -08:00
request_body = { "requests" : [{ "addSheet" : { "properties" : { "title" : sheet_name }}}]}
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-12-13 13:49:28 -08: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-12-13 13:49:28 -08:00
logger . info (
f "Successfully created sheet for { user_google_email } . Sheet ID: { sheet_id } "
)
2025-06-18 16:29:35 -04:00
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
2026-03-01 12:50:40 -05:00
list_spreadsheet_comments = _comment_tools [ "list_comments" ]
manage_spreadsheet_comment = _comment_tools [ "manage_comment" ]