agentsclimarketplace

Google workspace mcp

Skill dipseth/google_workspace_fastmcp2/.claude/skills/google-workspace-mcp

Guide for using the Google Workspace MCP server — DSL notation, code mode, macros, and tools across 11+ Google servicesFrom its SKILL.md

Install
npx -y skills add dipseth/google_workspace_fastmcp2 --skill google-workspace-mcp

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

SKILL.md

22.6 KB, as published. Nobody here has run it

Google Workspace MCP Server

FastMCP-based Google Workspace server with DSL-driven composition for emails and chat cards, sandboxed code execution, Jinja2 macros, privacy middleware, and Qdrant-backed response history. Not all tools are active at any given time — use manage_tools(action="list") to see what's currently enabled.

Core Wrapped Tools

compose_dynamic_email — MJML Email Composer

Composes responsive HTML emails via DSL notation, then sends or saves as draft.

Parameters:

ParameterTypeDefaultDescription
email_descriptionstrrequiredDSL structure ONLY + optional subject. Text after the DSL becomes the email subject (e.g., ε[ħ, τ] Welcome aboard)
email_paramsdict/strNoneREQUIRED for content — block text/titles/URLs go here, keyed by symbol. email_description is structure ONLY. Also accepts subject and preheader keys. Supports _shared/_items merging
tostr/list"myself"Recipients: "myself", comma-separated string, or list of emails
user_google_emailstrNoneAuto-injected from OAuth session. Pass only to override (e.g., for secondary accounts)
action"send"/"draft""draft"Draft is safe default; "send" delivers immediately
cc / bccstrNoneOptional CC/BCC recipients
reply_to_message_idstrNoneThread the composed email as a reply to this Gmail message. Recipients derive from the original; the reply subject overrides the DSL subject
draft_idstrNoneUpdate an existing draft in place with the freshly rendered email instead of creating a new one (action='draft' only)

Email DSL Symbols:

SymbolBlock
εEmailSpec
ħHeroBlock
τTextBlock
ɃButtonBlock
ɨImageBlock
¢ColumnsBlock
©Column
ĦHeaderBlock
ƒFooterBlock
đDividerBlock
şSpacerBlock
ąAccordionBlock
ȼCarouselBlock
ʂSocialBlock
ƭTableBlock

DSL Syntax:

  • ε[ħ, τ] — EmailSpec with HeroBlock and TextBlock
  • ε[Ħ, ħ, τ×3, Ƀ] — Header, hero, 3 text blocks, button
  • ε[ħ, ¢[©×2], Ƀ] — Hero, 2-column layout, button
  • ε[Ħ, τ, đ, ƒ] — Header, text, divider, footer

Params Structure (_shared/_items merging):

{
  "subject": "Weekly Report",
  "τ": {
    "_shared": {"padding": "10px 25px"},
    "_items": [
      {"text": "Hello {{user://current/email.email}}"},
      {"text": "Here is your report."}
    ]
  },
  "Ƀ": {
    "_items": [
      {"text": "View Report", "url": "https://example.com/report"}
    ]
  }
}

Example — Simple email:

email_description: ε[Ħ, ħ, τ, Ƀ]
email_params: {
  "subject": "Meeting Reminder",
  "Ħ": {"_items": [{"title": "Acme Corp"}]},
  "ħ": {"_items": [{"title": "Team Standup", "subtitle": "Tomorrow at 10am"}]},
  "τ": {"_items": [{"text": "Don't forget the standup meeting."}]},
  "Ƀ": {"_items": [{"text": "Add to Calendar", "url": "https://calendar.google.com"}]}
}

Draft → patch → send workflow (iterative composition):

Email composition is rarely one-shot. Treat a draft as a canvas you refine over several turns — never accumulate duplicate drafts:

  1. Create — compose_dynamic_email(..., action="draft"). The response's draftId is the stable handle for every later edit (the draft's message id changes on each update; the draft id does not). Keep it.
  2. Patch bit by bit — change only what you need in email_params (or the DSL), then re-call with draft_id=<draftId>. The same draft is re-rendered in place. Repeat freely: tweak one block's text, swap a color theme, add a section — one call per iteration.
  3. Recover a lost draft id — search_gmail_messages(query="in:draft") returns draft_id on every draft result (including drafts created in the Gmail UI), and get_gmail_message_content includes it when the message is a draft. No draft is unreachable.
  4. Thread as a reply — add reply_to_message_id=<message id>: action="draft" creates a threaded reply draft (combine with draft_id to re-render it), action="send" sends the reply immediately. The reply subject comes from the original message; the DSL subject is ignored.
  5. Send — after review, send from the Gmail UI (safest, sends the draft itself), or re-call with action="send" (renders and sends a new message; the draft stays behind — delete it afterwards).

The plain-HTML tools follow the same pattern: draft_gmail_message and draft_gmail_reply accept the same draft_id for in-place updates, and draft_gmail_reply accepts email_spec for MJML reply drafts.

Example — patch one block of an existing draft (code mode):

drafts = await call_tool("search_gmail_messages", {"query": "in:draft", "page_size": 5})
target = drafts["messages"][0]  # newest draft; has draft_id

params = from_json(saved_params)          # the params used to create it
params["τ"]["_items"][0]["text"] = "Updated opening paragraph."

result = await call_tool("compose_dynamic_email", {
    "email_description": "ε[ħ, τx2, Ƀ]",
    "email_params": params,
    "action": "draft",
    "draft_id": target["draft_id"],
})
return result["draftId"]  # same id — edited in place

Macro-powered iteration: for an email you patch repeatedly (or a layout you reuse), store it once as a dual-mode Jinja2 macro via create_template_macro (persist_to_file=True survives restarts). Each iteration then re-invokes the macro with only the changed data instead of resending the whole params blob — the Template Middleware renders {{ ... }} in tool parameters before the tool runs:

await call_tool("compose_dynamic_email", {
    "email_description": "{{ team_update(email_symbols, mode='dsl') }}",
    "email_params": "{{ team_update(email_symbols, mode='params', headline='Week 3 recap') }}",
    "action": "draft",
    "draft_id": known_draft_id,
})

Macros can also pull live data via resource URIs (e.g. service://gmail/labels, user://current/profile) as macro arguments — see the Jinja2 Macro System section below.

For detailed component field reference: Use skill://mjml-email/ resources (15 component docs, symbols, containment rules).


send_dynamic_card — Google Chat Card Builder

Sends a card to Google Chat using DSL notation for structure control.

Parameters:

ParameterTypeDefaultDescription
card_descriptionstrrequiredDSL structure string — see Card DSL Symbols table. Supports both Structure DSL (§[δx2]) and Content DSL (δ 'text' success bold)
card_paramsdict/strNoneWidget content keyed by symbol. Supports _shared/_items merging. Takes priority over Content DSL in card_description
space_idstrNoneChat space ID. Required only for API mode (no webhook)
user_google_emailstrNoneAuto-injected from OAuth session. Pass only to override (e.g., for secondary accounts)
webhook_urlstrNoneWebhook URL. Defaults to MCP_CHAT_WEBHOOK env var
thread_keystrNoneThread key for replies

Card DSL Symbols (primary building blocks):

SymbolComponent
§Section
δDecoratedText
ĐDivider
ᵬButton
ɃButtonList
ℊGrid
ǵGridItem
◦Carousel
▼CarouselCard
ȼChipList
ℂChip
ʈTextParagraph
τTextInput
ǐImage
ɨIcon
▲SelectionInput
◙DateTimePicker
◇CardHeader
¢Columns
çColumn

DSL Syntax:

  • §[δ] — Section with one DecoratedText
  • §[δ×3, Ƀ[ᵬ×2]] — Section with 3 DecoratedTexts and 2 Buttons
  • §[ℊ[ǵ×4]] — Section with a 4-item Grid
  • §[δ, ǐ, Ƀ[ᵬ]] — Section with text, image, and button

Context Consumption Pattern: Widget content comes from card_params via symbol keys, NOT from inline text in card_description. The builder creates structure from DSL; content comes from context consumption.

{
  "δ": {
    "_shared": {"top_label": "Status"},
    "_items": [
      {"text": "Build passed", "icon": "STAR"},
      {"text": "Tests: 142/142"}
    ]
  },
  "ᵬ": {
    "_items": [
      {"text": "View Build", "url": "https://ci.example.com/build/123"}
    ]
  }
}

Session-scope enable required: This tool often needs session-scope enablement:

manage_tools(action="enable", tool_names="send_dynamic_card", scope="session")

After session enable, must /mcp reconnect for Claude Code to discover the tool.

For detailed component field reference: Use skill://gchat-cards/ resources (symbols, DSL syntax, 100+ component docs).


Qdrant Search Tools

ToolPurpose
qdrant_searchSearch tool response history with semantic or hybrid search
search_tool_historyFind past tool outputs by tool name, date range, or keywords
get_tool_analyticsUsage statistics across tools
get_response_detailsGet full details of a specific stored response
fetchFetch a specific Qdrant point by ID
cleanup_qdrant_dataClean up old or orphaned Qdrant data

Collections:

  • mcp_tool_responses — Stores all tool invocation results with embeddings
  • mcp_gchat_cards_v8 — Card templates and feedback patterns

Code Mode (Execute Tool)

The execute tool runs sandboxed Python with call_tool() available for chaining MCP tool calls.

Key rules:

  • Use await call_tool(tool_name, params) to call any MCP tool
  • Use return to produce output
  • import is NOT available — use built-in helpers instead
  • Prefer returning the final answer from a single block
  • Execution is time-limited (default 90s, CODE_MODE_MAX_DURATION_SECS env var) — split long chains of heavy tool calls (e.g. several full email renders) into separate execute blocks rather than one mega-block

Built-in Helpers:

CategoryFunctions
Datetimenow(tz_offset=-6), today(tz_offset=-6), days_ago(n), hours_ago(n), format_date(iso, fmt), parse_date(iso), timestamp()
JSONto_json(obj, indent), from_json(s)
URLurl_encode(s), url_decode(s), url_join(base, *parts), query_string(params)
Regexre_find(pattern, text), re_match(pattern, text), re_sub(pattern, repl, text)
Texttruncate(text, n=80), join(items, sep), html_escape(s), dedent(text), wrap_text(text, width), pad_left(s, width), pad_right(s, width)
Mathsqrt(n), ceil(n), floor(n), round_(n, digits), abs_(), min_(), max_(), sum_()
Collectionssorted_(items, key, reverse), unique(items), flatten(lists), counter(items), chunk(items, size), zip_(*iterables), dict_get(d, 'a.b.c', default)
Hashingmd5(s), sha256(s)
Asyncgather(*coros), sleep(seconds)

Code Mode Discovery Tools:

ToolPurpose
tagsList all tool tags for filtering
searchBM25 keyword search across tool names/descriptions
get_schemaGet full JSON schema for a tool before calling it
semantic_searchFull Qdrant vector search — semantic, DSL filters (filter_dsl), recommendation (positive/negative_point_ids), advanced queries (query_dsl, prefetch_dsl), and dry_run validation
fetch_documentPeek at a stored response by point ID (truncated preview)
tool_activityDashboard of tool usage — counts, error rates, sample IDs
executeRun sandboxed Python code

Example — Chain tool calls:

# Fetch labels, then compose a report email
labels = await call_tool("list_gmail_labels", {"user_google_email": "[email protected]"})
label_count = len(labels.get("labels", []))

await call_tool("compose_dynamic_email", {
    "email_description": "ε[Ħ, τ, đ, ƒ]",
    "email_params": to_json({
        "subject": f"Label Report - {today()}",
        "Ħ": {"_items": [{"title": "Gmail Labels"}]},
        "τ": {"_items": [{"text": f"You have {label_count} labels."}]},
        "ƒ": {"_items": [{"text": f"Generated {now()}"}]}
    }),
    "action": "draft"
})
return f"Draft created with {label_count} labels"

Jinja2 Macro System

Macros enable reusable, data-driven templates for emails and cards.

Location: middleware/templates/*.j2 (tracked) and middleware/templates/dynamic/*.j2 (gitignored, experimental)

Dual-Mode Pattern

Macros support two modes:

  • mode='dsl' — Returns DSL structure string (used as email_description or card_description)
  • mode='params' — Returns JSON params (used as email_params or card_params)
{% macro email_resource_report(label_data, profile_data, session_data, symbols, mode='dsl') %}
  {%- if mode == 'dsl' -%}
    {{ s.EmailSpec }}[{{ s.HeaderBlock }}, {{ s.HeroBlock }}, ...]
  {%- elif mode == 'params' -%}
    {{ result | tojson }}
  {%- endif -%}
{% endmacro %}

Resource URI Templating

Inside macros, service:// URIs are preprocessed into Jinja2 variables:

  • service://gmail/labels becomes service_gmail_labels (SimpleNamespace with dot access)
  • user://current/email becomes user_current_email
  • Access via: {{ service_gmail_labels.labels[0].name }}

create_template_macro Tool

Create macros at runtime:

ParameterTypeDescription
macro_namestrValid Python identifier (1-100 chars)
macro_contentstrFull {% macro %}...{% endmacro %} Jinja2 block (10-50K chars)
descriptionstrOptional description
usage_examplestrOptional usage example
persist_to_fileboolSave to templates/dynamic/ for persistence across restarts

Re-creating an existing name overwrites it. remove_template_macro(macro_name) deletes a dynamic macro (and its persisted file); file-based macros shipped with the server are protected. Listing stays resource-based: template://macros.

Macro invocations inside tool parameters are detected and rendered by the Template Middleware, including calls with full dict/list literals in their arguments — e.g. {{ my_macro(items=[{"k": "v"}], mode='params') }}.

email_symbols Global

The email_symbols object is available as a Jinja2 global, providing symbol-to-class mappings for constructing DSL strings dynamically in macros.


Resource URIs

User & Auth

URIDescription
user://current/emailAuthenticated user's email
user://current/profileUser profile with auth status
user://profile/{email}Profile for specific email
auth://session/currentCurrent session info
auth://sessions/listAll active sessions
auth://credentials/{email}/statusCredential status for email

Service Data

URIDescription
service://{service}/listsAvailable list types for a service
service://{service}/{list_type}All items for list type
service://{service}/{list_type}/{item_id}Specific item details

Supported services: gmail, drive, calendar, docs, sheets, chat, forms, slides, photos

Templates & Macros

URIDescription
template://macrosList all available macros
template://macros/{macro_name}Specific macro usage example

Qdrant

URIDescription
qdrant://collections/listAll Qdrant collections
qdrant://collection/{name}/infoCollection details
qdrant://collection/{name}/responses/recentRecent responses
qdrant://search/{query}Search across collections
qdrant://collection/{name}/{point_id}Specific point
qdrant://statusOverall Qdrant status

Other

URIDescription
recent://{service}Recent items for service
chat://digestChat digest (supports ?hours=N&limit=N)
gmail://messages/recentRecent Gmail messages
tools://list/allAll available tools

Skill Resources

ResourceContentWhen to Use
skill://gchat-cards/100+ component docs, symbols, containment rules, DSL syntaxDetailed card component field reference
skill://mjml-email/Email block symbols, DSL syntax, per-component docsDetailed email block field reference

Tool Management

manage_tools

ParameterTypeDefaultDescription
actionstrrequiredlist, enable, disable, disable_all_except, enable_all
tool_namesstr/listNoneTool name(s) — string, list, comma-separated, or JSON array
scopestr"global"global (all clients) or session (current client only)
service_filterstrNoneFilter by service: gmail, chat, drive, etc.

Protected tools (cannot be disabled): manage_tools, qdrant_search, health_check, start_google_auth, check_drive_auth, and Code Mode meta-tools (tags, search, get_schema, semantic_search, fetch_document, tool_activity, execute).


All Services Quick Reference

Use manage_tools(action="list") or manage_tools(action="list", service_filter="gmail") to see currently active tools.

ServiceKey Tools
Driveupload_to_drive, search_drive_files, list_drive_items, create_drive_file, share_drive_files, manage_drive_files, get_drive_file_content, make_drive_files_public
Gmailcompose_dynamic_email, search_gmail_messages, send_gmail_message, draft_gmail_message, reply_to_gmail_message, list_gmail_labels, manage_gmail_label, list_gmail_filters, create_gmail_filter, manage_gmail_allow_list
Chatsend_dynamic_card, list_spaces, list_messages, send_message, search_messages
Chat Cardssend_simple_card, send_rich_card, send_enhanced_card, send_interactive_card, send_smart_card, preview_card_from_description, validate_card, find_card_templates, save_card_template
Calendarlist_calendars, list_events, create_event, modify_event, delete_event, get_event, create_calendar, bulk_calendar_operations, move_events_between_calendars
Docssearch_docs, get_doc_content, list_docs_in_folder, create_doc
Sheetslist_spreadsheets, get_spreadsheet_info, read_sheet_values, modify_sheet_values, batch_modify_sheet_values, create_spreadsheet, create_sheet, format_sheet_range, batch_update_sheet
Slidescreate_presentation, get_presentation_info, add_slide, update_slide_content, export_and_download_presentation
Formscreate_form, add_questions_to_form, get_form, set_form_publish_state, publish_form_publicly, get_form_response, list_form_responses, update_form_questions
Photoslist_photos_albums, search_photos, upload_photos, upload_folder_photos, photos_smart_search, photos_batch_details, create_photos_album, get_photo_details
Peoplelist_people_contact_labels, get_people_contact_group_members, manage_people_contact_labels
Qdrantqdrant_search, search_tool_history, get_tool_analytics, fetch, get_response_details, cleanup_qdrant_data
Email Templatescreate_email_template, list_email_templates, preview_email_template, intelligent_email_composer, send_smart_email
Module Wrapperslist_wrapped_modules, wrap_module, search_module, list_module_components, get_module_component
Serverhealth_check, manage_credentials, manage_tools, create_template_macro
Code Modetags, search, get_schema, semantic_search, fetch_document, tool_activity, execute

Sheets Power Tools

Beyond simple reads/writes, three tools cover advanced spreadsheet work:

  • batch_update_sheet — raw spreadsheets.batchUpdate passthrough (up to 100 requests, atomic: one bad request rolls back the whole batch). This is the route to everything the dedicated tools don't cover: charts (addChart — all 9 chart families: basic BAR/LINE/AREA/COLUMN/SCATTER/COMBO/STEPPED_AREA, pie, bubble, candlestick, histogram, org, scorecard, treemap, waterfall), data-validation dropdowns (setDataValidation with ONE_OF_LIST + showCustomUi), protected/banded/named ranges, unmergeCells, delete/duplicate/rename sheets, sortRange, findReplace, autoResizeDimensions, row/column grouping, filter views, and slicers. Request objects follow the Sheets API batchUpdate reference verbatim; replies return created IDs (e.g. chart IDs). Gotcha: BAR chart series must target BOTTOM_AXIS; other basic charts use LEFT_AXIS.
  • batch_modify_sheet_values — write many ranges and/or clear ranges in single API calls; prefer it over looping modify_sheet_values whenever touching more than ~2 ranges.
  • get_spreadsheet_info with fields — read back formatting, conditional-format rules, charts, merges, and validation via a Google API field mask (raw response in raw), e.g. sheets(properties,charts(chartId,spec(title))). Scope grid-data masks tightly.

Cell values are real scalars: numbers/booleans round-trip properly (RAW input writes real numbers; UNFORMATTED_VALUE reads them back typed). modify_sheet_values also supports append=True to add rows after existing data.


Cross-Cutting Features

Privacy Middleware

  • Encrypts PII in tool responses using per-session Fernet keys
  • LLM sees masked tokens ([MASKED:token_1]), wire carries ciphertext
  • Modes: auto (heuristic PII detection) or strict (encrypt all strings)
  • Round-trip: [PRIVATE:token_N] in tool arguments resolves back to plaintext

Authentication

  1. Call start_google_auth with email to begin OAuth flow
  2. Complete authentication in browser
  3. Call check_drive_auth to verify credentials
  4. Session-level account linking tracks authed emails across session lifetime

Response History (Qdrant)

  • All tool responses are automatically stored with embeddings
  • Searchable via qdrant_search (semantic), search_tool_history (filtered), or get_tool_analytics (stats)
  • Collections: mcp_tool_responses (all responses), mcp_gchat_cards_v8 (card templates + feedback)

Keep looking

Skills are one crate of 628,971. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.