Skip to content

feat(sheets): add sheets.appendRow write tool#342

Open
rohanaceres wants to merge 5 commits into
gemini-cli-extensions:mainfrom
rohanaceres:sheets_appendRow
Open

feat(sheets): add sheets.appendRow write tool#342
rohanaceres wants to merge 5 commits into
gemini-cli-extensions:mainfrom
rohanaceres:sheets_appendRow

Conversation

@rohanaceres

@rohanaceres rohanaceres commented Apr 22, 2026

Copy link
Copy Markdown

Summary

  • Adds sheets.appendRow MCP tool to append rows to a Google Sheets spreadsheet
  • Tool is gated behind the sheets write feature group (defaultEnabled: false, requires spreadsheets OAuth scope)
  • Uses USER_ENTERED value input option so formulas and dates are parsed correctly

Test Plan

  • Unit tests added for success case, URL-to-ID extraction, and error handling (SheetsService.test.ts)
  • Run npm run test && npm run lint to verify all checks pass
  • Manually verify appending a row to a spreadsheet returns correct updatedRange, updatedRows, updatedColumns, updatedCells
  • Verify tool is disabled by default and only activates when sheets write feature is enabled

Issue #341

@google-cla

google-cla Bot commented Apr 22, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new appendRow tool for Google Sheets, enabling the addition of multiple rows to a spreadsheet. The implementation includes the core service logic, tool registration with Zod validation, and unit tests. Review feedback suggests renaming the tool and its associated methods to appendRows to accurately reflect its multi-row capability and recommends broadening the input schema and TypeScript types to support numbers, booleans, and null values in addition to strings.

Comment thread workspace-server/src/features/feature-config.ts Outdated
Comment thread workspace-server/src/index.ts Outdated
Comment thread workspace-server/src/services/SheetsService.ts Outdated
Comment thread workspace-server/src/__tests__/services/SheetsService.test.ts Outdated
Implements the ability to append rows to a Google Sheets spreadsheet
via the MCP sheets.appendRow tool. The tool is gated behind the
sheets write feature group (disabled by default, scoped to
spreadsheets OAuth scope).
rohanaceres and others added 3 commits April 22, 2026 09:48
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Incorporates review feedback: plural name better reflects that multiple
rows can be appended at once, and values now accepts numbers, booleans,
and nulls in addition to strings.

@allenhutchison allenhutchison left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review summary

Clean, idiomatic addition — appendRows mirrors the sibling getRange method exactly (ID extraction via extractDocId, logToFile start/finish/error, the try/catch result shape), the feature gating is correct (sheets/write, defaultEnabled: false, spreadsheets scope), and the happy-path test asserts the full request shape including valueInputOption: 'USER_ENTERED'. Nothing here is merge-blocking. I've left inline notes on a phantom-success edge case, the isError convention, and an empty-values guard; grouping the docs and test items here.

Docs parity (please update before merge)

The repo enumerates every tool per feature group, but the new write tool isn't listed:

  • docs/index.md (Google Sheets section, ~line 42): add a bullet, e.g. - `sheets.appendRows`: Appends rows to a Google Sheets spreadsheet.
  • docs/feature-configuration.md: there's a ### sheets.read section (line 194) but no ### sheets.write section — every other write group (docs.write, drive.write, gmail.write, …) has one. The scope table (line 25) also lists only sheets | read with no sheets | write row. Both need a sheets.write entry listing sheets.appendRows.

Test coverage

The new tests cover the happy path (with full request-shape assertion), URL-to-ID extraction, and the thrown-error path — good, and consistent with the existing getRange/getMetadata style. Two gaps worth closing:

  • response.data.updates === undefined — the highest-value addition, directly exercising the phantom-success path flagged inline. Mock { data: {} } and assert the result reflects "no rows written."
  • Empty values: [] — documents whether an empty append is rejected locally (it would be, if .min(1) is added) or passed through.
  • If isError: true is added, a regression test asserting result.isError === true on the error path would lock it in (see DriveService.test.ts / DocsService.comments.test.ts for the pattern).

Minor

  • The PR title/body say sheets.appendRow (singular), but the tool is registered as sheets.appendRows (plural) everywhere in code — the code is internally consistent; just the description is off.
  • The URL-extraction test uses an unrealistically short ID (abc123); a real-style ID like 1A2b-_C3d would also guard the -/_ character class in the ID regex.

Strengths

  • Faithful to the established getRange pattern, so it reads as a natural sibling.
  • Correct off-by-default gating; the existing feature-config test still passes.
  • USER_ENTERED matches the stated intent, and the string | number | boolean | null cell union is exactly right (allows null, excludes undefined).

Review assisted by automated analysis agents; findings verified against the diff.

{
type: 'text' as const,
text: JSON.stringify({
updatedRange: response.data.updates?.updatedRange,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The four fields are read with response.data.updates?.… optional chaining. If updates is undefined — which the API can return on a 200 where nothing was written — JSON.stringify drops all four undefined fields and the tool returns {}, plus the "Finished appendRows" log line, which is indistinguishable from a real success. For a write tool, a phantom success can mean silently lost data with no signal to the caller. Suggest treating a missing updates as a failure rather than masking it:

const updates = response.data.updates;
if (!updates) {
  logToFile(`[SheetsService] appendRows returned no update metadata for: ${id}`);
  return {
    isError: true,
    content: [{ type: 'text' as const, text: JSON.stringify({
      error: 'Append returned no update information; rows may not have been written.',
    }) }],
  };
}
// then read updates.updatedRange, etc. directly (no `?.`)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — added an explicit check that returns with a descriptive error message rather than silently returning . The success path now reads the fields directly without optional chaining.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — added an explicit updates check that returns isError: true with a descriptive error message rather than silently returning {}. The success path now reads the fields directly without optional chaining. Also added a test case for this path.

},
],
};
} catch (error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This error result isn't flagged with isError: true, so the MCP client treats the call as a success and has to string-parse the JSON body to notice the error key. DriveService (handleError) and DocsService already set isError: true on failures. Note this is consistent with the other Sheets methods (getText/getRange/getMetadata all omit it), so it's a pre-existing pattern rather than something this PR introduces — but since appendRows is a write operation where a false "success" is more costly, it's worth adding here (ideally to all four in one pass):

return {
  isError: true,
  content: [{ type: 'text' as const, text: JSON.stringify({ error: errorMessage }) }],
};

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added isError: true to the catch block. Agreed it's more impactful here given this is a write operation.

'The A1 notation range to append to (e.g., "Sheet1!A1"). Data is appended after the last row with data in this range.',
),
values: z
.array(z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

values: [] currently passes validation and forwards an empty requestBody.values to the API — a wasted no-op call. A .min(1) guard rejects it at the tool boundary with a clear message: z.array(z.array(z.union([...]))).min(1). (Don't enforce rectangular rows — ragged rows are legal in Sheets, where shorter rows just leave trailing cells untouched.)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added .min(1) on the outer array. Left inner rows unconstrained as suggested — ragged rows are valid in Sheets.

@bdoyle0182

Copy link
Copy Markdown

I think we should close this one in favor of the other PR that adds the full write tools suite for sheets.

- Handle phantom success: return isError when response.data.updates is undefined
- Add isError: true to catch block for consistency with Drive/Docs services
- Add .min(1) guard on values array in index.ts to reject empty appends
- Use realistic spreadsheet ID in URL extraction test
- Add test for missing updates metadata (isError path)
- Add isError assertion to error path test
- Add sheets.appendRows to docs/index.md
- Add sheets.write section to docs/feature-configuration.md
@rohanaceres

Copy link
Copy Markdown
Author

Hi @bdoyle0182 — can you link the other PR? Happy to close this one if the full write suite covers the same ground, or rebase on top of it if there is overlap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants