DEV Community

Tanaike for Google Developer Experts

Posted on

Integrating Remote Subagents Built by Google Apps Script with Gemini CLI

fig1a

Abstract

This article explores integrating remote subagents built with Google Apps Script into the Gemini CLI using the Agent-to-Agent (A2A) protocol. It demonstrates how bypassing standard authentication via local agent cards enables seamless execution of complex workflows while effectively overcoming Tool Space Interference (TSI) for massive toolsets.

Introduction

Recently, remote subagent support was introduced to the Gemini CLI. Ref With this feature, the Gemini CLI connects to remote subagents using the Agent-to-Agent (A2A) protocol, expanding its capabilities by delegating tasks to external services. I have previously published several articles discussing the A2A server architecture:

These articles introduce A2A servers built with Google Apps Script (GAS) Web Apps. GAS serves as an ideal foundation for A2A servers because its low-code environment is easily navigable by both human developers and generative AI. Furthermore, GAS is accessible to anyone with a Google account and offers native affinity with Google Workspace. It simplifies complex operations by facilitating seamless OAuth scope authentication and providing direct access to Google APIs via Advanced Google Services.

By utilizing GAS, developers can rapidly deploy lightweight, category-specific agents that communicate via the A2A protocol. This forms a robust mesh of capabilities without the infrastructure overhead of traditional server environments.

An excellent foundational article on the basic methods for using subagents with the Gemini CLI was recently published by Romin Irani. Ref Building on that foundation, this article introduces a specific approach for securely and natively integrating remote subagents built by Google Apps Script Web Apps with the Gemini CLI.

Demonstration

Prerequisites and Repository

This article assumes the following prerequisites:

  • You have installed and configured the latest Gemini CLI.
  • You have an active API key for the Gemini API.

You can view all the sample scripts used in this article at the following repository:https://github.com/tanaikech/gemini-cli-gas-a2a-subagents

1. Basic Integration: Connecting Gemini CLI to a GAS-based A2A Server

This section demonstrates a simple test to confirm the connection between the Gemini CLI and a remote subagent (A2A server) built with Google Apps Script.

1.1 Building the A2A Server

1.1.1 Google Apps Script Preparation

Copy Google Apps Script

Log in to your Google account and access the following URL to view the Google Apps Script project dashboard. Click the copy icon in the top right corner to copy the standalone script to your root folder:https://script.google.com/home/projects/1vcbr7E7XeJafVGdV6QKEgsr9H0-Vl_zLOQSjgysDCs2olWlrE43HGOne

Manual Preparation of Google Apps Script

If you prefer to create the sample A2A server manually, follow these steps:

Create a new standalone Google Apps Script project. Ref

appsscript.json

Overwrite appsscript.json with the following JSON. Adjust timeZone to your local timezone.

{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
    "enabledAdvancedServices": [],
    "libraries": [
      {
        "userSymbol": "MCPA2Aserver",
        "libraryId": "1xRlK6KPhqp374qAyumrtVXXNEtb7iZ_rD7yRuYxccQuYieKCCao9VuB6",
        "version": "0",
        "developmentMode": true
      }
    ]
  },
  "webapp": {
    "executeAs": "USER_DEPLOYING",
    "access": "ANYONE_ANONYMOUS"
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}
Enter fullscreen mode Exit fullscreen mode

MCPA2Aserver is a Google Apps Script library designed to consolidate Generative AI protocols into a single server solution. It enables developers to easily build and deploy servers supporting both the Model Context Protocol (MCP) and the Agent-to-Agent (A2A) protocol. The repository is available athttps://github.com/tanaikech/MCPA2Aserver-GAS-Library.

code.gs

Copy and paste the following script into code.gs. Set your API key for using the Gemini API.

// --- Your variables ---
const object = {
  apiKey: "{apiKey}", // Your API key for using Gemini API.
  model: "models/gemini-3-flash-preview",
  accessKey: "sample", // If you want to use an access key for requesting Web Apps, please use this.
  // logSpreadsheetId: "{spreadsheetId}", // If you use this, the logs are stored to Google Spreadsheet.
};

// --- Entry Points ---
const doGet = (e) => main(e);
const doPost = (e) => main(e);

/**
 * Main Dispatcher Function
 * Routes the request to either A2A handler or MCP handler based on the payload or path.
 *
 * @param {EventObject} e - The event object from doGet/doPost
 * @return {ContentService.TextOutput} The JSON response
 */
function main(e) {
  const context = createServerContext_(); // Load sample tools.
  const m = MCPA2Aserver;
  m.a2a = true;
  m.apiKey = object.apiKey;
  m.model = object.model;
  if (object.accessKey) m.accessKey = object.accessKey;
  if (object.logSpreadsheetId) m.logSpreadsheetId = object.logSpreadsheetId;
  const res = m.main(e, context);
  return res;
}

/**
 * Please get the agent card from the following function.
 */
function getAgentCard() {
  const obj = createServerContext_();
  const agentCard = obj.A2AObj.agentCard();
  const disp = JSON.stringify(agentCard, null, 2)
    .split("\n")
    .map((e) => `  ${e}`)
    .join("\n");
  console.log(disp);
}
Enter fullscreen mode Exit fullscreen mode

agent.gs

Create another script file named agent.gs and paste the following code. Make sure to update the url within agentCard with your Web App URL later.

function createServerContext_() {
  const functions = {
    params_: {
      get_exchange_rate: {
        description: "Use this to get current exchange rate.",
        parameters: {
          type: "object",
          properties: {
            currency_from: {
              type: "string",
              description: "Source currency (major currency). Default is USD.",
            },
            currency_to: {
              type: "string",
              description:
                "Destination currency (major currency). Default is EUR.",
            },
            currency_date: {
              type: "string",
              description:
                "Date of the currency. Default is latest. It should be ISO format (YYYY-MM-DD).",
            },
          },
          required: ["currency_from", "currency_to", "currency_date"],
        },
      },

      get_current_weather: {
        description: [
          "Use this to get the weather using the latitude and the longitude.",
          "At that time, convert the location to the latitude and the longitude and provide them to the function.",
          `The date is required to be included. The date format is "yyyy-MM-dd HH:mm"`,
          `If you cannot know the location, decide the location using the timezone.`,
        ].join("\n"),
        parameters: {
          type: "object",
          properties: {
            latitude: {
              type: "number",
              description: "The latitude of the inputed location.",
            },
            longitude: {
              type: "number",
              description: "The longitude of the inputed location.",
            },
            date: {
              type: "string",
              description: `Date for searching the weather. The date format is "yyyy-MM-dd HH:mm"`,
            },
            timezone: {
              type: "string",
              description: `The timezone. In the case of Japan, "Asia/Tokyo" is used.`,
            },
          },
          required: ["latitude", "longitude", "date", "timezone"],
        },
      },
    },

    /**
     * Ref: https://github.com/google/A2A/blob/main/samples/python/agents/langgraph/agent.py#L19
     */
    get_exchange_rate: (object) => {
      console.log("Run the function get_exchange_rate.");
      console.log(object); // Check arguments.
      const {
        currency_from = "USD",
        currency_to = "EUR",
        currency_date = "latest",
      } = object;
      let res;
      try {
        const resStr = UrlFetchApp.fetch(
          `https://api.frankfurter.app/${currency_date}?from=${currency_from}&to=${currency_to}`,
        ).getContentText();
        const obj = JSON.parse(resStr);
        res = [
          `The raw data from the API is ${resStr}. The detailed result is as follows.`,
          `The currency rate at ${currency_date} from "${currency_from}" to "${currency_to}" is ${obj.rates[currency_to]}.`,
        ].join("\n");
      } catch ({ stack }) {
        res = stack;
      }
      console.log(res); // Check response.
      return {
        mcp: {
          jsonrpc: "2.0",
          result: { content: [{ type: "text", text: res }], isError: false },
        },
        a2a: { result: res },
      };
    },

    /**
     * This function returns the current weather.
     * The API is from https://open-meteo.com/
     *
     * { latitude = "35.681236", longitude = "139.767125", date = "2025-05-27 12:00", timezone = "Asia/Tokyo" } is Tokyo station.
     */
    get_current_weather: (object) => {
      console.log("Run the function get_current_weather.");
      console.log(object); // Check arguments.
      const {
        latitude = "35.681236",
        longitude = "139.767125",
        date = "2025-05-27 12:00",
        timezone = "Asia/Tokyo",
      } = object;
      let res;
      try {
        // Ref: https://open-meteo.com/en/docs?hourly=weather_code&current=weather_code#weather_variable_documentation
        const code = {
          0: "Clear sky",
          1: "Mainly clear, partly cloudy, and overcast",
          2: "Mainly clear, partly cloudy, and overcast",
          3: "Mainly clear, partly cloudy, and overcast",
          45: "Fog and depositing rime fog",
          48: "Fog and depositing rime fog",
          51: "Drizzle: Light, moderate, and dense intensity",
          53: "Drizzle: Light, moderate, and dense intensity",
          55: "Drizzle: Light, moderate, and dense intensity",
          56: "Freezing Drizzle: Light and dense intensity",
          57: "Freezing Drizzle: Light and dense intensity",
          61: "Rain: Slight, moderate and heavy intensity",
          63: "Rain: Slight, moderate and heavy intensity",
          65: "Rain: Slight, moderate and heavy intensity",
          66: "Freezing Rain: Light and heavy intensity",
          67: "Freezing Rain: Light and heavy intensity",
          71: "Snow fall: Slight, moderate, and heavy intensity",
          73: "Snow fall: Slight, moderate, and heavy intensity",
          75: "Snow fall: Slight, moderate, and heavy intensity",
          77: "Snow grains",
          80: "Rain showers: Slight, moderate, and violent",
          81: "Rain showers: Slight, moderate, and violent",
          82: "Rain showers: Slight, moderate, and violent",
          85: "Snow showers slight and heavy",
          86: "Snow showers slight and heavy",
          95: "Thunderstorm: Slight or moderate",
          96: "Thunderstorm with slight and heavy hail",
          99: "Thunderstorm with slight and heavy hail",
        };
        const endpoint = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&hourly=weather_code&timezone=${encodeURIComponent(
          timezone,
        )}`;
        const resObj = UrlFetchApp.fetch(endpoint, {
          muteHttpExceptions: true,
        });
        if (resObj.getResponseCode() == 200) {
          const obj = JSON.parse(resObj.getContentText());
          const {
            hourly: { time, weather_code },
          } = obj;
          const widx = time.indexOf(date.replace(" ", "T").trim());
          if (widx != -1) {
            res = code[weather_code[widx]];
          } else {
            res = "No value was returned. Please try again.";
          }
        } else {
          res = "No value was returned. Please try again.";
        }
      } catch ({ stack }) {
        res = stack;
      }
      console.log(res); // Check response.
      return {
        mcp: {
          jsonrpc: "2.0",
          result: { content: [{ type: "text", text: res }], isError: false },
        },
        a2a: { result: res },
      };
    },
  };

  /**
   * If you want to return the file content to MCP client, please set the return value as follows.
   * {
   *   jsonrpc: "2.0",
   *   result: {
   *    content:[
   *      {
   *        type: "text",
   *         text: "sample text",
   *       },
   *       {
   *         type: "image",
   *         data: "base64 data",
   *         mimeType: "mimetype",
   *       },
   *     ],
   *     isError: false,
   *   }
   * }
   *
   * If you want to return the file content to A2A client, please set the return value as follows.
   * {
   *   result: {
   *     type: "file",
   *     kind: "file",
   *     file: {
   *       name: "filename",
   *       bytes: "base64 data",
   *       mimeType: "mimetype",
   *     },
   *     metadata: null
   *   }
   * }
   *
   */

  // for A2A
  const agentCard = {
    name: "API Manager",
    description: [
      `Provide management for using various APIs.`,
      `- Run with exchange values between various currencies. For example, this answers "What is the exchange rate between USD and GBP?".`,
      `- Return the weather information by providing the location and the date, and the time.`,
    ].join("\n"),
    provider: {
      organization: "Tanaike",
      url: "https://github.com/tanaikech",
    },
    version: "1.0.0",
    url: `https://script.google.com/macros/s/{deploymentId}/exec?accessKey=sample`, // <--- Please replace this to your Web Apps URL.
    defaultInputModes: ["text/plain"],
    defaultOutputModes: ["text/plain"],
    capabilities: {
      streaming: false,
      pushNotifications: false,
      stateTransitionHistory: false,
    },
    skills: [
      {
        id: "get_exchange_rate",
        name: "Currency Exchange Rates Tool",
        description: "Helps with exchange values between various currencies",
        tags: ["currency conversion", "currency exchange"],
        examples: ["What is exchange rate between USD and GBP?"],
        inputModes: ["text/plain"],
        outputModes: ["text/plain"],
      },
      {
        id: "get_current_weather",
        name: "Get current weather",
        description:
          "This agent can return the weather information by providing the location and the date, and the time.",
        tags: ["weather"],
        examples: [
          "Return the weather in Tokyo for tomorrow's lunchtime.",
          "Return the weather in Tokyo for 9 AM on May 27, 2025.",
        ],
        inputModes: ["text/plain"],
        outputModes: ["text/plain"],
      },
    ],
  };

  return {
    A2AObj: { functions: (_) => functions, agentCard: (_) => agentCard },
  };
}
Enter fullscreen mode Exit fullscreen mode

1.1.2 Deploying Google Apps Script as a Web App

  • Open the script editor.
  • Click Deploy > New deployment.
  • Select Web App.
  • Execute as: Me.
  • Who has access: Anyone.
  • Copy the Web App URL.

1.1.3 Configuration

Update the url property inside the agentCard variable in your agent.gs script with your copied Web App URL. Make sure to redeploy the Web App after updating the code to apply the changes.

1.2 Configuring the Gemini CLI

1.2.1 Setting the GAS A2A Server as a Subagent

Despite the advantages of using Google Apps Script, a technical hurdle persists: standard A2A clients cannot natively interact with GAS-based servers automatically. This is because retrieving the agent card dynamically requires a GET request to https://script.google.com/macros/s/{deploymentId}/exec/.well-known/agent.json. Currently, this endpoint requires access token authorization. While Google Application Default Credentials (ADC) can be used for installing the subagents on Gemini CLI, they are restricted to *.googleapis.com and *.run.app endpoints. GAS Web App endpoints are not yet supported for automated retrieval. Ref

Fortunately, remote subagents in the Gemini CLI allow developers to directly define the agent card locally. This elegant workaround bypasses the authentication process entirely and reduces the network overhead of loading the agent card from the GAS Web App.

To generate the agent card JSON, run the getAgentCard() function in your code.gs script. This prints the necessary JSON object to the Apps Script execution console.

Save the obtained JSON to define the subagent in your working directory. Create a file at .gemini/agents/sample-gas-agent.md:

---
kind: remote
name: sample-gas-agent
agent_card_json: |
  {
    "name": "API Manager",
    "description": "Provide management for using various APIs.\n- Run with exchange values between various currencies. For example, this answers \"What is the exchange rate between USD and GBP?\".\n- Return the weather information by providing the location and the date, and the time.",
    "provider": {
      "organization": "Tanaike",
      "url": "https://github.com/tanaikech"
    },
    "version": "1.0.0",
    "url": "https://script.google.com/macros/s/{your deployment ID}/exec?accessKey=sample",
    "defaultInputModes":[
      "text/plain"
    ],
    "defaultOutputModes":[
      "text/plain"
    ],
    "capabilities": {
      "streaming": false,
      "pushNotifications": false,
      "stateTransitionHistory": false
    },
    "skills":[
      {
        "id": "get_exchange_rate",
        "name": "Currency Exchange Rates Tool",
        "description": "Helps with exchange values between various currencies",
        "tags":[
          "currency conversion",
          "currency exchange"
        ],
        "examples":[
          "What is exchange rate between USD and GBP?"
        ],
        "inputModes": [
          "text/plain"
        ],
        "outputModes":[
          "text/plain"
        ]
      },
      {
        "id": "get_current_weather",
        "name": "Get current weather",
        "description": "This agent can return the weather information by providing the location and the date, and the time.",
        "tags":[
          "weather"
        ],
        "examples":[
          "Return the weather in Tokyo for tomorrow's lunchtime.",
          "Return the weather in Tokyo for 9 AM on May 27, 2025."
        ],
        "inputModes": [
          "text/plain"
        ],
        "outputModes":[
          "text/plain"
        ]
      }
    ]
  }
---
Enter fullscreen mode Exit fullscreen mode

1.3 Testing the Connection

Launch the Gemini CLI. The application will detect the new subagent as shown below. Select Acknowledge and Enable to activate it.

fig2a

Running the /agents command in the chat displays the installed subagents. Your installed sample-gas-agent will appear under remote agents.

fig2b

You can also invoke the agent directly using @sample-gas-agent:

fig2c

Input the following sample prompts:

What is exchange rate between USD and GBP?
Enter fullscreen mode Exit fullscreen mode

and

Return the weather in Tokyo for tomorrow's lunchtime.
Enter fullscreen mode Exit fullscreen mode

The following results are returned for each prompt, confirming that the responses were generated securely through the GAS-based sample-gas-agent.

fig2d

When combining these requests into a single prompt:

I’m planning a trip from London to Japan and need to finalize my budget and itinerary; could you tell me the current USD to GBP exchange rate so I can manage my funds, and also let me know tomorrow's lunchtime weather in Tokyo so I can decide whether to book an outdoor terrace for my arrival meal?
Enter fullscreen mode Exit fullscreen mode

fig2e

The subagent successfully processes both requests and generates the correct response.

Alternatively, you can direct a specific prompt exclusively to the subagent by prefixing it:

@sample-gas-agent What is exchange rate between USD and GBP?
Enter fullscreen mode Exit fullscreen mode

2. Advanced Integration: Orchestrating Google Workspace with Subagents

As an enhanced test, we will install the google-workspace-orchestrator subagent, which integrates heavily with Google Workspace to perform complex cross-application tasks.

2.1 Building the Advanced A2A Server

2.1.1 Copying the Google Apps Script

For this advanced setup, copy the following Google Apps Script project by clicking the copy button on the dashboard:https://script.google.com/home/projects/1xIwskiWAychSp3JN25s7AVbpJf9aRlSvCE8C9szIxPnFHZBeeX3Eo7vT

2.1.2 Deploying as a Web App

  • Open the script editor.
  • Click Deploy > New deployment.
  • Select Web App.
  • Execute as: Me.
  • Who has access: Anyone.
  • Copy the Web App URL.

2.1.3 Configuration

Update webAppsUrl within the object variable in the code.gs script. Make sure to redeploy the Web App after updating the code.

2.2 Configuring the Gemini CLI

2.2.1 Setting the Advanced Subagent

To define the agent card for this advanced subagent, run the getAgentCard() function in code.gs. This will create a text file containing the JSON of the agent card directly in your Google Drive root folder.

Create a file named .gemini/agents/google-workspace-orchestrator.md in your working directory and paste the generated JSON:

---
kind: remote
name: google-workspace-orchestrator
agent_card_json: |
  {
    "name": "Google Workspace Orchestrator",
    "description": "This agent acts as a comprehensive interface for the Google Workspace ecosystem and associated Google APIs. It provides extensive capabilities to manage and automate tasks across Gmail (sending, organizing, retrieving), Google Drive (file management, search, permission handling, content generation), and Google Calendar (schedule management). It features deep integration with Google Classroom for managing courses, assignments, and rosters, as well as Google Analytics for reporting. Additionally, it controls Google Docs, Sheets, and Slides for document creation and data manipulation. Advanced features include RAG (Retrieval-Augmented Generation) via File Search stores, image generation, YouTube video summarization, and Google Maps utilities. It serves as a central hub for executing complex workflows involving multiple Google services.",
    "provider": {
      "organization": "Tanaike",
      "url": "https://github.com/tanaikech"
    },
    "version": "1.0.0",
    "url": "https://script.google.com/macros/s/{your deployment ID}/exec?accessKey=sample",
    "defaultInputModes":[
      "text/plain"
    ],
    "defaultOutputModes":[
      "text/plain"
    ],
    "capabilities": {
      "streaming": false,
      "pushNotifications": false,
      "stateTransitionHistory": false
    },
    "skills":[
      // ...
      // 160 skills
      // ...
   ]
  }
---
Enter fullscreen mode Exit fullscreen mode

2.3 Testing the Advanced Workflows

Launch the Gemini CLI and select Acknowledge and Enable for the newly detected google-workspace-orchestrator subagent.

To avoid interference, disable the previous subagent by running the following command in the chat:

/agents disable sample-gas-agent
Enter fullscreen mode Exit fullscreen mode

Running the /agents command will now show that the google-workspace-orchestrator subagent has 160 skills enabled. You can view the full list of detailed skills at https://github.com/tanaikech/ToolsForMCPServer.

 > /agents

Local Agents

  - Codebase Investigator Agent (codebase_investigator)
    The specialized tool for codebase analysis...
  - CLI Help Agent (cli_help)
    Specialized agent for answering questions about the Gemini CLI...
  - Generalist Agent (generalist)
    A general-purpose AI agent with access to all tools...

Remote Agents

  - google-workspace-orchestrator
    Agent Description: This agent acts as a comprehensive interface for the Google Workspace ecosystem...
    Skills:
    .
    .
    160 skills
    .
    .
Enter fullscreen mode Exit fullscreen mode

Test 1: Generating a Cooking Roadmap

Input the following prompt:

I want to cook miso soup.
To achieve this goal, create a new Google Spreadsheet,
generate a roadmap for cooking miso soup in the spreadsheet,
and return the Spreadsheet URL.
Enter fullscreen mode Exit fullscreen mode

The Gemini CLI initiates the task:

fig3a

The requested roadmap is successfully generated and populated into Google Sheets:

fig3b

Test 2: Complex Document Generation and Email Delivery

Input the following prompt:

Write a comprehensive article about developing Google Apps Script (GAS) using generative AI.
The article should include an introductory overview, formatted lists for best practices,
and a table comparing different AI-assisted coding techniques.
Once generated, please create a new Google Document, insert the content, convert the Google Document to a PDF file,
and send an email to `tanaike@hotmail.com` including the shareable URL of the PDF file by giving a suitable title and email body.
Enter fullscreen mode Exit fullscreen mode

The Gemini CLI coordinates multiple tools to execute the workflow:

fig3c

The following PDF file is created from the generated Google Document:

fig3d

Finally, an email containing the PDF link is sent securely:

fig3e

3. Architectural Advantage: Overcoming Tool Space Interference (TSI)

The advanced task demonstrated above requires executing multiple parallel processes natively through the google-workspace-orchestrator subagent, which contains an astonishing 160 distinct skills.

When this massive number of skills is loaded directly into a single MCP server or a standard AI context window, it frequently causes a critical challenge known as Tool Space Interference (TSI). TSI is a phenomenon where verbose metadata saturates the context window, severely degrading the AI's reasoning accuracy and frequently causing logic failures or hallucinations. Ref Ref Current industry guidelines suggest a “soft limit” of 20 functions per agent to maintain stability.

To mitigate TSI, I previously proposed Nexus-MCP. Ref Nexus-MCP functions as a centralized gateway employing a deterministic four-phase workflow to map and filter tools. While highly effective, Nexus-MCP relies on a single AI agent acting as the client, making it less suitable for true distributed task execution where specific tool categories should be handled by specialized agents.

As an alternative approach, I proposed using an A2A server architecture. Ref At the time, this required utilizing a heavily modified @a2a-js/sdk because the agent card could not be natively retrieved from GAS Web Apps.

A major merit of the integration method introduced in this article is its ability to bypass TSI entirely while using official tools. By directly defining the agent card locally in .gemini/agents/google-workspace-orchestrator.md, this architecture functions natively within the Gemini CLI. It elegantly avoids TSI by delegating the vast tool execution space entirely to the remote A2A subagent, thereby preserving the main CLI agent's reasoning capacity, stability, and speed.

Summary

  • The Gemini CLI now supports remote subagents via the A2A protocol, allowing developers to safely extend and delegate capabilities.
  • Google Apps Script (GAS) serves as an ideal, accessible backend for these subagents due to its low-code environment and native integration with Google Workspace.
  • Defining the agent card locally in the Gemini CLI easily bypasses the authentication hurdles typically associated with retrieving metadata dynamically from GAS Web Apps.
  • This architectural pattern resolves Tool Space Interference (TSI) by offloading massive toolsets (like 160+ Google Workspace skills) to dedicated remote agents.
  • Consequently, developers can reliably execute complex, multi-step operations without degrading the reasoning capacity or token limits of the main AI agent.

Top comments (0)