Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ You're ready! Start issuing commands via your MCP client.
## ✨ Key Features

* **Seamless Integration:** Connects directly to Google Drive & Google Sheets APIs.
* **Comprehensive Tools:** Offers a wide range of operations (CRUD, listing, batching, sharing, formatting, etc.).
* **Comprehensive Tools:** Offers a wide range of operations (CRUD, listing, batching, sharing, cell formatting, etc.).
* **Cell Formatting:** Apply number formats, colors, text styles, and alignment to spreadsheet cells.
* **Flexible Authentication:** Supports **Service Accounts (recommended)**, OAuth 2.0, and direct credential injection via environment variables.
* **Easy Deployment:** Run instantly with `uvx` (zero-install feel) or clone for development using `uv`.
* **AI-Ready:** Designed for use with MCP-compatible clients, enabling natural language spreadsheet interaction.
Expand Down Expand Up @@ -148,6 +149,15 @@ This server exposes the following tools for interacting with Google Sheets:
* `recipients` (array of objects): `[{email_address: '[email protected]', role: 'writer'}, ...]`. Roles: `reader`, `commenter`, `writer`.
* `send_notification` (optional boolean, default True): Send email notifications.
* _Returns:_ Dictionary with `successes` and `failures` lists.
* **`format_cells`**: Apply formatting to cells in a Google Spreadsheet.
* `spreadsheet_id` (string)
* `sheet` (string): Name of the sheet.
* `range` (string): Cell range in A1 notation (e.g., `'A1:C10'` or `'E17'`).
* `number_format` (optional object): Number format with `type` and `pattern` keys. Example: `{'type': 'CURRENCY', 'pattern': '$#,##0.00'}`. Common types: `NUMBER`, `CURRENCY`, `PERCENT`, `DATE`, `TIME`, `TEXT`.
* `background_color` (optional object): Background color with `red`, `green`, `blue` keys (0-1 range). Example: `{'red': 1, 'green': 0.647, 'blue': 0}` for orange.
* `text_format` (optional object): Text format with keys like `bold`, `italic`, `fontSize`, `foregroundColor`, etc. Example: `{'bold': True, 'fontSize': 11}`.
* `horizontal_alignment` (optional string): One of: `LEFT`, `CENTER`, `RIGHT`.
* _Returns:_ Format operation result object.
* **`add_columns`**: Adds columns to a sheet. *(Verify parameters if implemented)*
* **`copy_sheet`**: Duplicates a sheet within a spreadsheet. *(Verify parameters if implemented)*
* **`rename_sheet`**: Renames an existing sheet. *(Verify parameters if implemented)*
Expand Down Expand Up @@ -458,6 +468,9 @@ Once connected, try prompts like:
* "Append these rows to the 'Log' sheet in spreadsheet `XYZ`: `[['2024-07-31', 'Task A Completed'], ['2024-08-01', 'Task B Started']]`"
* "Get a summary of the spreadsheets 'Sales Data' and 'Inventory Count'."
* "Share the 'Team Vacation Schedule' spreadsheet with `[email protected]` as a reader and `[email protected]` as a writer. Don't send notifications."
* "Format cells A1:E1 in Sheet1 with orange background, bold black text."
* "Apply currency formatting to cells E2:E10 in the 'Sales' sheet."
* "Center align the text in cells B1:D1 and make them bold."

---

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ source = "uv-dynamic-versioning"

[tool.uv-dynamic-versioning]
pattern = "default"
strict = true
strict = false
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change from strict = true to strict = false appears unrelated to the cell formatting feature. Consider removing this change or documenting the reason for it in the PR description. Disabling strict mode for dynamic versioning can allow version tag mismatches to pass silently.

Suggested change
strict = false
strict = true

Copilot uses AI. Check for mistakes.

[project.scripts]
mcp-google-sheets = "mcp_google_sheets:main"
122 changes: 122 additions & 0 deletions src/mcp_google_sheets/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,128 @@ def share_spreadsheet(spreadsheet_id: str,

return {"successes": successes, "failures": failures}


@mcp.tool()
def format_cells(spreadsheet_id: str,
sheet: str,
range: str,
number_format: Optional[Dict[str, str]] = None,
background_color: Optional[Dict[str, float]] = None,
text_format: Optional[Dict[str, Any]] = None,
horizontal_alignment: Optional[str] = None,
ctx: Context = None) -> Dict[str, Any]:
"""
Apply formatting to cells in a Google Spreadsheet.

Args:
spreadsheet_id: The ID of the spreadsheet (found in the URL)
sheet: The name of the sheet
range: Cell range in A1 notation (e.g., 'A1:C10' or 'E17')
number_format: Optional number format with 'type' and 'pattern' keys.
Example: {'type': 'NUMBER', 'pattern': '$#,##0.00'} for currency
Common types: 'NUMBER', 'CURRENCY', 'PERCENT', 'DATE', 'TIME', 'TEXT'
background_color: Optional background color with 'red', 'green', 'blue' keys (0-1 range).
Example: {'red': 1, 'green': 1, 'blue': 1} for white
text_format: Optional text format with keys like 'bold', 'italic', 'fontSize', etc.
Example: {'bold': True, 'fontSize': 11}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider providing more specific documentation for the text_format parameter. For example, mention that foregroundColor should be an object with red, green, blue keys (0-1 range), similar to background_color. Example: {'bold': True, 'fontSize': 11, 'foregroundColor': {'red': 0, 'green': 0, 'blue': 0}}

Suggested change
Example: {'bold': True, 'fontSize': 11}
You can specify text color using the 'foregroundColor' key, which should be an object with 'red', 'green', 'blue' keys (0-1 range), similar to background_color.
Example: {'bold': True, 'fontSize': 11, 'foregroundColor': {'red': 0, 'green': 0, 'blue': 0}} for black text.

Copilot uses AI. Check for mistakes.
horizontal_alignment: Optional horizontal alignment. One of: 'LEFT', 'CENTER', 'RIGHT'

Returns:
Result of the format operation
"""
sheets_service = ctx.request_context.lifespan_context.sheets_service

# Get sheet ID
spreadsheet = sheets_service.spreadsheets().get(spreadsheetId=spreadsheet_id).execute()
sheet_id = None

for s in spreadsheet['sheets']:
if s['properties']['title'] == sheet:
sheet_id = s['properties']['sheetId']
break

if sheet_id is None:
return {"error": f"Sheet '{sheet}' not found"}

# Parse A1 notation to get row/column indices
# Simple parser for ranges like 'A1', 'A1:B2', 'E17', etc.
import re
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import re statement should be moved to the top of the file with other imports (around line 1-25). Local imports within functions are generally discouraged unless there's a specific reason (e.g., optional dependencies, circular imports).

Copilot uses AI. Check for mistakes.
match = re.match(r'([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?', range)
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern doesn't account for lowercase column letters. Google Sheets A1 notation can be case-insensitive in practice. Consider using r'([A-Za-z]+)(\d+)(?::([A-Za-z]+)(\d+))?' and converting to uppercase, or add the re.IGNORECASE flag.

Copilot uses AI. Check for mistakes.
if not match:
return {"error": f"Invalid range format: {range}"}

def col_to_index(col: str) -> int:
"""Convert column letter to 0-based index"""
result = 0
for char in col:
result = result * 26 + (ord(char) - ord('A') + 1)
return result - 1

start_col = col_to_index(match.group(1))
start_row = int(match.group(2)) - 1

if match.group(3) and match.group(4):
end_col = col_to_index(match.group(3)) + 1
end_row = int(match.group(4))
else:
end_col = start_col + 1
end_row = start_row + 1
Comment on lines +974 to +996
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider extracting the A1 notation parsing logic (lines 974-996) into a separate module-level helper function. This would make it reusable for other tools that might need to parse A1 ranges in the future and easier to test independently.

Copilot uses AI. Check for mistakes.

# Build the cell format
cell_format = {}
fields = []

if number_format:
cell_format['numberFormat'] = number_format
fields.append('userEnteredFormat.numberFormat')

if background_color:
cell_format['backgroundColor'] = background_color
cell_format['backgroundColorStyle'] = {'rgbColor': background_color}
fields.append('userEnteredFormat.backgroundColor')
fields.append('userEnteredFormat.backgroundColorStyle')

if text_format:
cell_format['textFormat'] = text_format
fields.append('userEnteredFormat.textFormat')

if horizontal_alignment:
cell_format['horizontalAlignment'] = horizontal_alignment
fields.append('userEnteredFormat.horizontalAlignment')

if not fields:
return {"error": "No format options provided"}

# Prepare the format request
request_body = {
"requests": [
{
"repeatCell": {
"range": {
"sheetId": sheet_id,
"startRowIndex": start_row,
"endRowIndex": end_row,
"startColumnIndex": start_col,
"endColumnIndex": end_col
},
"cell": {
"userEnteredFormat": cell_format
},
"fields": ','.join(fields)
}
}
]
}

# Execute the request
result = sheets_service.spreadsheets().batchUpdate(
spreadsheetId=spreadsheet_id,
body=request_body
).execute()

return result

def main():
# Run the server
print("Starting Google Sheets MCP server...")
mcp.run()