| name | gsheets |
| description | Use when the user shares a Google Sheets URL (docs.google.com/spreadsheets/d/), mentions "google sheet", "spreadsheet", or "gsheet", or asks to "extract data from" or "read from" a sheet. Provides reliable structured data extraction via CSV export instead of canvas scraping or screenshots.
|
| user-invocable | false |
| allowed-tools | ["Bash","Read","ToolSearch"] |
Google Sheets Data Extraction
Google Sheets renders on HTML canvas. DOM scraping and get_page_text on a Sheets page will NOT return cell data. Do NOT attempt it. Do NOT use screenshots for data extraction.
Instead, use the htmlembed + plain text extraction pattern. The browser's existing Google login handles authentication.
Rules
- NEVER attempt DOM scraping on Google Sheets — canvas rendering makes it useless.
- NEVER use screenshots for data extraction — use the htmlembed extraction flow. Screenshots are only acceptable for understanding visual layout or formatting.
- ALWAYS use the helper scripts — do not improvise URL construction or CSV parsing.
- ALWAYS confirm with the user before writing — reads are safe, writes need user approval.
- For large sheets, request specific ranges — avoid downloading the entire sheet when you only need a subset.
Reading Data
Step 1: Parse the URL
bash "${CLAUDE_SKILL_DIR}/scripts/parse-sheets-url.sh" "SHEETS_URL"
Returns JSON with spreadsheet_id, gid, export_url, and htmlembed_url.
To export a specific range:
bash "${CLAUDE_SKILL_DIR}/scripts/parse-sheets-url.sh" "SHEETS_URL" "A1:D10"
Step 2: Extract data via htmlembed
Do NOT do any of these — they are all proven failure modes:
- Navigate to the
export_url — Chrome downloads the file instead of displaying it
- Use
fetch() against the export URL — always times out for private sheets
- Replace
document.body — get_page_text can't read it, DOM state lost between JS calls
- Return raw cell content containing URLs — triggers Chrome MCP
[BLOCKED: Cookie/query string data]
- Encode data as base64 or hex — Chrome MCP blocks both (
[BLOCKED: Base64 encoded data] and long encoded-looking blobs)
Load Chrome tools: tabs_create_mcp, navigate, javascript_tool.
-
Create a new tab and navigate to the htmlembed_url from Step 1.
-
Use javascript_tool to extract the table, calculate optimal slice size, and return the first slice in one call:
var rows = document.querySelectorAll('table tr');
window.__rows = Array.from(rows).map(function(r) {
return Array.from(r.querySelectorAll('td, th')).map(function(c) {
var v = c.innerText
.replace(/https?:\/\/\S+/g, '[url]')
.replace(/"/g, '""');
return (v.indexOf(',') >= 0 || v.indexOf('"') >= 0 || v.indexOf('\n') >= 0)
? '"' + v + '"' : v;
}).join(',');
});
var total = window.__rows.join('\n');
if (total.length < 2800) {
'ALL:' + window.__rows.length + '\n' + total
} else {
var avg = Math.ceil(total.length / window.__rows.length);
window.__ss = Math.max(1, Math.floor(2800 / avg));
'MORE:' + window.__rows.length + ':' + window.__ss + '\n' + window.__rows.slice(0, window.__ss).join('\n')
}
URLs are replaced with [url] to avoid the Chrome MCP security filter. There is no encoding that bypasses it without hitting the ~3400 char output truncation limit.
Reading the response:
- Starts with
ALL:<count> — entire sheet fits in one call. Done, skip to Step 3.
- Starts with
MORE:<count>:<sliceSize> — first slice included. Continue reading with the auto-calculated slice size:
window.__rows.slice(window.__ss, window.__ss * 2).join('\n')
window.__rows.slice(window.__ss * 2, window.__ss * 3).join('\n')
Continue until all rows are read.
-
If htmlembed returns 0 rows, ask the user to paste the data.
If the user needs exact URLs from cells, ask them to paste the relevant cells directly — the Chrome MCP security filter makes it impossible to transport URLs programmatically.
Step 3: Parse to JSON
Concatenate all row slices into CSV text and pipe through the converter:
cat <<'CSVEOF' | bash "${CLAUDE_SKILL_DIR}/scripts/csv-to-json.sh"
<paste concatenated CSV rows here>
CSVEOF
This returns a JSON array with the header row as keys.
Reading Multiple Tabs
Each tab in a spreadsheet has its own GID. To list available tabs:
- Navigate Chrome to the spreadsheet's edit URL.
- Run JavaScript to read tab names:
Array.from(document.querySelectorAll('.docs-sheet-tab-name')).map(t => t.textContent)
- Click each tab and note the
gid= value in the URL, or export each GID individually.
Writing Data
Ask the user for confirmation before writing any data.
Do NOT do any of these — they are all proven failure modes:
- Use
navigator.clipboard.writeText() — fails with "Document is not focused" because Chrome MCP JS execution doesn't have user-gesture context
- Use
document.querySelector('input[aria-label="Name Box"]') to find the Name Box — this selector is outdated and returns null
Load Chrome tools: tabs_create_mcp, navigate, javascript_tool, computer.
Step 1: Navigate to the Sheet
Open the spreadsheet's edit URL in Chrome (not the export URL).
Step 2: Navigate to the Target Cell
Use the Name Box (#t-name-box) to jump to any cell or range:
var nb = document.getElementById('t-name-box');
nb.focus();
nb.value = 'A5';
nb.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true}));
To select a range (e.g., for overwriting a block): set nb.value = 'A5:C10'.
Step 3: Enter Data
For multiple cells (preferred): Use a synthetic paste event with TSV data. Google Sheets parses tab-separated columns and newline-separated rows from paste events:
var tsv = 'value1\tvalue2\tvalue3\nvalue4\tvalue5\tvalue6';
var dt = new DataTransfer();
dt.setData('text/plain', tsv);
var pasteEvent = new ClipboardEvent('paste', {
clipboardData: dt,
bubbles: true,
cancelable: true
});
document.activeElement.dispatchEvent(pasteEvent);
This writes an entire block of cells in one call. Build the TSV string from the data:
- Columns separated by
\t (tab)
- Rows separated by
\n (newline)
- The paste fills cells starting from the currently selected cell, expanding right and down
For single cells: Use the computer tool's type action to enter a value, then key action with Tab (move right) or Enter (move down):
computer: type "value1"
computer: key "Tab"
computer: type "value2"
computer: key "Enter"
In Google Sheets, after a Tab-initiated editing sequence, pressing Enter returns the cursor to the starting column of the sequence (not the current column).
Step 4: Verify
Re-read the sheet via htmlembed (repeat the read flow) and confirm the written data appears correctly.
Step 5: Undo on Failure
If a write goes wrong, undo via Ctrl+Z:
var el = document.activeElement;
el.dispatchEvent(new KeyboardEvent('keydown', {key: 'z', code: 'KeyZ', keyCode: 90, ctrlKey: true, bubbles: true}));
Dispatch multiple times to undo multiple operations. Each paste counts as one undo step regardless of how many cells it filled.
Write Limitations
- Text and numbers only — no formatting (bold, colors, merging cells).
- The paste approach writes plain text values. Formulas, data validation, and conditional formatting are not preserved or created.
- For very large updates (200+ cells), batch into multiple paste operations to avoid TSV strings exceeding Chrome MCP's output limits.
Quick Reference: URL Patterns
| Action | URL |
|---|
| htmlembed (use this) | https://docs.google.com/spreadsheets/d/{ID}/htmlembed/sheet?gid={GID} |
| htmlembed with range | https://docs.google.com/spreadsheets/d/{ID}/htmlembed/sheet?gid={GID}&range=A1:D10 |
| CSV export (download only) | https://docs.google.com/spreadsheets/d/{ID}/export?format=csv&gid={GID} |