AppsScriptPulse

Navigating AI Agent Protocols: From Connection to Skill

The growing landscape of AI agent development is overloaded with acronyms: MCP, A2A, UCP, AP2, A2UI, and AG-UI, just to name a few. If you’ve ever looked at this list of protocols and felt like you were staring at a wall of competing standards, you are not alone. To help you understand their value, we are going to demonstrate what each one does to save you from writing and maintaining custom integration code for every single tool, API, and frontend component your agent touches

In Pulse we have recently featured some excellent examples of agentic integration into Google Workspace, in particular, Pierrick Voulet’s insightful posts on extending Chat apps with Universal Actions provide a great template for building intelligent assistants. For those following Pierrick on LinkedIn you’ll know these are just the tip of the iceberg.

With users increasingly expecting these intelligent assistants to communicate seamlessly across their everyday tools, knowing the right standards to connect different systems becomes a significant challenge. Fortunately, the Google Developers Blog has published a comprehensive guide to help navigate this exact problem.

In a recent post there is an overview of the growing list of acronyms associated with AI agent development. The post breaks down six main protocols:

  • Model Context Protocol or MCP: This standardizes connection patterns for servers. Instead of writing custom API requests for every service, your agent discovers tools automatically.
  • Agent2Agent Protocol or A2A: This standardizes how remote agents discover and communicate with each other using well-known URLs.
  • Universal Commerce Protocol or UCP: This modularizes the shopping lifecycle into strongly typed schemas that remain consistent across any underlying transport.
  • Agent Payments Protocol or AP2: This adds cryptographic proof of authorization to checkout flows to enforce configurable guardrails on transactions.
  • Agent-to-User Interface Protocol or A2UI: This lets the agent dynamically compose novel layouts from a fixed catalog of safe component primitives.
  • Agent-User Interaction Protocol or AG-UI: This acts as middleware that translates raw framework events into a standardized stream for the frontend.

To illustrate these concepts, the guide walks through building a multi-step supply chain agent using the Agent Development Kit. The scenario starts with a bare large language model and progressively adds protocols until the agent can check inventory, get quotes, place orders, authorize payments, and render interactive dashboards.

Adopting standard protocols is only half the equation, and how effectively you apply them shouldn’t be overlooked. As an example, Richard Seroter recently highlighted this in his analysis of agent token consumption. He discovered that simply attaching an MCP to an agent often leads to excessive planning iterations and high token costs. By pairing an MCP with a highly focused “Skill”, a structured set of instructions that guides the tool’s application, developers can drastically cut down on wasted turns. In one test, combining an MCP with a specific skill resulted in an 87% reduction in token usage compared to letting the agent figure out the tool on its own.

For Google Workspace developers building complex integrations, it’s important not to just be aware of the emerging protocols, but also the best practices for implementing them. Giving your agent a connection is a great start; teaching it the specific skill to use that connection efficiently is essential.

Sources:

From Prompt to Pull Request: Automating Development with Jules and Apps Script

Late last year, Google introduced Jules, an experimental coding agent designed to handle the heavy lifting of software development asynchronously. While you may have explored Jules through its web interface, the recent release of a dedicated Apps Script SDK by David East opens up interesting possibilities for the Google Workspace developer community.

What is Jules?

For those who missed the initial announcement, Jules is an autonomous agent that integrates with GitHub to fix bugs, refactor code, and build new features. Unlike a standard code completion tool, Jules works in the background within a secure virtual machine. As Google describes it:

“Jules is an asynchronous, agentic coding assistant that integrates directly with your existing repositories”

As of the latest updates, Jules now can use Gemini 3.1 Pro. This upgrade provides a smarter baseline for complex problem-solving, allowing the agent to create multi-step plans with clearer reasoning and stronger intent alignment.

Google also released an alpha API which allows management of your Jules agent programmatically. This means developers can move beyond manual prompting. One potential use case is building a triage system that listens for bug reports in a support channel and automatically invokes Jules to analyse the codebase and prepare a fix.

Integrating with Google Apps Script

The new jules-apps-script-sdk simplifies the process of making these API calls from within the Apps Script environment. The library handles the authentication and request structures required to interact with the Jules service, allowing you to trigger agentic tasks directly from a spreadsheet, a form submission, or a time-based trigger.

For example, you could set up a workflow where a specific comment in a Google Doc triggers an Apps Script function to send a prompt to Jules, requesting a unit test for a new function in your repository. You can find out more by following the source link on GitHub.

Source: JulesApp SDK for Google Apps Script

Cross-cloud automation: Deploying Google Apps Script logic to Azure

Bruce Mcpherson has been working on a series of articles exploring how to liberate native Apps Script logic from its browser-based sandbox. If you’ve been following along, you’ll know he’s previously covered AWS Lambda, Google Cloud Run, and Kubernetes. Bruce has now added Azure Container Apps (ACA) to the list of supported environments.

TL;DR: By using the @mcpher/gas-fakes library and Workload Identity Federation, you can run native Apps Script logic on Azure Container Apps Jobs. This approach offers a massive 24-hour execution window, making it suitable for high-intensity tasks like large data migrations or security scans that would otherwise time out in the standard Google environment.

Moving logic to Azure introduces some complexity, particularly around authentication, which Bruce covers in his post and to bridge this gap, the project includes a custom Identity Bridge to handle the handshake between Azure and Google.

You can read the full technical breakdown and access the comparison matrix on Bruce’s blog.

Source: Yes – you can run native apps script code on Azure ACA as well! – Desktop liberation

Major update to the website framework for Apps Script Web Apps

Editor: We’ve previously featured Zig Mandel’s comprehensive framework that integrates Google Apps Script web apps into a standard website. Zig has been busy with an update which we are reposting from Reddit with permission:

I’ve shipped a major update to my Apps Script Website Integration Framework. The framework now allows running an HTMLService frontend entirely outside the GAS iframe, directly on your website.

Why use this?
HTMLService is convenient, but the iframe environment blocks a lot of modern web-dev capabilities: slow load, limited browser APIs, no TypeScript, no React, no Vite/live-reload, no custom domains, etc.

This update removes all of those constraints. You can develop, debug, and deploy a GAS webapp like a normal website – using any tooling, libraries, or build process you want.

How this compares to the previous method
The original method already bypassed several HTMLService limitations. The new approach goes further by running completely outside the iframe (faster, full capabilities), with one trade-off: it doesn’t support HTML templates. If you rely on templates, you can start with the original method and later migrate to this new method once templates are no longer needed.

The monorepo includes live working examples. Star if you like it!

Repo here: https://github.com/zmandel/demosite_appscript

Breaking boundaries: Running native Apps Script on Google Cloud Run

Containerize apps script code and run it on cloud platforms such as Cloud Run outside the context limitations of the Apps Script IDE.

Bruce Mcpherson continues to expand the capabilities of his gas-fakes library, a tool that has already proven valuable for running Google Apps Script locally on Node.js. In his latest update, Bruce demonstrates how to take this a step further by containerising Apps Script code to run on Google Cloud Run.

For developers familiar with the constraints of the Apps Script IDE, particularly the execution time limits, moving logic to a serverless container environment offers a powerful alternative. With the release of version 2.0.2, gas-fakes now includes a managed configuration for Workspace Domain-Wide Delegation (DWD) enabling secure keyless authentication.

Essentially, this allows developers to package their Apps Script logic into containers, enabling execution on scalable platforms like Cloud Run, free from the constraints of the standard IDE.

Authentication and Service Accounts

One of the friction points in moving from a bound script to a cloud environment is authentication. Bruce highlights that while Application Default Credentials (ADC) work well for local development, a more secure method is required for Cloud Run. The updated gas-fakes CLI simplifies this by handling the service account configuration automatically.

The library supports two primary authentication types:

  • Domain-Wide Delegation (DWD): Recommended for production environments and cross-platform scenarios, such as Cloud Run or Kubernetes.
  • Application Default Credentials (ADC): A fallback method primarily for local development.

Containerisation Workflow

For those looking to deploy their own scripts, Bruce’s guide walks through the essentials required to containerise a project.

It is important to note that while the core logic can be written in JavaScript, you will need to manually set up the infrastructure configuration. The guide provides the specific code required for:

  1. The Dockerfile: To mimic the Apps Script runtime environment.
  2. Cloud Build Configuration: A cloudbuild.yaml file to manage the build steps.

To tie it all together, the article includes a deployment script (referred to as deploy-cloudrun.sh in the text) which automates the pipeline. It handles everything from creating the Artifact Registry repository and submitting the build, to monitoring the deployment.

Code Example

To help developers get started, Bruce has provided a dedicated repository containing a working example.

Note on usage: The repository contains the core Node.js logic (example.js) and a deployment helper script (named exgcp.sh in the repo). However, to deploy this successfully, you will need to combine these files with the Dockerfile and cloudbuild.yaml configurations detailed in the main article. Bruce notes there is more to come.

The example.js file illustrates how standard Apps Script services are imported and used within the Node.js environment. By requiring gas-fakes, you can access services like DriveApp or SpreadsheetApp using the exact same syntax you use in the Apps Script editor, bridging the gap between local Node development and the Google Cloud runtime.

Summary

This development opens up interesting possibilities for hybrid workflows where lightweight tasks remain in Apps Script, while heavier processing is offloaded to Cloud Run without needing to rewrite the core logic in a different language. If you are interested in trying this out, Bruce has provided example deployment scripts and a troubleshooting guide to help you get started.

Source: Yes – you can now run native Apps Script on Cloud Run! – Desktop liberation

Vertex AI Advanced Service in Apps Script: A Step Forward or a Missed Opportunity?

As Google Apps Script developers, we are used to waiting. We wait for new runtime features, we wait for quotas to reset, and recently, we have been waiting for a first-class way to integrate Gemini into our projects.

With the recent release of the Vertex AI Advanced Service, the wait is technically over. But as detailed in Justin Poehnelt’s recent post, Using Gemini in Apps Script, you might find yourself asking if this was the solution we were actually looking for.

While the new service undoubtedly reduces the boilerplate code required to call Google’s AI models, it brings its own set of frustrations that leave me, and others in the community, feeling somewhat underwhelmed.

The “Wrapper” Trap

On the surface, the new VertexAI service looks like a win. As Justin highlights, replacing complex UrlFetchApp headers with a single VertexAI.Endpoints.generateContent() call is a significant cleanup.

However, this convenience comes with an administrative price tag. The Vertex AI Advanced Service requires a standard Google Cloud Project, understandable for billing, but requires the creation of an oAuth consent screen. For the majority of internal enterprise applications, I would imagine either a service account or a https://www.googleapis.com/auth/cloud-platform scope and associated IAM will be the preferred approach. This removes the need for a consent screen and, in the case of Service Accounts, rules out the Vertex AI Advanced Service.

It begs the question: Why didn’t Google take the approach of the Google Gen AI SDK?

In the Node.js and JavaScript world, the new Google Gen AI SDK offers a unified interface. You can start with a simple API key (using Google AI Studio) for prototyping, and switch to Vertex AI (via OAuth) for production, all without changing your core code logic. The Apps Script service, by contrast, locks us strictly into the “Enterprise” Vertex path. We seem to have traded boilerplate code for boilerplate configuration.

A Third Way: The Community Approach

If you are looking for that Unified SDK experience I mentioned earlier, where you can use the standard Google AI Studio code patterns within Apps Script, there is a third way.

I have published a library, GeminiApp, which wraps UrlFetchApp but mimics the official Google Gen AI SDK for Node.js. This allows you to write code that looks and feels like the modern JavaScript SDK, handling the complex UrlFetchApp configuration under the hood.

As you can see in the comparison above, the Advanced Service (left) abstracts away the request complexity, the UrlFetchApp method (middle) gives you the transparency and control you often need in production, and the GeminiApp library (right) offers a balance of both.

Disclaimer: As the creator of this library, I admit some bias, but it was built specifically to address the gap.

It is important to note a distinction in scope. Both the Google Gen AI SDK and GeminiApp are focused strictly on generative AI features. The Vertex AI Advanced Service, much like the platform it wraps, offers a broader range of methods beyond just content generation.

If your needs extend into those wider Vertex AI capabilities, but you still require the authentication flexibility of UrlFetchApp (such as using Service Accounts), I have a solution for that as well. My Google API Client Library Generator for Apps Script includes a build for the full Vertex AI (AI Platform) API. This gives you the comprehensive coverage of the Advanced Service with the architectural flexibility of an open-source library.

Here is how you can use the generated client library to authenticate with a Service Account, something impossible with the official Advanced Service:

/**
 * Example using the generated Aiplatform library with a Service Account.
 * Library: https://github.com/mhawksey/Google-API-Client-Library-Generator-for-Apps-Script/tree/main/build/Aiplatform
 */
function callGemini(prompt) {
  const projectId = 'GOOGLE_CLOUD_PROJECT_ID';
  const region = 'us-central1';
  const modelName = 'gemini-2.5-flash';

  const modelResourceName = `projects/${projectId}/locations/${region}/publishers/google/models/${modelName}`;

  const serviceAccountToken = getServiceAccountToken_(); 

  const vertexai = new Aiplatform({
    token: serviceAccountToken
  });

  const payload = {
    contents: [{
      role: 'user',
      parts: [{
        text: prompt
      }]
    }],
    generationConfig: {
      temperature: 0.1,
      maxOutputTokens: 2048
    }
  };

  const result = vertexai.projects.locations.publishers.models.generateContent({
    model: modelResourceName,
    requestBody: payload
  });

  return result.data.candidates?.[0]?.content?.parts?.[0]?.text || 'No response generated.';
}

When “Advanced” Means “Behind”

There is another catch that Justin uncovered during his testing: the service struggles with the bleeding edge.

If you are trying to access the latest “Preview” models to prototype, such as the highly anticipated gemini-3-pro-preview, the advanced service may fail you. It appears the wrapper doesn’t yet support the auto-discovery needed for these newer endpoints.

In his companion post, UrlFetchApp: The Unofficial Documentation, Justin reminds us why UrlFetchApp is still the backbone of Apps Script development. When the “official” wrapper doesn’t support a specific header or a beta model, UrlFetchApp is the only way to bypass the limitations.

The Verdict

The Vertex AI service is a welcome addition for stable, enterprise-focused applications. But for developers, particularly those who want to test the latest Gemini 3 capabilities, it feels rigid compared to the flexibility seen in other Google developer ecosystems.

It serves as a good reminder that in Apps Script, convenience services are great, but understanding the underlying HTTP requests via UrlFetchApp  extends what you can achieve.

Breaking the Grid: 4 Ways to Generate Excel Files from Google Workspace

We have all been there: You build a beautiful, automated dashboard in Google Sheets. It’s dynamic, it’s collaborative, and it’s perfectly integrated with your data sources. Then, the stakeholder request comes in: “Can you email this to me as an Excel file every Monday morning?”

For Google Workspace developers, the “Excel Requirement” is often a stumbling block. While Google Sheets is powerful, the bridge to Excel can be shaky. In this post, I’m going to explore four architectural approaches to solving this problem. If you would like to play along here is the Google Sheet Invoice Template and the Excel Invoice Template.

1. The Low-Code Agent (AppSheet Automation)

Before you write a single line of code, you should consider if the platform can do the heavy lifting for you. Since AppSheet is now a core part of Google Workspace, it offers a native “No-Code” way to generate Excel files that is robust, scheduled, and template-based.

The Strategy

Instead of writing a script to fetch data and build a file, you connect your Google Sheet to an AppSheet app. You then configure an Automation that triggers on a schedule or a data change.

The Workflow:

  1. Trigger: A new row is added to your “Invoices” sheet (or a weekly schedule fires).
  2. Task: AppSheet runs a “Create a new file” task.
  3. Template: It reads a simplified Excel template stored in Drive, replaces tags like <<[Qty]>> with actual data, and generates the file.
  4. Delivery: The file is saved to Drive or emailed directly to the client.

Why This Wins

  • Zero Maintenance: No scripts to debug, no OAuth tokens to manage, and no breaking changes when APIs update.
  • Native Templates: It supports complex Excel formatting out of the box.

The Limitation

  • Syntax: You must use AppSheet’s specific templating syntax (<< >>) inside your Excel file.
  • Scaling: Generating reports with thousands of line items can hit automation timeout limits.

2. The Native Export (Apps Script)

If you need more granular control or don’t want to spin up an AppSheet app, the most common starting point is attempting to do everything within Apps Script. You create a temporary Google Sheet, fill it with data, and then export it using the Drive API.

The Code

In this scenario, we assume you have a Google Sheet version of your Invoice template. We first write the specific line items to the Sheet, then export it.

function generateInvoiceExport() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('InvoiceTemplate');
  
  // 1. Prepare Invoice Data
  const invoiceData = [
    { desc: "Web Development - Phase 1", qty: 40, unit: 100 },
    { desc: "Server Hosting (Annual)", qty: 1, unit: 250 },
    { desc: "Domain Registration", qty: 2, unit: 15 },
    { desc: "Maintenance Retainer", qty: 10, unit: 85 }
  ];
  
  // 2. Map data to the Sheet structure (Cols B, C, D, E, F)
  // B=Desc, E=Qty, F=Unit. (C and D are skipped/empty)
  const rows = invoiceData.map(item => [item.desc, "", "", item.qty, item.unit]);

  // 3. Write data to the Google Sheet
  // Matches Template: Start at Row 19, Column B (2)
  if (rows.length > 0) {
    // Corrected: ensure the number of columns (5) matches your data structure
    sheet.getRange(19, 2, rows.length, 5).setValues(rows);
  }
  
  // 4. Construct the export URL for the populated sheet
  const fileName = `Invoice_${new Date().toISOString().slice(0,10)}.xlsx`;
  const url = `https://docs.google.com/spreadsheets/d/${ss.getId()}/export?format=xlsx&gid=${sheet.getSheetId()}`;
  
  // 5. Fetch and Save
  const params = {
    method: "GET",
    headers: { "Authorization": `Bearer ${ScriptApp.getOAuthToken()}` },
    muteHttpExceptions: true
  };
  
  const response = UrlFetchApp.fetch(url, params);
  
  if (response.getResponseCode() === 200) {
    const blob = response.getBlob().setName(fileName);
    const file = DriveApp.createFile(blob);
    console.log(`Success: Invoice saved to Drive (ID: ${file.getId()})`);
  } else {
    console.log(`Error: ${response.getContentText()}`);
  }
};

The Limitations

  • Speed & Scale: The overhead of creating a temporary sheet and exporting it adds up. Generating 50 invoices in a loop will take a long time.
  • The “Source” Gap: Your source material must be a Google Sheet, meaning you lose Excel-specific features like Power Query or VBA macros from your template.

3. The Browser Round-Trip (ExcelJS)

If you need to maintain stricter fidelity to an existing Excel template (preserving fonts, colors, and layouts) and are interested in speed, you can use the “Round Trip” technique.

This method uses the user’s browser as a processing engine. You fetch the Invoice.xlsx template from Drive, process it client-side with ExcelJS, and send the result back.

View the Code Project

The Strategy

  1. Parallel Fetching (Client): The browser fires concurrent requests to Apps Script to fetch the template (Base64) and invoice data.
  2. Client-Side Processing: ExcelJS loads the template, injects data into specific cells (preserving layout), and recompiles the file.
  3. Upload: The new file is encoded and sent back to Drive.

The Code

This snippet focuses on the core orchestration logic. For the full UI setup and helper functions, see the linked project.

/* --- CLIENT SIDE (Core Logic) --- */
async function startGeneration() {
  
  // 1. Parallel Fetching
  // Fire both requests at once to speed up the wait time
  const [templateResponse, invoiceData] = await Promise.all([
    runGoogleScript('getTemplateBase64', null), 
    runGoogleScript('getInvoiceData')
  ]);

  // 2. Load into ExcelJS
  const workbook = new ExcelJS.Workbook();
  const binaryString = atob(templateResponse.base64);
  const bytes = new Uint8Array(binaryString.length);
  for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } await workbook.xlsx.load(bytes.buffer); // 3. Modify the Sheet const sheet = workbook.worksheets[0]; let currentRow = 19; // Starting at Row 19 based on template invoiceData.forEach((item, index) => {
    // Ensure we do not write past the visual template limit (e.g. Row 22)
    if (currentRow + index > 22) return;

    const row = sheet.getRow(currentRow + index);
    
    // Inject data into specific cells (B, E, F)
    row.getCell(2).value = item.desc;  
    row.getCell(5).value = item.qty;   
    row.getCell(6).value = item.unit;  
    
    // ExcelJS preserves the existing styles of these cells!
    row.commit();
  });

  // 4. Export & Save
  const outBuffer = await workbook.xlsx.writeBuffer();
  
  // Convert buffer to base64 (simplified for brevity)
  let binary = '';
  const outBytes = new Uint8Array(outBuffer);
  for (let i = 0; i < outBytes.length; i++) {
    binary += String.fromCharCode(outBytes[i]);
  }
  const outBase64 = btoa(binary);

  // Upload back to Drive
  await runGoogleScript('saveProcessedFile', outBase64, `Invoice_${Date.now()}.xlsx`);
}

/* --- SERVER SIDE (Apps Script) --- */
function getTemplateBase64(templateId) {
  const file = DriveApp.getFileById(templateId);
  return {
    base64: Utilities.base64Encode(file.getBlob().getBytes())
  };
}

function saveProcessedFile(base64Content, fileName) {
  const blob = Utilities.newBlob(
    Utilities.base64Decode(base64Content), 
    MimeType.MICROSOFT_EXCEL, 
    fileName
  );
  return { url: DriveApp.createFile(blob).getUrl() };
}

The Trade-off

  • Payload Limits: google.script.run calls can become unstable when the payload exceeds 50MB.
  • Base64 Inflation: Remember that Base64 encoding increases file size by ~33%.

4. The Enterprise Engine (Cloud Functions + Python)

For robust, high-fidelity reporting, we need to leave the Google Sheets conversion pipeline behind. By using Google Cloud Functions with Python, we can manipulate native Excel files directly using libraries like openpyxl.

The “User Identity” Pattern

Instead of managing complex Service Account permissions, this solution uses the User’s Identity to perform Drive operations.

  1. Apps Script: Generates an OIDC ID Token (for identity), using getIdentityToken()) and an OAuth Access Token (for Drive permissions) with getOAuthToken().
  2. Cloud Function: Verifies the ID Token and uses the Access Token to initialise the Drive API client as the user.

Setup Instructions

1. Google Cloud Function (Python)

  • Deploy the Function:
    • Source: cloud-fn.py
    • Runtime: Python 3.x
    • Entry point: generate_excel_report
    • Requirements: functions-frameworkgoogle-authgoogle-api-python-clientopenpyxl
  • Configure Environment:
    • Open cloud-fn.py and update APPS_SCRIPT_CLIENT_ID with your Apps Script’s OAuth Client ID.
      Note: You can find this ID by running the logClientId() helper function in Code.js.

2. Google Apps Script (Code.js)

  • Update Manifest (appsscript.json): Add the https://www.googleapis.com/auth/userinfo.email scope.
  • Configuration: Update YOUR_CLOUD_FUNCTION_URL in callInvoiceEngine() with your deployed function URL.

The Code

Google Apps Script (Code.js)

function callInvoiceEngine() {
    const url = "YOUR_CLOUD_FUNCTION_URL";

    // 1. Get Tokens
    const accessToken = ScriptApp.getOAuthToken();
    const idToken = ScriptApp.getIdentityToken(); // Requires 'openid' scope in manifest

    // --- LOGGING FOR LOCAL TESTING ---
    console.log("--- CREDENTIALS FOR LOCAL TESTING ---");
    console.log("ID Token:", idToken);
    console.log("Access Token:", accessToken);
    console.log("-------------------------------------");

    const payload = {
        templateId: "ID_OF_TEMPLATE_FILE",
        folderId: "ID_OF_DESTINATION_FOLDER",
        // Matches the same dataset used in our other examples
        data: [
            { desc: "Web Development - Phase 1", qty: 40, unit: 100 },
            { desc: "Server Hosting (Annual)", qty: 1, unit: 250},
            { desc: "Domain Registration", qty: 2, unit: 15},
            { desc: "Maintenance Retainer", qty: 10, unit: 85}
        ],
        // The Magic Key: Passing the active user's Access Token for Drive operations
        userToken: accessToken
    };

    const options = {
        method: "post",
        contentType: "application/json",
        headers: {
            "Authorization": `Bearer ${idToken}`
        },
        payload: JSON.stringify(payload)
    };

    UrlFetchApp.fetch(url, options);
}

function logClientId() {
    // Helper to find the Project's OAuth Client ID
    var token = ScriptApp.getOAuthToken();
    var url = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' + token;
    var response = UrlFetchApp.fetch(url);
    var data = JSON.parse(response.getContentText());
    
    console.log('Client ID:', data.issued_to);
}

Pro Tip: Batching Requests

If you need to generate 50 invoices at once, don’t loop through them one by one. Use UrlFetchApp.fetchAll(requests) to fire off parallel requests to your Cloud Function. Since the heavy lifting happens in the cloud, Apps Script just acts as the orchestrator.

Google Apps Script (appsscript.json)

{
    "oauthScopes": [
        "https://www.googleapis.com/auth/drive",
        "https://www.googleapis.com/auth/script.external_request",
        "https://www.googleapis.com/auth/userinfo.email"
    ]
}

Cloud Function (cloud-fn.py)

import requests
import json

def run_test():
    print("--- Local Cloud Function Tester ---")
    print("Ensure you are running: functions-framework --target=generate_excel_report --source=cloud-fn.py --port=8080\n")

    # 1. Gather Tokens & IDs
    print("Paste your Apps Script Tokens (use Logger.log in GAS):")
    id_token = input("1. ID Token (ScriptApp.getIdentityToken()): ").strip()
    if not id_token: return
    
    access_token = input("2. Access Token (ScriptApp.getOAuthToken()): ").strip()
    if not access_token: return

    template_id = input("3. Google Sheet/Excel Template ID: ").strip()
    folder_id = input("4. Destination Folder ID: ").strip()

    # 2. Prepare Payload
    payload = {
        "templateId": template_id,
        "folderId": folder_id,
        "data": [
            { "desc": "Local Test Item 1", "qty": 1, "unit": 10 },
            { "desc": "Local Test Item 2", "qty": 5, "unit": 20 }
        ],
        "userToken": access_token
    }

    headers = {
        "Authorization": f"Bearer {id_token}",
        "Content-Type": "application/json"
    }

    # 3. Send Request
    url = "http://localhost:8080"
    print(f"\nSending POST request to {url}...")
    
    try:
        response = requests.post(url, json=payload, headers=headers)
        print(f"\nStatus Code: {response.status_code}")
        print(f"Response Body: {response.text}")
            
    except requests.exceptions.ConnectionError:
        print(f"\n[!] Could not connect to {url}.")
        print("Is the functions-framework running?")

if __name__ == "__main__":
    run_test()

Local Test Script (test_request.py)

Before deploying, you can test the entire flow locally to ensure your tokens and permissions are working correctly.

  1. Start the Function: functions-framework --target=generate_excel_report --source=cloud-fn.py --port=8080
  2. Run the Tester: python3 test_request.py (Paste tokens from Apps Script logs when prompted).
import requests
import json

def run_test():
    print("--- Local Cloud Function Tester ---")
    print("Ensure you are running: functions-framework --target=generate_excel_report --source=cloud-fn.py --port=8080\n")

    # 1. Gather Tokens & IDs
    print("Paste your Apps Script Tokens (use Logger.log in GAS):")
    id_token = input("1. ID Token (ScriptApp.getIdentityToken()): ").strip()
    if not id_token: return
    
    access_token = input("2. Access Token (ScriptApp.getOAuthToken()): ").strip()
    if not access_token: return

    template_id = input("3. Google Sheet/Excel Template ID: ").strip()
    folder_id = input("4. Destination Folder ID: ").strip()

    # 2. Prepare Payload
    payload = {
        "templateId": template_id,
        "folderId": folder_id,
        "data": [
            { "desc": "Local Test Item 1", "qty": 1, "unit": 10 },
            { "desc": "Local Test Item 2", "qty": 5, "unit": 20 }
        ],
        "userToken": access_token
    }

    headers = {
        "Authorization": f"Bearer {id_token}",
        "Content-Type": "application/json"
    }

    # 3. Send Request
    url = "http://localhost:8080"
    print(f"\nSending POST request to {url}...")
    
    try:
        response = requests.post(url, json=payload, headers=headers)
        print(f"\nStatus Code: {response.status_code}")
        print(f"Response Body: {response.text}")
            
    except requests.exceptions.ConnectionError:
        print(f"\n[!] Could not connect to {url}.")
        print("Is the functions-framework running?")

if __name__ == "__main__":
    run_test()

Why This Wins

  • True Excel Support: You are editing the actual binary file, not a conversion.
  • Separation of Concerns: The heavy lifting is done in the cloud, not in the browser or the Apps Script runtime.

Summary: Which Tool for Which Job?

Feature AppSheet Apps Script (Native) Client-Side (ExcelJS) Cloud Functions (Python)
Complexity Very Low Low High High
Cost Free Free Free GCP Usage (Low)
Best For Standard Workflows Simple Reports Interactive Template Fills Enterprise Reports

Final Thoughts

There is no “one size fits all” here. The “Excel Requirement” is a spectrum:

  • For quick internal tools: Stick to Method 1 (AppSheet) and Method 2 (Native). It’s fast to build and free.
  • For polished client reports: Method 3 (ExcelJS) is your best friend. It keeps the heavy lifting in the browser and preserves your branding.
  • For enterprise scale: If you are generating hundreds of invoices a week, Method 4 (Cloud Functions) is the robust choice. It decouples the generation logic from the spreadsheet UI.

Choose the architecture that fits your volume and your “pixel-perfect” tolerance. I’m also keen to hear have got another ‘Excel Generator’ approach? Happy scripting!

The Age of the Agent: Google’s Managed MCP Servers and Community Contributions

Image credit: Kanshi Tanaike

 

The “USB-C for AI” has officially arrived at Google.

If you have been following the rapid rise of Anthropic’s Model Context Protocol (MCP), you know it promises a standardised way for AI models to connect to data and tools. Until now, connecting an LLM to your Google Drive or BigQuery data meant building custom connectors or relying on fragmented community solutions.

That changed this week with Google’s announcement of fully managed, remote MCP servers for Google and Google Cloud services. But typically for the Google Workspace developer community, we aren’t just waiting for official endpoints—we are building our own local solutions too.

Here is an overview of the different approaches emerging for MCP tools and what the future looks like for Workspace developers.

The Official Path: Managed MCP Servers

Google’s announcement represents a massive shift in how they view AI agents. Rather than asking developers to wrap every granular API endpoint (like compute.instances.insert) into a tool, Google is releasing Managed MCP Servers.

Think of these as “Client Libraries for Agents.” Just as Google provides SDKs for Python or Node.js to handle the heavy lifting of authentication and typing, these managed servers provide a simplified, reasoned interface for agents.

The announcement lists a massive rollout over the “next few months” covering everything from Cloud Run and Storage to the Android Management API. The goal described is to make MCP the standard way agents connect to the entire Google ecosystem. To start with Google announced MCPs for:

  • Google Maps: instead of just raw data, the agent gets “Grounding Lite” to accurately answer location queries without hallucinating.
  • BigQuery: The agent can interpret schemas and execute queries in place, meaning massive datasets don’t need to be moved into the context window.
  • Google Kubernetes Engine (GKE): Agents can diagnose issues and optimize costs without needing to string together brittle CLI commands.

The Community Path: Privacy-First and Scriptable

While managed servers are excellent for scale and standardisation, there are some other notable approaches using local execution.

1. The Gemini CLI Workspace Extension As detailed in a recent tutorial by Romin Irani, the official google-workspace extension for the Gemini CLI takes a privacy-first approach. Instead of running in the cloud, the MCP server runs locally on your machine.

It uses your own local OAuth token, meaning your data flow is strictly Google Cloud <-> User's Local Machine <-> Gemini CLI. This is ideal for tasks like searching Drive, checking your Calendar, or drafting Gmail replies where you want to ensure no third-party intermediary is processing your data.

2. The Power of Apps Script (gas-fakes) For true customisability, Kanshi Tanaike has been pushing Google Apps Script. A recent proof-of-concept using gas-fakes CLI demonstrates how to build MCP tools directly using Apps Script.

This is a big step for Apps Script developers. It allows you to write standard GAS code (e.g., DriveApp.getFilesByName()) and expose it as a tool to an MCP client like Gemini CLI or Google Antigravity. Because it uses the gas-fakes sandbox, you can execute these scripts locally, bridging the gap between your local AI agent and your cloud-based Workspace automation.

The Future: A Universal MCP Strategy?

We are likely moving toward a world where every Google product launch includes three things: API documentation, Client SDKs, and an Official MCP Server Endpoint.

For developers, the choice will come down to granularity and control:

  • Use Managed Servers when you need reliable, standard access to robust services like BigQuery or Maps with zero infrastructure overhead.
  • Use Community/Local Tools (like the Workspace extension or gas-fakes) when you need rapid prototyping, deep customization, or strict data privacy on your local machine.

The “Agentic” future is here, and whether you are using Google’s managed infrastructure or writing your own tools, the ecosystem is ready for you to build.

Sources:

Bring Your AI Agents to Life Across Google Workspace with the New Travel Concierge Sample

This tutorial shows you how to publish AI agents to Google Workspace as Google Workspace add-ons, using Apps Script or HTTP endpoints. After your publish your add-on, your users can interact with the AI agents within their workflows.

Google has released a comprehensive new tutorial that demonstrates how to bridge the gap between complex AI agents and the daily workflows of Google Workspace users. The Travel Concierge sample uses the Agent Development Kit (ADK) to deploy a conversational, multi-agent AI directly into the Google Workspace platform.

While many AI demos focus on standalone chat interfaces, this solution stands out by implementing the agent as a Google Workspace Add-on. This means the AI isn’t just a browser tab; it is a helpful sidebar accessible directly within Gmail, Calendar, Drive, Docs, Sheets, and Slides, as well as a fully functioning Google Chat app.

Key Features for Developers

The sample provides a robust blueprint for developers looking to build “agentic” workflows. Key capabilities include:

  • Multi-Agent Architecture: The solution uses Vertex AI to manage a “Travel Concierge” that orchestrates sub-agents for specific tasks, utilising tools like the Google Maps Platform Places API and Google Search Grounding.
  • Context Awareness: In applications like Gmail, the agent can read the context of the currently selected email to help plan trips based on actual correspondence.
  • Rich User Interfaces: The add-on moves beyond simple text, utilising the Card framework to display rich widgets and interactive elements.
  • Cross-Platform Persistence: User sessions are managed by Vertex AI, ensuring that a conversation started in Google Chat can be seamlessly continued in a Google Doc sidebar.

Flexible Implementation

The Apps Script implementation demonstrates how to connect a standard Workspace Add-on to the powerful Vertex AI backend using UrlFetchApp. It serves as an excellent reference for handling synchronous execution limits and rendering complex agent responses within the constraints of the Add-on sidebar.

Whether you are looking to build a travel planner or a complex enterprise assistant, this sample provides the architectural patterns needed to bring your AI agents to where your users actually work.

Source: Plan travels with an AI agent accessible across Google Workspace  |  Google Workspace add-ons  |  Google for Developers

Mastering Workspace API Authentication in ADK Agents with a Reusable Pattern

For developers seasoned in the Google Workspace ecosystem, the promise of agentic AI is not just about automating tasks, but about creating intelligent, nuanced interactions with the services we know so well. Google’s Agent Development Kit (ADK) provides the foundation, but integrating the familiar, user-centric OAuth 2.0 flow into this new world requires a deliberate architectural approach.

This article explores the patterns for building a sophisticated, multi-capability agent. It explores why you might choose a custom tool implementation (ADK’s “Journey 2”) over auto-generation, and presents a reusable authentication pattern that you can apply to any Workspace API.

The full source code for this project is available in the companion GitHub repository. It’s designed as a high-fidelity local workbench, perfect for development, debugging, and rapid iteration.

The Architectural Crossroads: Journey 1 vs. Journey 2

When integrating a REST API with the ADK, you face a choice, as detailed in the official ADK authentication documentation:

  • Journey 1: The “Auto-Toolify” Approach. This involves using components like OpenAPIToolset or the pre-built GoogleApiToolSet sets. You provide an API specification, and the ADK instantly generates tools for the entire API surface. This is incredibly fast for prototyping but can lead to an agent that is “noisy” (has too many tools and scopes) and lacks the robustness to handle API-specific complexities like pagination.
  • Journey 2: The “Crafted Tool” Approach. This involves using FunctionTool to wrap your own Python functions. This is the path taken in this project. It requires more initial effort but yields a far superior agent for several reasons:
    • Control: This approach exposes only the high-level capabilities needed (e.g., search_all_chat_spaces), not every raw API endpoint.
    • Robustness: Logic can be built directly into the tools to handle real-world challenges like pagination, abstracting this complexity away from the LLM.
    • Efficiency: The tool can pre-process data from the API, returning a concise summary to the LLM and preserving its valuable context window.

Journey 2 was chosen because the goal is not just to call an API, but to provide the agent with a reliable, intelligent capability.

The Core Implementation: A Reusable Pattern

The cornerstone of this integration is a single, centralised function: get_credentials. This function lives in agent.py and is called by every tool that needs to access a protected resource.

It elegantly manages the entire lifecycle of an OAuth token within the ADK session by following a clear, four-step process:

  1. Check Cache: It first looks in the session state (tool_context.state) for a valid, cached token.
  2. Refresh: If a token exists but is expired, it uses the refresh token to get a new one and updates the cache.
  3. Check for Auth Response: If no token is found, it checks if the user has just completed the OAuth consent flow using tool_context.get_auth_response().
  4. Request Credentials: If all else fails, it formally requests credentials via tool_context.request_credential(), which signals the ADK runtime to pause and trigger the interactive user flow.

This pattern is completely generic. You can see in the agent.py file how, by simply changing the SCOPES constant, you could reuse this exact function for Google Drive, Calendar, Sheets, or any other Workspace API.

Defining the Agent and Tools

With the authentication pattern defined, building the agent becomes a matter of composition. The agent.py file also defines the “Orchestrator/Worker” structure—a best practice that uses a fast, cheap model (gemini-2.5-flash) for routing and a more powerful, specialised model (gemini-2.5-pro) for analysis.

The Other Half: The Client-Side Auth Flow

The get_credentials function is only half the story. When it calls tool_context.request_credential(), it signals to the runtime that it needs user authentication. The cli.py script acts as that runtime, or “Agent Client.”

The cli.py script is responsible for the user-facing interaction. As you can see in the handle_agent_run function within cli.py, the script’s main loop does two key things:

  1. It iterates through agent events and uses a helper to check for the specific adk_request_credential function call.
  2. When it detects this request, it pauses, prints the authorisation URL for the user, and waits to receive the callback URL.
  3. It then bundles this callback URL into a FunctionResponse and sends it back to the agent, which can then resume the tool call, this time successfully.

This client-side loop is the essential counterpart to the get_credentials pattern.

Production Considerations: Moving Beyond the Workbench

This entire setup is designed as a high-fidelity local workbench. Before moving to a production environment, several changes are necessary:

  • Persistent Sessions: The InMemorySessionService is ephemeral. For production, you must switch to a persistent service like DatabaseSessionService (backed by a database like PostgreSQL) or VertexAiSessionService. This is critical for remembering conversation history and, most importantly, for securely storing the user’s OAuth tokens.
  • Secure Credential Storage: While caching tokens in the session state is acceptable for local testing, production systems should encrypt the token data in the database or use a dedicated secret manager.
  • Runtime Environment: The cli.py would be replaced by a web server framework (like FastAPI or Flask) if you are deploying this as a web application, or adapted to the specific requirements of a platform like Vertex AI Agent Engine.

By building with this modular, Journey 2 approach, you create agents that are not only more capable and robust but are also well-architected for the transition from local development to a production-grade deployment.

Explore the Code

Explore the full source code in the GitHub repository to see these concepts in action. The README.md file provides all the necessary steps to get the agent up and running on your local machine. By combining the conceptual overview in this article with the hands-on code, you’ll be well-equipped to build your own robust, production-ready agents with the Google ADK.

Source: GitHub – mhawksey/ADK-Workspace-User-oAuth-Example