Skip to content

Remove stale README mention of the retired workspace worker - #973

Closed
tuongbeo wants to merge 127 commits into
taylorwilsdon:mainfrom
tuongbeo:claude/office-mcp-reauth-bug-c7ef27
Closed

Remove stale README mention of the retired workspace worker#973
tuongbeo wants to merge 127 commits into
taylorwilsdon:mainfrom
tuongbeo:claude/office-mcp-reauth-bug-c7ef27

Conversation

@tuongbeo

@tuongbeo tuongbeo commented Jul 27, 2026

Copy link
Copy Markdown

Description

office is now the only deployed Cloudflare Worker for this project — the
legacy workspace worker's code was already removed in 384cc2f. The root
README still described a "broader workspace worker covering Gmail,
Calendar, Chat, Contacts, and Custom Search" that no longer exists in the
codebase, which would mislead anyone reading it about what's actually
deployed. This corrects that section to reflect reality.

(This also closes out a related investigation from this session: a reported
"Office MCP shows Connected but Claude says it needs re-auth" bug, traced to
the workspaceoffice KV token-namespace rename leaving pre-rename
users' tokens unreachable. A namespace-fallback fix was drafted and verified
with tests, then deliberately reverted at the user's request once they
confirmed no users are still stuck on the old namespace — so this PR only
carries the doc cleanup, no behavioral change.)

Type of Change

  • Documentation update

Testing

  • New and existing unit tests pass locally with my changes (npm test — 174 passed)
  • tsc --noEmit clean

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • My changes generate no new warnings
  • I have enabled "Allow edits from maintainers" for this pull request

Additional Notes

Doc-only change — no source/runtime code touched.

Summary by CodeRabbit

  • New Features

    • Introduced Workspace Lens, a hosted MCP service for connecting Claude.ai with Google Workspace.
    • Added tools for Gmail, Drive, Calendar, Docs, Sheets, Slides, Forms, Tasks, Contacts, Chat, Apps Script, and web search.
    • Added tenant-aware OAuth sign-in and secure token refresh.
    • Added Markdown-based document and spreadsheet creation with themes, formatting, charts, images, and rich content.
    • Added a public landing site with setup guidance, pricing, privacy, and terms pages.
  • Documentation

    • Reworked project documentation for hosted deployment and security guidance.
  • Chores

    • Added automated validation and dependency update workflows.

tuongbeo and others added 30 commits March 14, 2026 04:21
…am (~101 tools)

- Gmail: 10 tools (batch, labels CRUD, attachments)
- Calendar: 8 tools (RSVP, recurrence, color)
- Drive: 14 tools (content read, share/permissions, batch share)
- Docs: 12 tools (modify, find-replace, structure, comments)
- Sheets: 8 tools (format, create sheet tab, list)
- Slides: 5 tools (batch update, delete slide)
- Chat: 4 tools (search messages)
- Tasks: 9 tools (subtasks, move, clear completed, task lists CRUD)
- Forms: 5 tools (create form, batch update, get response)
- Contacts: 12 tools (search, update, delete, groups CRUD)
- Apps Script: 11 tools (full project management + execution)
- Custom Search: 3 tools (PSE integration)
- types.ts: add GOOGLE_PSE_API_KEY / ENGINE_ID env vars
- Split docs_sheets.ts into docs.ts + sheets.ts for clarity
Gmail: +get_gmail_threads_content_batch, +list_gmail_filters, +manage_gmail_filter
Drive: +get_drive_file_download_url, +check_drive_file_public_access
Docs: +search_docs, +list_docs_in_folder, +update_doc_headers_footers,
      +create_table_with_data, +debug_table_structure,
      +insert_doc_tab, +delete_doc_tab, +update_doc_tab
Sheets: +manage_conditional_formatting
Slides: +get_slide_page, +get_slide_thumbnail
Chat: +create_chat_reaction, +download_chat_attachment
Tasks: +get_task_list
Forms: +set_form_publish_settings
Contacts: +manage_contacts_batch
Apps Script: +create_script_version, +get_script_version, +list_script_versions,
             +delete_script_project, +get_script_metrics
- fix insert_doc_tab: wrong API insertTab → addDocumentTab, add icon_emoji param
- fix update_doc_tab: wrong API updateTabProperties → updateDocumentTabProperties, add icon_emoji support, proper field mask
- fix get_google_doc: add includeTabsContent support, tab_id/include_all_tabs params, render @person elements as @name
- new tool get_doc_tabs: list all tabs with full hierarchy (ID, title, emoji, level, parent)
- new tool get_doc_tab_content: read text content of a specific tab by tabId
- new tool insert_person_mention: insert @mention smart chip by email + display name, supports index/tab_id/at_end
- new tool insert_multiple_mentions: batch insert multiple mentions in one batchUpdate call
… build

- hono@4.12.8
- @modelcontextprotocol/sdk@1.27.1
- zod@3.25.76
- wrangler@4.73.0 (devDep)
- @cloudflare/workers-types@4.20260313.1 (devDep)

Cloudflare Workers build requires package.json present in worker directory
to resolve npm dependencies during wrangler deploy.
Google Docs API addDocumentTab only accepts { tabProperties: { title, iconEmoji?, parentTabId? } }
No other fields allowed at the addDocumentTab level.
Removing insertion_index param from insert_doc_tab tool schema as well.
New file: cloudflare-worker/src/tools/slides.ts

Slide management:
- duplicate_slide: duplicate an existing slide
- reorder_slides: move slides to a new position
- update_slide_background: set background solid color

Speaker notes:
- get_slide_notes: read speaker notes from a slide
- set_slide_notes: replace speaker notes on a slide

Text & shapes:
- add_text_to_slide: insert text box with custom position/size
- delete_page_element: delete any shape/image/table element
- update_shape_position: move and/or resize an element (EMU)
- replace_all_text: find-and-replace across entire presentation
- update_text_style: bold/italic/underline/font/color on text range
- update_paragraph_alignment: START/CENTER/END/JUSTIFIED

Images:
- insert_image: insert image from URL with position/size
- replace_all_shapes_with_image: template-style image replacement

Tables:
- create_table: create table with rows/columns on a slide
- update_table_cell_text: set text in a specific cell
- insert_table_rows: add rows above/below reference row
- delete_table_row: delete a row by index
- insert_table_columns: add columns left/right of reference column
- delete_table_column: delete a column by index
- update_table_cell_style: background color and border styling

Total tools: ~130 → ~148
…index isolation docs

Priority 1 — get_doc_tabs: redesign output from human text → structured JSON
  - Returns flat array with tab_id, title, index, nested_level, parent_tab_id,
    icon_emoji, child_count fields — directly usable by AI agents
  - Uses includeTabsContent=false (metadata only, faster response)
  - Includes document_title, document_id, tab_count in envelope

Priority 2 — append_to_google_doc: add tab_id parameter
  - Uses endOfSegmentLocation (correct API approach — no endIndex computation)
  - Backward compatible: omitting tab_id appends to default/first tab
  inspect_doc_structure: add tab_id parameter
  - Fetches tab body content independently, labels output with tab context

Priority 3 — Content isolation model documented as code comments:
  - Each tab has INDEPENDENT index space starting at 1
  - endOfSegmentLocation is safest append method (avoids manual endIndex)
  - includeTabsContent=true required to read tab body content
  - Parent tab deletion cascades to all child tabs
  - First tab (index 0) cannot be deleted if it is the only tab
…or real smart chips

Root cause of workaround:
  - endOfSegmentLocation is unreliable for insertPerson (API inconsistency)
  - No support for inserting context text around chip in one call
  - insert_multiple_mentions required manual index calculation

Fixes:
  1. Always use location: { index } for insertPerson — remove endOfSegmentLocation
  2. Auto-detect safe insertion index when not provided (fetch doc end index)
  3. Add prefix_text / suffix_text params — wraps chip in one atomic batchUpdate
     e.g. prefix='Assigned to: ' suffix=' please review' → 1 tool call, 3 API ops
  4. insert_multiple_mentions: auto-detects base index, tracks running offset
     per insertion so indices stay correct across the batch
  5. Each mention in batch gets its own line (prepends \n automatically)

Result: No more 2-step workaround. Real smart chips in all scenarios.
- Support per-connector Google OAuth Client ID/Secret
  - /register: accept client_id/client_secret from Claude.ai, store in DCR record
  - /authorize: resolve googleClientId from DCR record (priority) > env var
  - /callback: defer Google token exchange, only store google_code temporarily
  - /token: perform Google token exchange using client_secret from request body

- Fix refresh token stability (prevent permission reset)
  - Store google_client_id/google_client_secret in KV token record for self-refresh
  - Only delete KV on permanent errors (invalid_grant/invalid_client), not transient 5xx
  - getValidAccessToken: credentials from token record, not from function params

- Extend DCR TTL from 7 days to 90 days
- Rename worker to google-workspace in wrangler.jsonc
- GOOGLE_OAUTH_CLIENT_ID/SECRET env vars now optional (fallback only)
- Add withErrorHandler wrapper for uniform error handling across all 164 tools
- Add textResult() helper utility
- McpServer singleton pattern for performance (getOrCreateServer)
- Add slidesRequest() export to google.ts (remove inline duplicate)
- Add Cache-Control + CORS headers to /.well-known/* endpoints
- Add MCP annotations (readOnlyHint, destructiveHint) to all tools
- Add wrangler.qts.jsonc for qts-google-workspace worker

Phase 5 - Docs Advanced (docs-advanced.ts, 11 tools):
  create_named_range, list_named_ranges, delete_named_range
  insert_footnote, insert_inline_image
  update_document_style, update_named_style
  get_doc_suggestions, accept_suggestion, reject_suggestion
  get_doc_metadata

Phase 6 - Drive Version Control (drive-revisions.ts, 6 tools):
  list_drive_revisions, get_drive_revision, update_drive_revision
  delete_drive_revision, download_drive_revision, pin_latest_revision

Total: 148 -> 164 tools
Gmail:
- extractBody: HTML-only email fallback (strip tags to plain text)
- get_gmail_message_content: expose In-Reply-To and References headers
- create_gmail_draft: reply threading via In-Reply-To/References,
  quoted original content with signature above quoted block,
  HTML body support, bcc param

Calendar:
- get_calendar_events: extract meeting URL from conferenceData + hangoutLink
- create_calendar_event: add visibility param (default/public/private/confidential)
- update_calendar_event: add visibility param

Docs:
- apply_doc_text_style: bold/italic/underline/strikethrough/fontSize/color
  with tab_id support for multi-tab docs
- create_bullet_list: convert paragraphs to bullet/numbered list

Sheets:
- read_sheet_values: include_notes param — fetches cell notes via
  includeGridData instead of values endpoint

Chat:
- get_chat_messages: add filter + order_by params (API filter strings)
- search_chat_messages: add createTime range filters + space filter

Forms:
- get_form: expand schema with quiz settings, linked sheet ID,
  full item type breakdown (CHOICE options, SCALE range, PAGE_BREAK, etc.)

Drive:
- transfer_drive_ownership: new tool to transfer file ownership
- drive.ts: fix import_to_google_doc - upgrade text/markdown → text/html upload
  with full markdown→HTML converter (H1-H6, bold, italic, lists, tables,
  code blocks, blockquotes, links, horizontal rules)
  Design tokens from Anthropic DOCX skill: Arial font, H1=16pt #111827,
  H2=14pt, H3=12pt, body=11pt, table border #D1D5DB, header-bg #F3F4F6
  Add input_format param: markdown|html|plain

- sheets.ts: add create_formatted_spreadsheet tool
  One-call pipeline: create + write + format
  Design tokens from Anthropic XLSX skill:
    - 4 color themes (blue/green/gray/orange) with dark headers + alt rows
    - Financial number formats: currency $#,##0;($#,##0);"-", 0.0%, 0.0x
    - Font: Arial 11pt, borders #D1D5DB, frozen row, auto-resize columns

- workspace.ts: add create_presentation_from_outline tool
  Design tokens from Anthropic PPTX skill (SKILL.md + pptxgenjs.md):
    - 16:9 layout = 9144000×5143500 EMU
    - 7 color palettes: midnight_executive, ocean_gradient, forest_moss,
      coral_energy, charcoal_minimal, teal_trust, warm_terracotta
    - 4 font pairings: Arial Black/Arial, Georgia/Calibri,
      Calibri/Calibri Light, Trebuchet MS/Calibri
    - Typography scale: cover=40pt, section=36pt, title=26pt, body=16pt,
      bullet=15pt, stat=60pt (from PPTX skill: title 36-44pt, body 14-16pt)
    - 6 slide types: cover, section, bullets, two_column, big_number, quote
    - Sandwich structure: dark cover/section, light content slides
    - No accent lines under titles (PPTX skill rule)
BUG-001 CRITICAL fix (index.ts): Add WWW-Authenticate header to invalid_token 401
  - RFC 6750 requires WWW-Authenticate on ALL 401 responses
  - Without it, Claude.ai cannot trigger re-authentication flow
  - Added error=invalid_token + resource_metadata_url to the header

BUG-002 HIGH fix (index.ts): Handle OPTIONS CORS preflight before auth check
  - OPTIONS /mcp returned 401, blocking browser-based MCP clients
  - Added app.options('/mcp') returning 204 with full CORS headers
  - Exposes: Mcp-Session-Id, Access-Control-Allow-Headers, max-age=86400

BUG-003 HIGH fix (drive.ts): markdownToHtml underscore italic regex corrupts URLs
  - Original regex (?<![*_])_(.+?)_(?![*_]) matched underscores inside URLs
  - https://x.com/my_file_namehttps://x.com/my<em>file</em>name (CORRUPT)
  - Fix: boundary-aware pattern (^|\s)_word_(?=\s|$) — only standalone _word_
  - Confirmed: 4 test cases pass (URL safe, snake_case safe, intended italic works)

BUG-004 MEDIUM fix (workspace.ts): uid() collision risk in tight loops
  - Date.now() returns same ms in tight loops; 3-char random = 46656 combinations
  - Confirmed collision at i=168 in 200 same-ms calls test
  - Fix: module-level counter _uidC + 8-char random (eliminates collisions)

BUG-005 MEDIUM fix (slides.ts): update_shape_position uses RELATIVE instead of ABSOLUTE
  - RELATIVE mode ADDS x/y to existing position instead of SETTING it
  - Users expect set_position(x=300) to place at 300, not +300 from current
  - Fix: applyMode 'RELATIVE' → 'ABSOLUTE' in updatePageElementTransform

BUG-006 LOW fix (mcp-agent.ts): Stale tool count comments
  - registerSheetsTools: 8 → 9 tools (create_formatted_spreadsheet added)
  - registerSlidesTools: 5 → 6 tools (create_presentation_from_outline added)
  - Total: ~164 → ~167 tools

Also re-applied visual output improvements from previous deploy:
  - drive.ts: markdownToHtml converter + updated import_to_google_doc
  - sheets.ts: create_formatted_spreadsheet (Anthropic XLSX design tokens)
  - workspace.ts: create_presentation_from_outline (Anthropic PPTX design tokens)

Version deployed: 62b57999-34bb-4200-ae1e-d54912de11d0
…KV vs TOKENS_KV)

ROOT CAUSE of 'Authorization with the MCP server failed':
  oauth.ts  line 319: storeTokens(sub, ..., env.OAUTH_KV)  ← wrote here
  mcp-agent.ts line 104: getValidAccessToken(sub, env.TOKENS_KV)  ← read from here

Two different KV namespaces:
  OAUTH_KV  = e51d896daf1f46019c04627a38beac8b (state, auth codes, DCR records)
  TOKENS_KV = f57a70d271024b669640803bd4a9a2a1 (Google access/refresh tokens)

Timeline:
  1. User authenticates with Google → /callback → /token succeeds
  2. storeTokens writes token: to OAUTH_KV  ← BUG
  3. Claude.ai calls /mcp with proxy JWT
  4. getValidAccessToken looks for token: in TOKENS_KV → NOT FOUND
  5. Throws 'No token found for sub=...' → 401 → 'Authorization failed'

Fix: env.OAUTH_KV → env.TOKENS_KV in handleToken's storeTokens call

Also added missing OAuth scopes:
  + gmail.settings.basic  (required: list_gmail_filters, manage_gmail_filter)
  + script.projects       (required: all Apps Script tools)
  + script.runs
  + script.metrics
  + script.deployments

Deployed: version 81af4f77-3f11-4230-9d1f-f9f964d496ab
…oken

Root cause of persistent auth failures:
  The 'deferred exchange' design assumed Claude.ai would send Google OAuth
  client_secret in /token. But Claude.ai sends the UUID client_secret from
  DCR response, not the user's real Google OAuth secret.
  Result: Google exchange always failed with invalid_client.

New flow (simplified):
  /callback → exchange with Google immediately (has all credentials from DCR)
            → store proxy JWT as pending_jwt:{tempCode} in OAUTH_KV (2min TTL)
            → redirect Claude.ai with tempCode
  /token    → read pending_jwt:{tempCode} → delete → return JWT
            (no more Google exchange in /token)

Changes:
  - oauth.ts: rewrote /callback and /token
  - oauth.ts: credentials resolved from DCR record at /callback time
  - oauth.ts: removed AuthCodeRecord (no longer needed)
  - oauth.ts: added console.log at each step for debugging
  - oauth.ts: error pages now show meaningful debug info
  - types.ts: added TOKENS_KV and CONFIG_KV to Env interface

Deployed: 6ed5f58a-f351-417c-a813-53efded0ad90
Critical: TOKENS_KV was missing from wrangler.jsonc bindings.
At runtime env.TOKENS_KV = undefined → storeTokens threw TypeError
→ 500 Internal Server Error on /callback.

Added:
  TOKENS_KV  f57a70d271024b669640803bd4a9a2a1  (Google access/refresh tokens)
  CONFIG_KV  78cc72f93f34486dab1ff1a3fd5ecd81  (config/flags)

Also improved /callback error page with setup instructions.
- handleCallback: wrap entire body in try/catch to prevent 500
  Any unexpected error now returns a readable HTML page with details
- pending_jwt TTL: 120s → 300s (5 min buffer for Claude.ai /token call)
…omments/suggestions/revisions/deployments/versions/contact-groups
Phạm Mạnh Tường and others added 27 commits June 3, 2026 18:20
- Navbar, Footer: Workspace MCP → Workspace Lens
- LandingPage: new hero title + subtitle with MCP server description
- PrivacyPage, TermsPage: update service name reference
- favicon.svg: new logo (rounded rect + lens iris + gradient)
- index.html: title, description, font → Plus Jakarta Sans
- index.css: brand colors #3B82F6 blue + #10B981 green, updated surface/border tones
favicon.svg + Navbar SVG now respond to OS color scheme:
  dark  → #0f172a bg, #3B82F6/#10B981 strokes
  light → #f8fafc bg, #2563eb/#059669 strokes (darker for contrast)
Add check_drive_file_public_access to SKIP_DRIVE because it reads permissions on arbitrary files and requires the drive.readonly scope. Also add list_script_projects to SKIP_APPSSCRIPT with a comment explaining it queries the Drive API and, under drive.file scope, only returns scripts created by this app (leading to misleading empty results). Includes clarifying comments for why these tools are skipped.
A large multi-phase pass over the cloudflare-worker service and its
supporting docs, covering four areas:

Bug fixes (across tools/*.ts):
- write_google_doc append mode inserted new content at doc start
  instead of the end (docs.ts)
- drive.ts called driveRequest() 24 times without importing it — the
  worker was failing to type-check entirely
- Chat: search filter for space_name was malformed (missing `space =`
  comparator); get_chat_messages double-flipped already-ordered results
- Forms: progressBar sent as a wrapped object instead of a boolean;
  is_quiz changes were dropped from the batchUpdate updateMask
- Tasks: update_task left a stale `completed` timestamp when moving a
  task back to needsAction
- Gmail: quoted-reply HTML wasn't escaped, so `<`/`&` in the original
  message could corrupt the draft
- Contacts: batch update used truthy checks instead of `!== undefined`,
  so clearing a field with "" silently no-opped
- Apps Script: deployment updates PUT a partial config, wiping
  unspecified fields (version/description) on every update
- Slides: duplicate_slide accepted insertion_index but never applied it
- search.ts: item.link could be undefined and rendered as the literal
  string "undefined" (caught while adding types)
- docs-engine: nested markdown list items were silently dropped by the
  parser, and the builder's style-cursor drifted out of sync whenever
  an item had subItems

Tech debt / infra:
- Added CI (type-check + test) on every PR; fixed the empty
  dependabot.yml stub; added a deploy:office script
- Bumped agents, @modelcontextprotocol/sdk, and
  @cloudflare/workers-oauth-provider, resolving a duplicate-SDK install
  that was breaking type-checking on the two McpAgent entry points
- Wired token namespace through env.TOKEN_NAMESPACE instead of a
  hardcoded per-agent string; /health now reports a namespace_mismatch
  if the wrangler config and the deployed agent ever disagree
- Deleted tools/composite.ts (confirmed via git history it was dead
  code left over from before write_google_doc existed, not unfinished
  work) and its two no-op call sites
- Rewrote README.md, which had described a different (Python/self-host)
  architecture than what's actually in this repo; added
  cloudflare-worker/README.md documenting the two-worker topology and
  delegated-OAuth flow; fixed SECURITY.md's stale contact/versioning
  info and updated contact email across README/SECURITY/landing pages

Test coverage:
- Added ~85 tests covering google-tokens.ts (refresh/retry/backoff,
  lock contention, the 5-minute stale-token grace window),
  auth/google.ts and workers/shared.ts (delegated OAuth, multi-tenant
  routing, including a cross-tenant replay-attack regression test),
  google.ts, index.ts, and the docs-engine nested-list fix
- npm run test:coverage now passes; it was failing outright before
  (0% coverage on 6 files with configured thresholds)

Type safety:
- Added google-api-types.ts with minimal per-service response
  interfaces (not full API schemas) and replaced ~76 blind `as any`
  casts across chat/calendar/forms/tasks/search/gmail/contacts/slides
  tools with them

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fix: bug sweep, tech debt remediation, test coverage, and type safety
Addresses every finding from a full security/correctness audit of
cloudflare-worker/src, from critical injection/RCE issues down to
maintainability cleanup.

Critical:
- Fix Drive query injection (list_drive_files/search_drive_files q= building)
- Fix SSRF in create_drive_file's source_url (new assertSafeExternalUrl guard)
- Add explicit destructive/warning annotations to Apps Script write+run tools,
  since script execution runs with the full authority of the authorizing
  account, not this MCP's own OAuth scopes
- Fix Sheets formula injection — defuse leading =+-@ in all USER_ENTERED
  writes; escape quotes in IMAGE() formula construction
- Fix nested markdown list parsing (was silently corrupting/dropping content)
  and the matching cursor-desync bug in the Docs builder's style pass

High:
- Fix write_google_doc "append" mode inserting at index 1 instead of the end
- Fix path/parameter injection across Gmail/Chat/Contacts/Tasks tools
  (encodeURIComponent or strict resource-name validation)
- Fix Gmail header injection (CRLF stripped from to/cc/bcc/subject)
- Reject unvalidated redirect_uri in OAuth dynamic client registration
- Cap Sheets/Slides/Docs input arrays to prevent batchUpdate/memory blowup
- Validate google-auth's `sub` response shape before using it as a KV key

Medium/Low:
- Mitigate token-refresh TOCTOU race; stop returning known-dead tokens during
  the grace window; don't mass-delete tokens on invalid_client
- Mask PII (email/sub) in logs
- get_drive_shareable_link no longer silently makes files public
- Parallelize N+1 batch loops (Gmail, Contacts, Drive)
- Fix PH_LEN off-by-one and dropped isVi locale format in the doc/sheet engines
- Fix CSV parser breaking on embedded newlines inside quoted fields
- Remove dead composite.ts (no-op tool registration, ~300 unused lines)
- Add fetch timeouts to all outbound Google/auth-service calls
- Add URL scheme validation for Docs/Slides hyperlinks and images
- Implement previously-dead conditional_rules/indent_rows sheet options

Also fixes several pre-existing issues surfaced by `tsc --noEmit` while
verifying these changes (confirmed via git stash that they predate this
work): drive.ts was missing its `driveRequest` import entirely (every Drive
tool call would throw ReferenceError at runtime), OAuthProps was missing the
index signature McpAgent's generic constraint requires, completeAuthorization
was missing the now-required `metadata` field, and workers/shared.ts's
WorkerConfig.agent.serve return type didn't match apiHandler's expected shape.

Verified with `tsc --noEmit` (clean), `vitest run` (102/102 passing), and
`wrangler deploy --dry-run` (builds successfully).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rebasing onto origin/main surfaced that the "bug sweep, tech debt
remediation" PR merged in the meantime touched nearly the same files —
several bugs (missing driveRequest import, the nested-list parser bug,
the append-mode index-1 bug, the isVi/PH_LEN issues) were independently
found and fixed the same way in both. Resolved all conflicts by keeping
both sets of improvements: upstream's typed API responses/as-any removal
plus this branch's security fixes (encodeURIComponent, resource-name
validation, formula defusing, etc.) on the same lines.

Also:
- Add missing `kind`/`nextPageToken` fields to the GTask/GTaskListResponse
  types introduced upstream, needed by this branch's pagination and
  update_task field-stripping logic.
- Relax isValidGoogleSub to just check for a non-empty, colon-free string
  instead of requiring Google's usual numeric format — the upstream
  google-auth service is independently versioned, and existing test
  fixtures (and any future non-numeric identifier format) shouldn't be
  rejected by an assumption that was stricter than the actual bug required.
- Update a google-tokens.test.ts case that asserted the old (buggy) grace-
  window behavior — a permanent invalid_grant failure now correctly
  propagates instead of returning a token already known to be dead.

Verified with `tsc --noEmit` (clean), `vitest run` (180/180 passing, no
unhandled rejections), and `wrangler deploy --dry-run` (builds successfully).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…view

fix: security and correctness fixes from full cloudflare-worker review
…CP annotation defaults

Cross-referenced ~100 non-dependabot upstream PRs (taylorwilsdon/google_workspace_mcp
main) against this TypeScript rewrite to find behavior the port had silently
dropped since diverging from Python upstream.

Security:
- Escape the `error` query param before it's interpolated into the OAuth
  callback error page HTML (reflected XSS in workers/shared.ts).

Correctness bugs:
- contacts.ts: manage_contact/manage_contacts_batch accepted only a single
  email/phone string, so updating a contact silently overwrote and dropped
  any other emails/phones it already had. email/phone now accept a string
  or an array.
- slides.ts: text inside grouped shapes (elementGroup.children) was never
  read, so get_presentation/get_slide_page/get_slide_notes silently dropped
  it. Added a recursive extractElementsText helper.
- docs.ts: list_document_comments fetched a single page with no way to
  reach further comments. Added page_token param + nextPageToken passthrough.
- gmail.ts: batch/thread/single message reads never surfaced attachment
  metadata (filename/mimeType/attachment_id), making attachments effectively
  invisible without already knowing their IDs.
- calendar.ts: send_updates wasn't wired into event creation (only
  update/delete), and get_calendar_event was missing the Creator field.

New tools (Sheets):
- duplicate_sheet — duplicate a sheet tab via the Sheets API's duplicateSheet
  request, preserving data/formulas/formatting.
- manage_sheet_rows — insert/delete/move rows or columns (dimension-based),
  filling a gap only reachable before via the raw batch_update_spreadsheet
  escape hatch.

Cross-cutting MCP tool annotations:
- 37 of 193 tools registered with no ToolAnnotations at all, which per the
  MCP spec defaults destructiveHint to true for any non-read-only tool —
  mislabeling plain reads (get_slide_notes, export_doc_to_pdf, ...) as
  destructive writes, and ordinary writes (add_document_comment,
  send_chat_message, ...) as destructive.
- Added utils/tool-annotations.ts::withSmartDefaults, a wrapper around
  McpServer.tool() applied once at each server's construction (mcp-worker.ts,
  office-agent.ts) that fills in title/readOnlyHint/destructiveHint/
  idempotentHint/openWorldHint for any tool that doesn't set them, without
  touching all 193 call sites.
- Explicitly annotated the 20 tools where the wrapper's safe default
  (destructiveHint: false) would otherwise be wrong: 8 reads that were
  missing readOnlyHint, and 8 delete/remove/replace-destructively tools that
  need destructiveHint: true called out explicitly rather than relying on
  spec-default behavior.

Verified: tsc --noEmit clean, 183/183 vitest tests pass (3 new, covering
withSmartDefaults), wrangler deploy --dry-run succeeds for both the
mcp-google-workspace and mcp-office workers.
…and-tooling

Fix XSS + data-loss bugs from upstream audit, add Sheets tools and MCP annotation defaults
Cloudflare KV rejects expirationTtl values below 60, so the 30s
refresh lock in getValidAccessToken() failed every write with
"KV PUT failed: 400 Invalid expiration_ttl of 30." This blocked
token refresh entirely, breaking every Office MCP write tool
(write_sheet_values, create_drive_file, etc.) once the access
token neared expiry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fix: raise refresh-lock KV TTL to Cloudflare's 60s minimum
…ailure

Google's token endpoint returns unauthorized_client when the client_id/secret
used for a refresh call doesn't match the client the refresh_token was issued
to. refreshWithRetry only special-cased invalid_grant and invalid_client, so
this error fell into the generic transient-error branch: it retried 3 times
with backoff for nothing, and getValidAccessToken's isPermanent check didn't
recognize the resulting message, so a refresh failing this way within the
5-minute stale-token grace window would silently hand back the old access
token instead of surfacing the real problem.

Traced from a live "refresh failed (401): unauthorized_client" report; the
actual credential mismatch turned out to be in the separate google-auth
service (delegate.ts never persists which client_id/secret a token was
issued under), but this repo's error handling should surface any such
permanent, client-config-level rejection clearly rather than mask it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…resh

fix: treat unauthorized_client as a permanent token-refresh failure
office.lens.io.vn was failing every token refresh with unauthorized_client:
getValidAccessToken() fell back to this worker's own GOOGLE_OAUTH_CLIENT_ID
when refreshing, but the stored TOKENS_KV record was minted by google-auth
under a different Google OAuth client (Client A1) — a client Google never
issued that refresh_token to.

google-auth now exposes POST /delegate/refresh (companion PR in the
google-auth repo) and is the only place holding Google client secrets.
getValidAccessToken() now calls that endpoint instead of talking to Google
directly, switching on a structured {error: ok|reauth_required|config_error
|transient|not_found} response instead of parsing Google's raw error
strings. Removed as dead weight now that refresh is centralized: the local
refresh lock (locking moved server-side), the unused storeTokens() function,
and the google_client_id/secret fields from StoredTokenRecord/makeGetCreds.

GOOGLE_OAUTH_CLIENT_ID/SECRET wrangler secrets are deliberately left
provisioned (unused) as an instant-rollback path for now. Also genericized
doc comments that listed plan/social as example sub-workers — neither is
registered/active or has a wrangler config in this repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Refresh Google tokens via google-auth's centralized /delegate/refresh
Renames this deployment's identity to keep wrangler.jsonc, the KV
token namespace, and the OAuth delegating handler's server name
consistent with each other. The defaultHandler's serverName must
match TOKEN_NAMESPACE, since it's the identifier this worker is
registered under in google-auth's mcp_servers table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rename Cloudflare worker deployment from workspace to office
wrangler.jsonc pointed at the already-decommissioned "workspace" Cloudflare
script and had drifted to a conflicting `name: "office"` — deploying it would
have clobbered office's Durable Objects. Its GoogleWorkspaceAgent entry point
(src/index.ts, src/mcp-worker.ts) has no live deployment left to serve, so it
and its config/tests are removed along with the now-dead GOOGLE_OAUTH_CLIENT_ID/
SECRET and GW_SERVER bindings in Env — refreshing is centralized in google-auth's
/delegate/refresh, and the office worker no longer holds a Google OAuth client
secret at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Retire legacy workspace worker and unused OAuth fallback secrets
The workspace worker's code was already deleted in 384cc2f; office is
the only deployed worker now, so the README should say that instead of
describing a worker that no longer exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bca2f157-980e-48e0-83ff-dddce9447d2d

📥 Commits

Reviewing files that changed from the base of the PR and between 59b5342 and d804576.

⛔ Files ignored due to path filters (10)
  • cloudflare-worker/coverage/lcov-report/favicon.png is excluded by !**/*.png
  • cloudflare-worker/coverage/lcov-report/sort-arrow-sprite.png is excluded by !**/*.png
  • cloudflare-worker/package-lock.json is excluded by !**/package-lock.json
  • landing/package-lock.json is excluded by !**/package-lock.json
  • landing/public/favicon.svg is excluded by !**/*.svg
  • landing/public/icons.svg is excluded by !**/*.svg
  • landing/src/assets/hero.png is excluded by !**/*.png
  • landing/src/assets/react.svg is excluded by !**/*.svg
  • landing/src/assets/vite.svg is excluded by !**/*.svg
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (210)
  • .dockerignore
  • .dxtignore
  • .env.oauth21
  • .github/dependabot.yml
  • .github/workflows/ci.yml
  • .github/workflows/docker-publish.yml
  • .github/workflows/publish-mcp-registry.yml
  • .github/workflows/ruff.yml
  • .gitignore
  • .python-version
  • Dockerfile
  • README.md
  • README_NEW.md
  • SECURITY.md
  • auth/__init__.py
  • auth/auth_info_middleware.py
  • auth/credential_store.py
  • auth/external_oauth_provider.py
  • auth/google_auth.py
  • auth/mcp_session_middleware.py
  • auth/oauth21_session_store.py
  • auth/oauth_callback_server.py
  • auth/oauth_config.py
  • auth/oauth_responses.py
  • auth/oauth_types.py
  • auth/permissions.py
  • auth/scopes.py
  • auth/service_decorator.py
  • cloudflare-worker/.gitignore
  • cloudflare-worker/.npmrc
  • cloudflare-worker/README.md
  • cloudflare-worker/coverage/lcov-report/base.css
  • cloudflare-worker/coverage/lcov-report/block-navigation.js
  • cloudflare-worker/coverage/lcov-report/index.html
  • cloudflare-worker/coverage/lcov-report/index.ts.html
  • cloudflare-worker/coverage/lcov-report/jwt.ts.html
  • cloudflare-worker/coverage/lcov-report/mcp-agent.ts.html
  • cloudflare-worker/coverage/lcov-report/oauth.ts.html
  • cloudflare-worker/coverage/lcov-report/prettify.css
  • cloudflare-worker/coverage/lcov-report/prettify.js
  • cloudflare-worker/coverage/lcov-report/sorter.js
  • cloudflare-worker/coverage/lcov.info
  • cloudflare-worker/package.json
  • cloudflare-worker/src/__tests__/auth-google.test.ts
  • cloudflare-worker/src/__tests__/docs-engine-lists.test.ts
  • cloudflare-worker/src/__tests__/google-tokens.test.ts
  • cloudflare-worker/src/__tests__/google.test.ts
  • cloudflare-worker/src/__tests__/tool-annotations.test.ts
  • cloudflare-worker/src/__tests__/workers-shared.test.ts
  • cloudflare-worker/src/auth/google.ts
  • cloudflare-worker/src/docs-engine/builder.ts
  • cloudflare-worker/src/docs-engine/executor.ts
  • cloudflare-worker/src/docs-engine/index.ts
  • cloudflare-worker/src/docs-engine/parser.ts
  • cloudflare-worker/src/docs-engine/types.ts
  • cloudflare-worker/src/google-tokens.ts
  • cloudflare-worker/src/google.ts
  • cloudflare-worker/src/sheets-engine/.keep
  • cloudflare-worker/src/sheets-engine/chipRuns.ts
  • cloudflare-worker/src/sheets-engine/executor.ts
  • cloudflare-worker/src/sheets-engine/parser.ts
  • cloudflare-worker/src/sheets-engine/types.ts
  • cloudflare-worker/src/styles/__tests__/contrast.test.ts
  • cloudflare-worker/src/styles/contrast-utils.ts
  • cloudflare-worker/src/styles/derive-doc.ts
  • cloudflare-worker/src/styles/derive-sheet.ts
  • cloudflare-worker/src/styles/derive-slide.ts
  • cloudflare-worker/src/styles/font-pairs.ts
  • cloudflare-worker/src/styles/index.ts
  • cloudflare-worker/src/styles/key-colors.ts
  • cloudflare-worker/src/tools/appsscript.ts
  • cloudflare-worker/src/tools/calendar.ts
  • cloudflare-worker/src/tools/chat.ts
  • cloudflare-worker/src/tools/contacts.ts
  • cloudflare-worker/src/tools/docs.ts
  • cloudflare-worker/src/tools/drive.ts
  • cloudflare-worker/src/tools/forms.ts
  • cloudflare-worker/src/tools/gmail.ts
  • cloudflare-worker/src/tools/google-api-types.ts
  • cloudflare-worker/src/tools/search.ts
  • cloudflare-worker/src/tools/sheets.ts
  • cloudflare-worker/src/tools/slides.ts
  • cloudflare-worker/src/tools/tasks.ts
  • cloudflare-worker/src/types.ts
  • cloudflare-worker/src/utils/tool-annotations.ts
  • cloudflare-worker/src/utils/tool-handler.ts
  • cloudflare-worker/src/workers/agents/office-agent.ts
  • cloudflare-worker/src/workers/office.ts
  • cloudflare-worker/src/workers/shared.ts
  • cloudflare-worker/tsconfig.json
  • cloudflare-worker/vitest.config.ts
  • cloudflare-worker/wrangler.office.jsonc
  • core/__init__.py
  • core/api_enablement.py
  • core/attachment_storage.py
  • core/cli_handler.py
  • core/comments.py
  • core/config.py
  • core/context.py
  • core/log_formatter.py
  • core/server.py
  • core/tool_registry.py
  • core/tool_tier_loader.py
  • core/tool_tiers.yaml
  • core/utils.py
  • docker-compose.yml
  • fastmcp.json
  • fastmcp_server.py
  • gappsscript/README.md
  • gappsscript/TESTING.md
  • gappsscript/apps_script_tools.py
  • gcalendar/__init__.py
  • gcalendar/calendar_tools.py
  • gchat/__init__.py
  • gchat/chat_tools.py
  • gcontacts/__init__.py
  • gcontacts/contacts_tools.py
  • gdocs/__init__.py
  • gdocs/docs_helpers.py
  • gdocs/docs_markdown.py
  • gdocs/docs_structure.py
  • gdocs/docs_tables.py
  • gdocs/docs_tools.py
  • gdocs/managers/__init__.py
  • gdocs/managers/batch_operation_manager.py
  • gdocs/managers/header_footer_manager.py
  • gdocs/managers/table_operation_manager.py
  • gdocs/managers/validation_manager.py
  • gdrive/__init__.py
  • gdrive/drive_helpers.py
  • gdrive/drive_tools.py
  • gforms/__init__.py
  • gforms/forms_tools.py
  • glama.json
  • gmail/__init__.py
  • gmail/gmail_tools.py
  • google_workspace_mcp.dxt
  • gsearch/__init__.py
  • gsearch/search_tools.py
  • gsheets/__init__.py
  • gsheets/sheets_helpers.py
  • gsheets/sheets_tools.py
  • gslides/__init__.py
  • gslides/slides_tools.py
  • gtasks/__init__.py
  • gtasks/tasks_tools.py
  • helm-chart/workspace-mcp/Chart.yaml
  • helm-chart/workspace-mcp/README.md
  • helm-chart/workspace-mcp/templates/NOTES.txt
  • helm-chart/workspace-mcp/templates/_helpers.tpl
  • helm-chart/workspace-mcp/templates/configmap.yaml
  • helm-chart/workspace-mcp/templates/deployment.yaml
  • helm-chart/workspace-mcp/templates/hpa.yaml
  • helm-chart/workspace-mcp/templates/ingress.yaml
  • helm-chart/workspace-mcp/templates/poddisruptionbudget.yaml
  • helm-chart/workspace-mcp/templates/secret.yaml
  • helm-chart/workspace-mcp/templates/service.yaml
  • helm-chart/workspace-mcp/templates/serviceaccount.yaml
  • helm-chart/workspace-mcp/values.yaml
  • landing/.gitignore
  • landing/.node-version
  • landing/README.md
  • landing/index.html
  • landing/package.json
  • landing/public/_redirects
  • landing/src/App.css
  • landing/src/App.tsx
  • landing/src/LandingPage.tsx
  • landing/src/PrivacyPage.tsx
  • landing/src/TermsPage.tsx
  • landing/src/components.tsx
  • landing/src/index.css
  • landing/src/main.tsx
  • landing/tsconfig.app.json
  • landing/tsconfig.json
  • landing/tsconfig.node.json
  • landing/vite.config.ts
  • main.py
  • manifest.json
  • pyproject.toml
  • server.json
  • smithery.yaml
  • tests/__init__.py
  • tests/auth/test_google_auth_pkce.py
  • tests/core/__init__.py
  • tests/core/test_attachment_route.py
  • tests/core/test_comments.py
  • tests/core/test_well_known_cache_control_middleware.py
  • tests/gappsscript/__init__.py
  • tests/gappsscript/manual_test.py
  • tests/gappsscript/test_apps_script_tools.py
  • tests/gchat/__init__.py
  • tests/gchat/test_chat_tools.py
  • tests/gcontacts/__init__.py
  • tests/gcontacts/test_contacts_tools.py
  • tests/gdocs/__init__.py
  • tests/gdocs/test_docs_markdown.py
  • tests/gdocs/test_paragraph_style.py
  • tests/gdrive/__init__.py
  • tests/gdrive/test_create_drive_folder.py
  • tests/gdrive/test_drive_tools.py
  • tests/gdrive/test_ssrf_protections.py
  • tests/gforms/__init__.py
  • tests/gforms/test_forms_tools.py
  • tests/gmail/test_attachment_fix.py
  • tests/gsheets/__init__.py
  • tests/gsheets/test_format_sheet_range.py
  • tests/test_main_permissions_tier.py
  • tests/test_permissions.py
  • tests/test_scopes.py

📝 Walkthrough

Walkthrough

The repository transitions from a Python MCP server to a TypeScript Cloudflare Workers platform with delegated OAuth, Google Workspace MCP tools, Docs and Sheets generation engines, shared styling utilities, CI, coverage reporting, and a React landing site.

Changes

Workspace Lens platform

Layer / File(s) Summary
Platform configuration and runtime
.github/*, cloudflare-worker/*, README.md, SECURITY.md
Adds Worker and landing CI, Dependabot entries, Cloudflare deployment configuration, shared runtime routing, delegated OAuth, token refresh, and updated project documentation.
Google Workspace tools
cloudflare-worker/src/tools/*
Adds MCP tool registries for Google Workspace services, shared API response types, authentication helpers, validation, error handling, and tool annotations.
Docs and Sheets engines
cloudflare-worker/src/docs-engine/*, cloudflare-worker/src/sheets-engine/*
Adds Markdown-to-Docs parsing and execution passes plus structured Sheets parsing, writing, styling, formulas, chips, charts, and overlays.
Shared styling
cloudflare-worker/src/styles/*
Adds theme palettes, font pairs, document tokens, Sheets/Slides tokens, color conversion, and WCAG contrast validation.
Coverage and validation
cloudflare-worker/coverage/*, cloudflare-worker/src/__tests__/*
Adds generated coverage reports and tests for OAuth delegation, token refresh, routing, API helpers, annotations, and nested document lists.
Landing site
landing/*
Adds a React/Vite landing page with pricing, feature, privacy, terms, navigation, clipboard controls, responsive CSS, and build configuration.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant OfficeWorker
  participant GoogleAuth
  participant OAuthProvider
  participant OfficeAgent
  Client->>OfficeWorker: Start OAuth or send MCP request
  OfficeWorker->>GoogleAuth: Delegate authorization or refresh
  GoogleAuth-->>OfficeWorker: Return identity or access token
  OfficeWorker->>OAuthProvider: Complete OAuth or forward authenticated request
  OAuthProvider->>OfficeAgent: Serve MCP tools
  OfficeAgent-->>Client: Return tool response
Loading

Possibly related PRs

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch claude/office-mcp-reauth-bug-c7ef27

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tuongbeo

Copy link
Copy Markdown
Author

Opened by mistake (wrong target repo — meant to open this against a personal fork). Closing.

@tuongbeo tuongbeo closed this Jul 27, 2026
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.

1 participant