<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Corsair</title>
    <description>The latest articles on DEV Community by Corsair (@corsairdev).</description>
    <link>https://dev.to/corsairdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3902355%2F9c351fc1-c5ca-40ee-a035-44f6816eda6f.jpg</url>
      <title>DEV Community: Corsair</title>
      <link>https://dev.to/corsairdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/corsairdev"/>
    <language>en</language>
    <item>
      <title>How to Provide SaaS Integrations to AI Agents: Sharing Integration Logic Across Products, Workflows, and MCP Tools</title>
      <dc:creator>Corsair</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:48:31 +0000</pubDate>
      <link>https://dev.to/corsairdev/how-to-provide-saas-integrations-to-ai-agents-sharing-integration-logic-across-products-ppb</link>
      <guid>https://dev.to/corsairdev/how-to-provide-saas-integrations-to-ai-agents-sharing-integration-logic-across-products-ppb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3kzz7598jdfhrj9hb9eg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3kzz7598jdfhrj9hb9eg.png" alt=" " width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
Most SaaS teams building an AI agent run into the same wall.&lt;/p&gt;

&lt;p&gt;The agent needs to send an email, update a record, or post a message, and that logic already exists somewhere in the product. It lives in a button handler, a background job, or a webhook processor.&lt;/p&gt;

&lt;p&gt;The instinct is to write a second version of that logic just for the agent, wrapped in a tool definition.&lt;/p&gt;

&lt;p&gt;That instinct is what causes the mess: two code paths for the same business operation, two places to fix a bug, and two sets of permission rules that quietly drift apart.&lt;/p&gt;

&lt;p&gt;This guide walks through how to provide SaaS integrations to AI agents without duplicating that logic.&lt;/p&gt;

&lt;p&gt;You will see how to structure integration code so a single service layer powers your product's UI, your background workflows, and your MCP tools at the same time, plus how to keep context, permissions, and error handling consistent no matter which entry point triggered the call.&lt;/p&gt;
&lt;h2&gt;
  
  
  How SaaS Products and AI Agents Can Share the Same Integration Logic
&lt;/h2&gt;

&lt;p&gt;The starting point is recognizing that an AI agent is just a third caller of code you already wrote.&lt;/p&gt;

&lt;p&gt;It is not a separate product.&lt;/p&gt;

&lt;p&gt;Once you treat it that way, the architecture question becomes simple: how do you expose one set of operations to three different callers?&lt;/p&gt;
&lt;h3&gt;
  
  
  Separate Business Operations From the Interfaces That Trigger Them
&lt;/h3&gt;

&lt;p&gt;Every integration action—sending a message, creating a record, updating a contact—is really two things bundled together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The business operation itself.&lt;/li&gt;
&lt;li&gt;The interface that triggered it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A product button, a scheduled workflow step, and an agent tool call are all just triggers.&lt;/p&gt;

&lt;p&gt;The operation underneath, such as &lt;code&gt;create an invoice&lt;/code&gt; or &lt;code&gt;post a Slack message&lt;/code&gt;, should not know or care which trigger fired it.&lt;/p&gt;

&lt;p&gt;When teams skip this separation, the tool definition ends up holding validation logic, credential lookups, and formatting rules that also exist in the API route.&lt;/p&gt;

&lt;p&gt;Any change now needs to happen twice, and the two versions inevitably fall out of sync within a few sprints.&lt;/p&gt;

&lt;p&gt;The fix is to write each business operation once, as a plain function or method with a defined input and output, and have every interface call into it.&lt;/p&gt;

&lt;p&gt;The interface layer becomes thin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parse the request.&lt;/li&gt;
&lt;li&gt;Call the operation.&lt;/li&gt;
&lt;li&gt;Format the response for that surface.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Connect Product Buttons, Background Workflows, and Agent Tools to One Shared Service
&lt;/h3&gt;

&lt;p&gt;In practice, this means your integration service exposes a consistent calling pattern regardless of caller.&lt;/p&gt;

&lt;p&gt;A minimal example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createInvoiceRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;integrationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;billing.invoices.create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Triggered by a product button&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createInvoiceRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Triggered by a background workflow step&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createInvoiceRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workflowContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Triggered by an MCP tool call&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createInvoiceRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agentContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toolArgs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function signature never changes.&lt;/p&gt;

&lt;p&gt;Only the &lt;code&gt;context&lt;/code&gt; object does, since each caller carries different information about who or what initiated the request.&lt;/p&gt;

&lt;p&gt;This is the same principle behind Corsair's plugin API, where every registered integration exposes operations through one typed method structure, so a &lt;a href="https://docs.corsair.dev/concepts/api" rel="noopener noreferrer"&gt;button handler, a workflow step, and an agent's tool call can all invoke the same underlying method&lt;/a&gt; instead of three separate implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Provide SaaS Integrations to AI Agents Through MCP
&lt;/h2&gt;

&lt;p&gt;Once your business operations are unified behind one service layer, exposing them to an AI agent through MCP becomes a mapping exercise rather than a rewrite.&lt;/p&gt;

&lt;p&gt;MCP just needs a way to describe which operations exist and how to call them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choose Which Business Operations to Expose as Tools
&lt;/h3&gt;

&lt;p&gt;Not every internal method belongs in front of an agent.&lt;/p&gt;

&lt;p&gt;A good filter is risk level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read operations:&lt;/strong&gt; Listing records and fetching a contact are generally safe to expose broadly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write operations:&lt;/strong&gt; Creating a ticket or updating a field should be scoped to the accounts and actions a given agent context is allowed to touch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destructive operations:&lt;/strong&gt; Deleting a repository or canceling a subscription usually needs a gate in front of it rather than a blanket expose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rather than hand-writing a static tool list and maintaining it by hand, some MCP setups expose a discovery layer instead.&lt;/p&gt;

&lt;p&gt;Corsair's &lt;a href="https://docs.corsair.dev/mcp-adapters/mcp-adapters" rel="noopener noreferrer"&gt;MCP adapters generate three standard tools automatically for every connected plugin: one that lists available operations, one that returns the schema for a specific operation, and one that executes it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The agent discovers what it can do at runtime, and new operations become available to it the moment they are added to the service layer, with no tool definitions to hand-write.&lt;/p&gt;

&lt;h3&gt;
  
  
  Map Tool Inputs and Outputs to Existing Integration Methods
&lt;/h3&gt;

&lt;p&gt;Whichever approach you take, the tool schema should map directly onto the existing method signature rather than inventing a parallel shape.&lt;/p&gt;

&lt;p&gt;If your internal method takes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ customerId, amount, dueDate }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the tool's input schema should describe those same three fields, not a reworded or restructured version of them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"billing_invoices_create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Create an invoice for a customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"input_schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dueDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output should be equally faithful to what the underlying method already returns.&lt;/p&gt;

&lt;p&gt;Reformatting data specifically for the agent is where subtle bugs creep in, since the transformation logic now exists nowhere else and gets no test coverage from your product's normal request path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve Execution Context Across Product Requests and Agent Actions
&lt;/h2&gt;

&lt;p&gt;A shared service layer only works if every call carries enough context to be executed correctly and safely, no matter which surface it came from.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identify the Requester, Executing Service, Tenant, and Connected Account
&lt;/h3&gt;

&lt;p&gt;Four pieces of context matter on every call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who is asking:&lt;/strong&gt; An end user clicking a button, a workflow engine acting on a schedule, or an agent acting on a user's behalf.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is executing:&lt;/strong&gt; Which service, worker, or agent runtime is actually making the call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which tenant owns the data:&lt;/strong&gt; The account or workspace the operation should be scoped to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which connected account holds the credentials:&lt;/strong&gt; Since a tenant may have several connected accounts for the same provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Multi-tenant setups need this resolved consistently across all three entry points, or a background job risks reading one tenant's data while a product route reads another's.&lt;/p&gt;

&lt;p&gt;Corsair handles this by requiring every call to pass through a tenant scoping function, so that &lt;a href="https://docs.corsair.dev/concepts/multi-tenancy" rel="noopener noreferrer"&gt;"a database ID or auth provider ID"&lt;/a&gt; determines which credentials and data the call can touch, whether that call originates from a UI action, a webhook, or an agent's tool invocation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Carry Request Context Into Background Jobs and Retries
&lt;/h3&gt;

&lt;p&gt;Context tends to get lost the moment work moves off the original request thread.&lt;/p&gt;

&lt;p&gt;A product route has the tenant ID and actor readily available, but once that same operation is handed to a queue for retrying, it is easy to drop that information and rebuild it from scratch, sometimes incorrectly.&lt;/p&gt;

&lt;p&gt;The safer pattern is to serialize the full context object—tenant, actor, originating surface—alongside the job payload itself, so a retry three hours later executes with the exact same identity and permissions as the original call.&lt;/p&gt;

&lt;p&gt;This matters even more for agent-triggered actions, since an approval or a long-running workflow may resume well after the initiating chat session has ended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply Consistent Permissions and Business Rules Across Every Entry Point
&lt;/h2&gt;

&lt;p&gt;Once context is reliable, permissions can be enforced in one place rather than reimplemented per interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforce Shared Validation, Authorization, and Approval Requirements
&lt;/h3&gt;

&lt;p&gt;Validation and authorization checks belong inside the shared service layer, not inside each interface.&lt;/p&gt;

&lt;p&gt;If a product route checks that a user owns a record before updating it, that same check needs to run when a workflow or an agent triggers the identical update.&lt;/p&gt;

&lt;p&gt;A useful pattern here is tiering operations by risk and mapping each tier to a policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allow immediately.&lt;/li&gt;
&lt;li&gt;Allow with a background audit log.&lt;/li&gt;
&lt;li&gt;Require human approval before execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Applying that tiering inside the service layer means a destructive action gets the same scrutiny whether a person clicked delete or an agent decided to call it.&lt;/p&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reads&lt;/strong&gt; generally proceed without friction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes&lt;/strong&gt; may proceed but get logged for review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destructive actions&lt;/strong&gt; pause for a human decision before they run, and that pause should apply equally regardless of which interface asked for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Resolve Provider Credentials From Verified Account Context
&lt;/h3&gt;

&lt;p&gt;Credential resolution should happen inside the service layer using the verified tenant and account context, never inside the interface code, and never inside the agent's reasoning loop.&lt;/p&gt;

&lt;p&gt;An agent should be able to call &lt;code&gt;send an email&lt;/code&gt; without ever seeing an OAuth token or an API key.&lt;/p&gt;

&lt;p&gt;The service layer looks up the correct credential for the resolved tenant, uses it for that single call, and returns only the result.&lt;/p&gt;

&lt;p&gt;This also limits blast radius if a tool call goes wrong.&lt;/p&gt;

&lt;p&gt;Since the agent only ever sees method names, parameters, and results, a misbehaving prompt or a compromised session cannot exfiltrate a raw credential, because the credential was never exposed to it in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle Retries, Errors, and Results Across Shared Integrations
&lt;/h2&gt;

&lt;p&gt;Sharing one service layer across three entry points means failure handling has to work for all three, even though each one reacts to failure differently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prevent Duplicate Actions With Shared Idempotency Controls
&lt;/h3&gt;

&lt;p&gt;Retries are unavoidable.&lt;/p&gt;

&lt;p&gt;Networks drop, workflows re-run failed steps, and agents sometimes call a tool twice when a response is slow.&lt;/p&gt;

&lt;p&gt;Without an idempotency layer, a &lt;code&gt;send invoice&lt;/code&gt; operation can fire twice for the same customer.&lt;/p&gt;

&lt;p&gt;A simple approach is to derive a deterministic key from the operation name, its arguments, and the tenant, and check for a matching in-flight or completed record before executing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is close to how Corsair handles pending permission requests: a repeated call with the same plugin, endpoint, arguments, and tenant returns the existing record instead of creating a second one, so an agent retrying a blocked action does not accidentally queue duplicate approvals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adapt Errors and Completion Results for Product Interfaces, Workflows, and Agents
&lt;/h3&gt;

&lt;p&gt;The error itself should be generated once, in a consistent shape, then adapted per consumer at the very last step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;product interface&lt;/strong&gt; turns it into a toast or an inline form error.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;workflow engine&lt;/strong&gt; turns it into a retry decision, a backoff delay, or a dead-letter entry.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;agent&lt;/strong&gt; turns it into a plain-language explanation the end user can act on, sometimes including a link if the failure requires a human decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping the error's origin and shape consistent across all three means your logs, alerts, and support tooling only need to understand one error taxonomy, not three overlapping ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test and Trace AI Agent SaaS Integrations Across All Three Entry Points
&lt;/h2&gt;

&lt;p&gt;The final piece is making sure the shared layer actually behaves the same way no matter which surface exercises it, and that you can follow any single action back to its source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Consistent Business Outcomes and Permission Enforcement
&lt;/h3&gt;

&lt;p&gt;Write tests that call the same business operation through each entry point and assert on the same outcome:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A product route call and an agent tool call against the same operation should produce identical database state.&lt;/li&gt;
&lt;li&gt;A destructive action should require approval regardless of whether a workflow or an agent triggered it.&lt;/li&gt;
&lt;li&gt;A revoked or missing credential should fail the same way, with the same error shape, from all three callers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of test catches the exact drift that duplicated logic tends to produce, usually the moment someone patches one code path and forgets the other two exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trace Each Action From Its Original Request to the Provider API
&lt;/h3&gt;

&lt;p&gt;Every call should carry a correlation ID from the moment it is triggered, whether that is a button click, a workflow tick, or an agent's decision to call a tool, all the way through to the actual provider API request and back.&lt;/p&gt;

&lt;p&gt;That single ID should show up in your logs at each hop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The interface.&lt;/li&gt;
&lt;li&gt;The service layer.&lt;/li&gt;
&lt;li&gt;The credential resolution step.&lt;/li&gt;
&lt;li&gt;The outbound API call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When something goes wrong three days later and a customer asks why an email never sent, this is what lets you answer in minutes instead of guessing.&lt;/p&gt;

&lt;p&gt;It also gives you an audit trail for anything an agent did autonomously, which matters both for debugging and for compliance conversations.&lt;/p&gt;

&lt;p&gt;Getting all of this right from scratch—tenant scoping, permission tiers, credential isolation, MCP tool generation—is a lot of infrastructure to build before you ship a single integration.&lt;/p&gt;

&lt;p&gt;Corsair packages this pattern as an open-source layer you run inside your own app, so the same integration works from a product button, a background workflow, and an agent's MCP tool call without three separate implementations to maintain.&lt;/p&gt;

&lt;p&gt;You can see how it fits together, self-hosted or through the hosted Hub, at &lt;a href="https://corsair.dev/" rel="noopener noreferrer"&gt;corsair.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the difference between a direct SaaS integration and an MCP-based AI agent integration?
&lt;/h3&gt;

&lt;p&gt;A direct integration is called explicitly from your own code, a button handler or a workflow step, using a fixed method and parameters you wrote in advance.&lt;/p&gt;

&lt;p&gt;An MCP-based integration is called by an agent that discovers available operations at runtime and decides which one to invoke based on a plain-language instruction.&lt;/p&gt;

&lt;p&gt;The underlying operation can be the exact same function in both cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do AI agents need direct access to API keys to use SaaS integrations?
&lt;/h3&gt;

&lt;p&gt;No, and they generally should not.&lt;/p&gt;

&lt;p&gt;A well-structured integration layer resolves credentials internally based on verified tenant and account context, then returns only the result of the call.&lt;/p&gt;

&lt;p&gt;The agent works with method names and parameters, never the raw token or key used to authenticate with the provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do permissions work when an AI agent tries to perform a destructive action?
&lt;/h3&gt;

&lt;p&gt;Most integration layers tier operations by risk, typically read, write, and destructive, and map each tier to a policy.&lt;/p&gt;

&lt;p&gt;A destructive action, like deleting a record, is usually set to require human approval before it executes, regardless of whether an agent, a workflow, or a person initiated it.&lt;/p&gt;

&lt;p&gt;The action stays pending until someone approves or denies it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can workflow automations and AI agents share the same integration code?
&lt;/h3&gt;

&lt;p&gt;Yes, and they should.&lt;/p&gt;

&lt;p&gt;Both are just different triggers calling into the same business operation.&lt;/p&gt;

&lt;p&gt;As long as the operation accepts a context object describing who is calling and on whose behalf, a workflow engine and an agent's MCP tool call can invoke the identical underlying method.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is idempotency and why does it matter for AI agent tool calls?
&lt;/h3&gt;

&lt;p&gt;Idempotency means a repeated call with the same parameters produces the same result instead of repeating the side effect.&lt;/p&gt;

&lt;p&gt;It matters for agents specifically because retries are common. A slow response can lead an agent to call a tool twice, and without an idempotency check that can mean sending the same email or creating the same record more than once.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Prevent Infinite Loops in Bidirectional API Syncs</title>
      <dc:creator>Corsair</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:28:06 +0000</pubDate>
      <link>https://dev.to/corsairdev/how-to-prevent-infinite-loops-in-bidirectional-api-syncs-3i2f</link>
      <guid>https://dev.to/corsairdev/how-to-prevent-infinite-loops-in-bidirectional-api-syncs-3i2f</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpymmbzafqww1iq94d1k1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpymmbzafqww1iq94d1k1.png" alt=" " width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
A sales rep updates a phone number in the CRM. Thirty seconds later, the same field has been rewritten four times, three systems are stuck firing events at each other, and nobody on the team can say why the sync queue keeps climbing. If you have built or maintained a bidirectional integration between two systems of record, this probably looks familiar.&lt;/p&gt;

&lt;p&gt;Bidirectional API syncs are one of the more deceptively hard problems in API integration. Two systems, each with their own webhooks, each capable of writing back to the other, sound simple on a whiteboard. In production, a single genuine edit can spiral into dozens of redundant writes, wasted API calls, and in the worst cases a loop that never settles on its own.&lt;/p&gt;

&lt;p&gt;This post walks through why infinite loops happen in bidirectional syncs, why deduplication and retry logic alone will not stop them, and the change ownership, origin metadata, and value comparison strategies that do. These are the same best practices for building reliable API integrations more broadly, not just a fix for this one failure mode. Whether you are designing a new API and data integration from scratch or debugging one that already misbehaves, the goal by the end of this is the same: writes that happen once, settle, and stop.&lt;/p&gt;
&lt;h2&gt;
  
  
  How One CRM Update Can Spiral Into an Infinite Sync Loop
&lt;/h2&gt;

&lt;p&gt;Picture two systems kept in sync by a middle layer, call it System A (a CRM) and System B (an ERP), connected through a sync engine that listens for change events on both sides. Here is the exact chain that turns one legitimate edit into a loop.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A user changes a phone number in System A. That edit fires a change data capture event, often shortened to CDC, which the sync engine picks up as a new fact to propagate.&lt;/li&gt;
&lt;li&gt;The sync engine calls System B's API and writes the new phone number.&lt;/li&gt;
&lt;li&gt;System B commits the write. Because System B has its own event pipeline (a webhook, a CDC stream, an audit log listener), that commit emits a change event of its own. System B has no way of knowing the change originated from an integration rather than a human.&lt;/li&gt;
&lt;li&gt;The sync engine, listening to System B's events the same way it listens to System A's, sees this new event and treats it as a fresh instruction: propagate the change back to System A.&lt;/li&gt;
&lt;li&gt;System A receives the write, commits it, and its own event pipeline fires again.&lt;/li&gt;
&lt;li&gt;The loop repeats, sometimes settling after a couple of bounces, sometimes continuing indefinitely if formatting or enrichment logic changes the value slightly on every pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The root cause is rarely a broken API call or a flaky network. It is that nothing in the pipeline checks whether an incoming change actually originated from the integration itself. Every event, whether caused by a human typing into a form or by the sync engine's own previous write, looks identical by the time it reaches the listener. Without a way to tell those two cases apart, the sync engine cannot help but treat its own echo as new work.&lt;/p&gt;
&lt;h2&gt;
  
  
  Duplicate Delivery, Retried Writes, and Echo Events Aren't the Same Failure
&lt;/h2&gt;

&lt;p&gt;It is tempting to lump every repeated event under one label and reach for one fix. Three distinct failure modes get confused with each other constantly, and only one of them causes infinite loops.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate delivery&lt;/strong&gt;: the same event, carrying the same transaction ID, redelivered by a message broker or webhook provider that guarantees at least once delivery rather than exactly once. Nothing in the underlying system has changed. Track the event ID you have already processed and drop anything you see twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retried write&lt;/strong&gt;: your own sync engine resending an HTTP call because the first attempt timed out or returned a retryable error. The destination may have actually applied the first write, so a naive retry risks a second one. Conditional headers such as ETag checks, along with idempotency keys on the write endpoint itself, catch this cleanly by making the second attempt harmless when the state has not moved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Echo event&lt;/strong&gt;: a genuinely new, freshly committed transaction on the target system, triggered by the sync engine's own write. It has a new transaction ID, a new timestamp, and a new payload. Deduplication will not catch it, because it is not a duplicate. Idempotency keys will not catch it, because it is not a retried write.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction matters because teams often spend weeks hardening how incoming webhook events are routed and verified or adding retry logic with backoff, watch the duplicate and retry problems disappear, and are then confused when the sync still loops. Those fixes were solving a different problem. Echo events need a different kind of defense, covered next.&lt;/p&gt;
&lt;h2&gt;
  
  
  Assigning Change Ownership When Systems Share a Record
&lt;/h2&gt;

&lt;p&gt;One of the most effective structural fixes has nothing to do with detecting echoes after the fact. It is deciding, up front, which system is allowed to change which fields, so an incoming event that touches a field it does not own can be ignored outright.&lt;/p&gt;

&lt;p&gt;Whole record ownership, where one system is declared the single source of truth for an entire object, sounds clean but rarely survives real data. A CRM and an ERP both have legitimate reasons to touch a shared customer record: the CRM owns the sales relationship (name, phone, lifecycle stage), while the ERP owns anything downstream of a signed contract (billing address, payment terms, tax ID). Forcing one system to own the whole record either strips the other of fields it genuinely needs to edit, or invites the exact write back that starts the loop in section one.&lt;/p&gt;

&lt;p&gt;Field level ownership is more precise. Instead of asking which system owns this record, you ask which system owns this field, and only propagate a change when the field being changed actually belongs to the receiving system's list. In practice this is implemented with a changedFields array attached to every event, listing exactly which fields were touched rather than sending the full record on every change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recordId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cust_48213"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changedFields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lifecycleStage"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crm"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before the sync engine writes anything to System B, it checks changedFields against the fields System B is allowed to own. If none match, there is nothing to propagate, and the event is dropped without a single API call. A write that never happens cannot trigger an echo.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. What Origin Tracking Metadata Can (and Can't) Tell You
&lt;/h2&gt;

&lt;p&gt;Field ownership handles the case where systems cleanly divide responsibility. It does not help when both systems legitimately need to write to the same field, which is where origin tracking comes in.&lt;/p&gt;

&lt;p&gt;The idea behind a changeOrigin field is simple: every write your sync engine makes carries a marker identifying who made it, typically a client ID, an integration version, and sometimes a request ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recordId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cust_48213"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+1 555 0134"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changeOrigin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"client"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sync_engine_v3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"actor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integration"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the sync engine later receives an event and sees a changeOrigin matching its own client ID, it can safely conclude this is its own echo and stop, rather than propagating the change back to where it came from. In theory this closes the loop cleanly. In practice it has three limitations.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Often blank&lt;/strong&gt;: many APIs only populate an origin or actor field when the calling client explicitly sets a custom header on the write, and it is easy to miss that step on endpoints the sync engine calls infrequently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gets stripped&lt;/strong&gt;: some destination systems do not store arbitrary metadata on the record, so a write that includes changeOrigin on the way in comes back out through the webhook or CDC stream with no trace of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can't be the sole defense&lt;/strong&gt;: it is a fast, cheap first check, not something you can rely on for every provider, every event type, and every edge case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point raises an obvious question: what do you do when the destination does not preserve origin metadata at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catching Your Own Echo When the Destination Doesn't Preserve Origin Metadata
&lt;/h2&gt;

&lt;p&gt;When origin metadata is missing or unreliable, the fallback is to compare values instead of trusting labels. Before writing an incoming change to System A, the sync engine checks whether the incoming value actually differs from what System A currently holds. If the value is already +1 555 0134 and the incoming echo says +1 555 0134, there is nothing to write, so the sync engine moves on without an API call, and never generates a new event to feed back into System B.&lt;/p&gt;

&lt;p&gt;This sounds straightforward but the implementation needs care in two places.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normalize before comparing&lt;/strong&gt;: "+1 555 0134", "1 555 0134", and "(555) 013 4" are the same value to a human and different strings to a naive equality check. Dates, currency amounts, and whitespace need the same treatment, or you end up writing back values that are functionally identical but fail the comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the timing&lt;/strong&gt;: a value check has to avoid silently swallowing a legitimate concurrent edit. If a second user changes the same field microseconds after the echo arrives, comparing against a value cached earlier in the pipeline can drop a change that should have gone through. Read the current value as close to the write as your database or API allows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is worth restating why deduplication and idempotency keys, the tools from section two, do not solve this on their own. Every echo is a fresh, validly committed transaction with its own transaction ID. It is not a replay and not a retried write, so none of that machinery ever fires. Value comparison sits closer to the write itself, and it is the layer that actually intercepts an echo once metadata has already failed to.&lt;/p&gt;

&lt;p&gt;If you are building this on an integration layer that exposes a hook before a webhook driven write goes through, this comparison is the natural place to put it. A before hook that can inspect the payload and skip processing entirely means the no effect decision happens in one place, next to the write itself, instead of being scattered across every consumer of the event. Storing the current value somewhere queryable, such as a synced entity table kept fresh by every API call and webhook, is what makes reading the current value right before deciding practical instead of theoretical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Concurrent Edits and Verifying Systems Actually Reach a Stable State
&lt;/h2&gt;

&lt;p&gt;Everything so far assumes one change moving through the pipeline at a time. Real systems have two people editing the same record within seconds of each other, and the sync engine needs a rule for who wins that does not also reintroduce a loop. Three approaches handle this, and each one catches a different failure while leaving a different gap.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ownership boundaries&lt;/strong&gt;: the field level approach from section three prevents the conflict from existing at all for any field only one system can touch. This is the strongest guarantee available, but it says nothing about a field like status or notes that both systems have a legitimate reason to write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version or ETag checks&lt;/strong&gt;: every write includes the version number it last read. If System B tries to write using version 3 but the current version is already 4, the write is rejected and the sync engine reads the latest state again before retrying. This catches real conflicts precisely, but it depends entirely on the destination supporting and returning version numbers, which not every API does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timestamp based decisions (last write wins)&lt;/strong&gt;: compare when each change happened and let the most recent one stand. This is the easiest to implement and works with almost any system, since most APIs return an updated timestamp by default. Its weakness is clock skew: if the two systems' clocks are not tightly synchronized, most recent can be wrong, and a genuinely newer change can lose to an older one that simply arrived with a later local timestamp.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most reliable syncs combine these rather than picking one: ownership boundaries remove as many fields from the conflict question as possible, version checks handle the fields both systems can touch wherever supported, and timestamp comparison is the fallback everywhere else.&lt;/p&gt;

&lt;p&gt;None of this matters if you cannot verify it works, which is where stable state testing comes in. Make one legitimate change in System A and watch what happens across both systems afterward.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A stable integration propagates the change to System B exactly once, and after that single propagation, no further events fire on either side.&lt;/li&gt;
&lt;li&gt;An unstable integration keeps generating events: the value bounces between two states, or the same value gets written repeatedly with no functional change, or event counts on your monitoring dashboard never return to zero after one edit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running this test deliberately, for every field and every direction, before a bidirectional sync goes into production, catches loops in staging instead of in a support ticket.&lt;/p&gt;

&lt;p&gt;Every pattern covered here (change ownership, origin metadata, value comparison, and conflict resolution) comes down to the same discipline: know whether a change is genuinely new before you act on it. That discipline is exactly what an integration layer should handle for you, rather than something every team rebuilds inside its own sync engine. &lt;a href="https://corsair.dev" rel="noopener noreferrer"&gt;Corsair&lt;/a&gt; is built as that layer, with webhook routing, retry handling, and a database that stays fresh on every API call and webhook as part of the SDK itself, so the logic above sits on top of a foundation instead of replacing one you have not built yet. If your team is wiring up a new API integration or hardening a bidirectional sync that already misbehaves, it is worth seeing how much of this Corsair already handles.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the difference between an infinite sync loop and normal duplicate webhook delivery?
&lt;/h3&gt;

&lt;p&gt;Duplicate delivery means the same event, with the same transaction ID, arrives more than once, with no new state change, and is solved by tracking event IDs and dropping repeats. An infinite loop is caused by echo events, which are new, validly committed transactions triggered by the sync engine's own earlier write. Because each one has a new transaction ID, event ID based deduplication never recognizes them as anything other than fresh work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can idempotency keys alone prevent bidirectional sync loops?
&lt;/h3&gt;

&lt;p&gt;No. Idempotency keys protect against retried writes, where your own sync engine resends the same request after a timeout, making sure a retry does not create a second write. They do nothing for echo events, since an echo is not a retry of a request you sent, it is a brand new event coming from the destination in response to a write that already succeeded. Preventing loops needs origin tracking or value comparison on top of idempotency, not instead of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you decide which system owns a shared field like phone number or billing address?
&lt;/h3&gt;

&lt;p&gt;Assign ownership at the field level rather than the whole record. Pick the system where the field is edited in the normal course of work, for example the CRM for contact details and the ERP for billing information, and only allow writes to that field from that system. Attach a changedFields array to every event so the receiving system can check whether an incoming change actually touches a field it owns, and skip it entirely if it does not.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should you do when a destination API does not return version numbers or ETags?
&lt;/h3&gt;

&lt;p&gt;Fall back to value comparison. Read the current value of the field immediately before deciding whether to write, normalize both values so equivalent values compare as equal, and skip the write if nothing has actually changed. This will not resolve every concurrent edit as precisely as version checks would, but it stops the specific failure that causes loops, which is writing back a value that never actually changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you test whether a bidirectional integration is actually stable before shipping it?
&lt;/h3&gt;

&lt;p&gt;Make one legitimate change on one side and watch both systems' event activity afterward. A stable integration propagates that change exactly once and then goes quiet. If you see the value bounce, or the same write repeat with no functional change, or event counts that never return to zero, the integration is not stable yet. Running this check for every field and direction in staging, before production traffic hits it, is far cheaper than debugging a live loop.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Connect Multiple MCP Servers to One AI Agent</title>
      <dc:creator>Corsair</dc:creator>
      <pubDate>Fri, 18 Sep 2026 14:09:53 +0000</pubDate>
      <link>https://dev.to/corsairdev/how-to-connect-multiple-mcp-servers-to-one-ai-agent-2o65</link>
      <guid>https://dev.to/corsairdev/how-to-connect-multiple-mcp-servers-to-one-ai-agent-2o65</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fauesb7xug3y0s1ehx0vz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fauesb7xug3y0s1ehx0vz.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
An agent that can only talk to one system is basically a chatbot with extra steps. The moment a task needs data from two places, pull a row from Postgres and open a GitHub issue about it, check a Slack thread and update a Notion page, a single MCP server stops being enough.&lt;/p&gt;

&lt;p&gt;This guide breaks down what connecting multiple MCP servers to one agent actually involves: how an MCP client aggregates and routes calls across several servers at once, two practical ways to wire it up yourself, and the two problems that show up as soon as you add a third or fourth server: tool name collisions and credential sprawl.&lt;/p&gt;

&lt;p&gt;By the end you will know how to connect several MCP servers to one agent, share that setup cleanly across projects, and where an integration layer can take the maintenance off your plate.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Does It Mean to Connect Multiple MCP Servers to One Agent?
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol is an open standard that lets an LLM discover and call tools exposed by a separate process, called a server, over a consistent interface.&lt;/p&gt;

&lt;p&gt;A server might wrap a single API, such as GitHub, Slack, or a database, or a whole toolkit, such as a filesystem or browser.&lt;/p&gt;

&lt;p&gt;"Connecting multiple MCP servers to one agent" means giving a single agent, whether that agent runs inside Claude Code, Cursor, Claude Desktop, or your own app, simultaneous access to the tools from more than one server at the same time, so it can reason across systems in the same conversation without you rewriting the integration for every new task.&lt;/p&gt;

&lt;p&gt;From the agent's point of view, there is no hard line between one server and many. The MCP client sits between the model and every connected server, merges each server's tool list into a single combined set, and routes every call back to the process that owns it.&lt;/p&gt;

&lt;p&gt;The agent does not need to know how many servers sit behind that list, only what each tool does and what arguments it takes. That merging layer is the whole trick, and it is also where most of the practical problems in this guide come from.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Run Multiple MCP Servers for a Single AI Agent?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real tasks cross more than one system:&lt;/strong&gt; "Reply to this issue and post the summary in Slack" needs GitHub and Slack in the same turn. Single-purpose agents rarely stay single-purpose for long.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each server specializes cleanly:&lt;/strong&gt; A filesystem server, database server, and ticketing server can each be maintained, updated, and swapped independently without touching the others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuse beats rebuilding:&lt;/strong&gt; Official and community servers already exist for hundreds of tools, so adding a server is usually faster than writing a new integration from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope grows without a rewrite:&lt;/strong&gt; Start with one server for a proof of concept, then add more as the agent's job expands. The client's aggregation logic does not change when the server count does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tighter blast radius per integration:&lt;/strong&gt; Keeping GitHub, Slack, and a database as separate server processes means a bug or credential leak in one does not automatically compromise the others, even though the agent still sees all their tools in one merged list.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How MCP Clients Aggregate Multiple Servers
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The MCP Client as an Aggregator
&lt;/h3&gt;

&lt;p&gt;MCP is a client and server protocol. The client lives inside the host application, Claude Code, Cursor, Claude Desktop, or a custom app you build with the MCP SDK.&lt;/p&gt;

&lt;p&gt;For every server entry in its configuration, the client opens a separate connection: typically a subprocess over stdio for a local server, or an HTTP connection for a remote one.&lt;/p&gt;

&lt;p&gt;Each connection is independent. A crash in one server does not take the others down, and each keeps its own session state.&lt;/p&gt;

&lt;p&gt;The client's job is to hold all of those connections open at once and present a single, merged interface to the model, so the model never has to address a specific server by name.&lt;/p&gt;
&lt;h3&gt;
  
  
  Tool Discovery, Merging, and Call Routing
&lt;/h3&gt;

&lt;p&gt;On startup, and again whenever a server's tools change, the client calls &lt;code&gt;tools/list&lt;/code&gt; on every connected server and merges the results into one array of tool definitions.&lt;/p&gt;

&lt;p&gt;That combined list is what gets passed to the model at the start of a turn.&lt;/p&gt;

&lt;p&gt;When the model picks a tool, the client looks up which underlying server owns that tool name and forwards the call there over &lt;code&gt;tools/call&lt;/code&gt;, then folds the result back into the conversation.&lt;/p&gt;

&lt;p&gt;New servers, or new tools registered by an existing server, become available the next time discovery runs, with no restart required for the whole client.&lt;/p&gt;

&lt;p&gt;This merge step is also where naming collisions between two servers first show up. The MCP specification is explicit that tool name uniqueness is scoped to a single server, and that clients or proxies aggregating multiple servers should implement a disambiguation strategy such as prefixing tool names with a server identifier once two servers register the same name.&lt;/p&gt;

&lt;p&gt;More on that in the next section.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to Connect Multiple MCP Servers to One AI Agent Step by Step
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Method 1: Configure Multiple Servers in Claude Desktop or Cursor
&lt;/h3&gt;

&lt;p&gt;The config format is identical across Claude Code, Claude Desktop, and Cursor: a JSON object under an &lt;code&gt;mcpServers&lt;/code&gt; key, one entry per server. Only the file location changes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Locate the config file for your client. Claude Code reads &lt;code&gt;.mcp.json&lt;/code&gt; at your project root, or &lt;code&gt;~/.claude.json&lt;/code&gt; for a user-level server. Cursor reads &lt;code&gt;.cursor/mcp.json&lt;/code&gt; at the project root. Claude Desktop reads &lt;code&gt;claude_desktop_config.json&lt;/code&gt;, found at &lt;code&gt;~/Library/Application Support/Claude/claude_desktop_config.json&lt;/code&gt; on macOS or &lt;code&gt;%APPDATA%\Claude\claude_desktop_config.json&lt;/code&gt; on Windows.&lt;/li&gt;
&lt;li&gt;Add one entry per server under &lt;code&gt;mcpServers&lt;/code&gt;, giving each a unique key.&lt;/li&gt;
&lt;li&gt;Restart the client so it spawns the new processes and runs discovery.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.githubcopilot.com/mcp/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer YOUR_GITHUB_PAT"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"filesystem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"@modelcontextprotocol/server-filesystem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"/Users/you/projects"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"internal-tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"internal_mcp_server.py"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"INTERNAL_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-key-here"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This defines three servers at once: a remote HTTP server for GitHub, a local stdio server started with &lt;code&gt;npx&lt;/code&gt;, and a custom local script.&lt;/p&gt;

&lt;p&gt;Claude Code and Cursor read the &lt;code&gt;type: "http"&lt;/code&gt; and &lt;code&gt;url&lt;/code&gt; fields directly.&lt;/p&gt;

&lt;p&gt;Claude Desktop currently only spawns local stdio servers from this file, so for a remote server like the GitHub example above, use a stdio-to-HTTP bridge with &lt;code&gt;npx mcp-remote &amp;lt;url&amp;gt;&lt;/code&gt; in the &lt;code&gt;command&lt;/code&gt; and &lt;code&gt;args&lt;/code&gt; fields, or add it as a Custom Connector from the Settings UI instead.&lt;/p&gt;
&lt;h3&gt;
  
  
  Method 2: Build a Custom Multi-Server Client in Python
&lt;/h3&gt;

&lt;p&gt;If you are building your own app rather than using a chat client, the Python MCP SDK lets you open one &lt;code&gt;ClientSession&lt;/code&gt; per server and manage all of them with an &lt;code&gt;AsyncExitStack&lt;/code&gt;, so every connection closes cleanly even if one server fails to start.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncExitStack&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StdioServerParameters&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.client.stdio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stdio_client&lt;/span&gt;

&lt;span class="n"&gt;SERVERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StdioServerParameters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp-remote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.githubcopilot.com/mcp/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filesystem&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StdioServerParameters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@modelcontextprotocol/server-filesystem&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;connect_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StdioServerParameters&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AsyncExitStack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;sessions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enter_async_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stdio_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enter_async_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connect_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SERVERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;merged_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;server_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_tools&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;merged_tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;server_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent can see &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merged_tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tools &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;across &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; servers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aclose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is the same one the built-in clients use internally: connect to every server, call &lt;code&gt;list_tools()&lt;/code&gt; on each, merge the results, and keep a lookup from tool name back to the owning session so a call can be routed correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling Tool Name Collisions and Namespacing
&lt;/h3&gt;

&lt;p&gt;The MCP specification does not guarantee that a tool name is unique across servers, only within a single server.&lt;/p&gt;

&lt;p&gt;If your GitHub server and a Jira server both register a tool called &lt;code&gt;search&lt;/code&gt;, or two Postgres servers pointed at different databases both expose &lt;code&gt;execute_sql&lt;/code&gt;, an unhandled client will pick one arbitrarily or reject the duplicate outright, and calls meant for the other server never arrive.&lt;/p&gt;

&lt;p&gt;The fix is to prefix every tool name with its server identifier before handing the merged list to the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;server_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_tools&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;namespaced_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;server_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;__&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="c1"&gt;# Register namespaced_name with the model.
&lt;/span&gt;        &lt;span class="c1"&gt;# Remember (server_name, tool.name) for routing.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A double underscore or a colon works well as the separator since neither is likely to appear in a real tool name.&lt;/p&gt;

&lt;p&gt;When the model calls &lt;code&gt;github__search&lt;/code&gt;, split on the separator, look up &lt;code&gt;github&lt;/code&gt; in your session map, and forward the call with the original name, &lt;code&gt;search&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do this once in your merging layer and every collision downstream is already solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Share MCP Servers Across Multiple Projects
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Reusing Server Configs Without Copy Pasting
&lt;/h3&gt;

&lt;p&gt;Hard coding a server's command, arguments, and credentials into every project's config file means a credential rotation turns into editing N files by hand.&lt;/p&gt;

&lt;p&gt;Keep the parts that vary, tokens, URLs, and connection strings, in environment variables rather than the JSON itself, and reference them with &lt;code&gt;env&lt;/code&gt; blocks so the config file is safe to commit while the actual secrets live in a &lt;code&gt;.env&lt;/code&gt; file or secrets manager.&lt;/p&gt;

&lt;p&gt;For a server you run locally across several projects, consider registering it once at the user level, such as &lt;code&gt;~/.claude.json&lt;/code&gt; for Claude Code, instead of pasting the same entry into every project's &lt;code&gt;.mcp.json&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Centralizing Servers Behind a Hosted Hub
&lt;/h3&gt;

&lt;p&gt;For a remote server, the cleanest way to share it across projects is to stop spawning a fresh local process per project and instead point every project's client at the same running instance over HTTP.&lt;/p&gt;

&lt;p&gt;That single instance can front many backend integrations at once, stay on one version, and get patched in one place instead of N.&lt;/p&gt;

&lt;p&gt;Some integration platforms package this as a hosted relay purpose-built for the parts of an integration that need a public URL, OAuth callbacks, connect pages, and approval screens, so every project registers one callback URL instead of one per environment per provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping Credentials Out of Every Project
&lt;/h3&gt;

&lt;p&gt;The moment you are sharing servers across projects, credential sprawl becomes the real maintenance burden, not the server code itself.&lt;/p&gt;

&lt;p&gt;A pattern worth borrowing from teams that have solved this at scale is to resolve credentials server-side, inside the integration layer, rather than handing tokens to every project's &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;The agent and the client config should see method names and results, never a raw key.&lt;/p&gt;

&lt;p&gt;Following each provider's own setup steps once and storing the result centrally, rather than repeating them per project, is the difference between a five-minute credential rotation and an afternoon of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing MCP Servers for AI Agent Integrations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Common MCP Servers by Use Case: GitHub, Slack, Notion, Google Drive, Postgres
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; Repository, issue, and pull request operations for coding agents that read code, triage bugs, or manage releases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack:&lt;/strong&gt; Reading and posting to channels for notification agents, support bots, or anything that needs to surface a result where a team already works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notion:&lt;/strong&gt; Reading and writing pages and databases for documentation, knowledge base, and project tracking workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Drive:&lt;/strong&gt; File search and content retrieval for agents that need to ground answers in documents your team already has.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres:&lt;/strong&gt; Schema introspection and querying for data agents. Point these at a read-only replica or a role scoped to &lt;code&gt;SELECT&lt;/code&gt; unless the agent genuinely needs to write.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to Check Before Adding a Server: Security, Freshness, Maintenance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; Is this maintained by the vendor, such as an official GitHub, Slack, or Google server, a well-known open-source project, or an anonymous fork with no history? Vendor and well-established community servers should be your default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness:&lt;/strong&gt; Check the last commit and release date. MCP's transport and spec have moved quickly, so a server untouched for a year likely predates current authentication and transport conventions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope of access:&lt;/strong&gt; Check exactly which permissions or API scopes the server requests, and whether it can be run with a narrower token than the default it documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What it executes:&lt;/strong&gt; Some servers run arbitrary code you supply, such as a &lt;code&gt;run_script&lt;/code&gt; style tool. Know whether that execution is sandboxed and what it can reach on the host machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community signal and license:&lt;/strong&gt; Stars, open issues, and how quickly maintainers respond are a reasonable proxy for whether a server will still be patched next year. Confirm the license permits how you intend to use and, if needed, modify it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Hidden Cost of Wiring MCP Servers by Hand
&lt;/h2&gt;

&lt;p&gt;Two or three servers are easy to reason about by hand. The costs compound quietly once you pass five, and they rarely show up until you are already depending on the setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool list bloat:&lt;/strong&gt; Every connected server's full tool set loads into the model's context on every turn, whether or not that turn needs it. More servers mean a bigger token bill and a higher chance the model reaches for the wrong tool out of a crowded list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential sprawl:&lt;/strong&gt; Each server authenticates differently, with its own token, OAuth app, or webhook secret, duplicated across every project and environment that needs it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent breakage:&lt;/strong&gt; Each server has its own release cycle and maintainer. A dependency bump that fixes one server can break compatibility with another, and a maintainer who goes quiet leaves you running an unpatched server with no warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Namespace conflicts you have to solve yourself:&lt;/strong&gt; The specification puts the burden of disambiguating collisions on the client, not the servers, so this is work every team wiring multiple servers ends up rebuilding independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No shared approval layer:&lt;/strong&gt; MCP itself does not define how a destructive call gets gated behind human review. Without a host that provides one, that safety net has to be built per project and per server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full duplication per project:&lt;/strong&gt; Every one of the problems above gets solved again from scratch in the next repository because none of it is centralized.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using an Integration Layer to Connect and Share MCP Servers at Scale
&lt;/h2&gt;

&lt;p&gt;An integration layer takes a different approach to the same problem: instead of running and maintaining a separate MCP server process for every service, you run one instance inside your own app, and each service is added as a plugin rather than a new server to deploy and patch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://corsair.dev/" rel="noopener noreferrer"&gt;Corsair&lt;/a&gt; is an open-source example of this pattern, built natively on MCP.&lt;/p&gt;

&lt;p&gt;You install the plugins you need and wire them once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createCorsair&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;corsair&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@corsair-dev/github&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slack&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@corsair-dev/slack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;notion&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@corsair-dev/notion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;corsair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCorsair&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;github&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;notion&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;kek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CORSAIR_KEK&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every framework adapter Corsair ships, for the Claude Agent SDK, the Anthropic SDK, the Vercel AI SDK, Cursor, or Claude Code, &lt;a href="https://docs.corsair.dev/use-cases/agents" rel="noopener noreferrer"&gt;exposes the same three tools&lt;/a&gt; regardless of how many plugins are installed: &lt;code&gt;list_operations&lt;/code&gt; to discover what is available, &lt;code&gt;get_schema&lt;/code&gt; to inspect an endpoint, and &lt;code&gt;run_script&lt;/code&gt; to execute.&lt;/p&gt;

&lt;p&gt;Adding a tenth plugin does not add a tenth top-level tool to the model's context, and it does not create a new naming collision to solve, since every operation is addressed by name, such as &lt;code&gt;github.issues.create&lt;/code&gt; or &lt;code&gt;slack.messages.post&lt;/code&gt;, inside one consistent script tool instead of competing for a spot in a flat, ever-growing tool list.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.corsair.dev/mcp-adapters/mcp-adapters" rel="noopener noreferrer"&gt;MCP adapters&lt;/a&gt; themselves handle the schema wiring, so connecting a new framework does not mean rewriting the tool surface either.&lt;/p&gt;

&lt;p&gt;Credentials follow the same centralizing logic described earlier in this guide. Corsair resolves them at call time from your own encrypted database, so the model, client config, and agent never hold a raw token.&lt;/p&gt;

&lt;p&gt;Its optional hosted relay, &lt;a href="https://docs.corsair.dev/hub/overview" rel="noopener noreferrer"&gt;Corsair Hub&lt;/a&gt;, gives every project one OAuth callback URL and one connect page instead of a separate one per provider per environment, which is exactly the sharing problem described above solved for you rather than rebuilt per project.&lt;/p&gt;

&lt;p&gt;Corsair is Apache 2.0 licensed, self-hostable for free with no per-seat pricing, and backed by Y Combinator, with more than 11,000 stars on GitHub at the time of writing. The Hobby plan runs unlimited tool calls with up to 50 connections and no credit card required.&lt;/p&gt;

&lt;p&gt;Wiring individual MCP servers by hand works fine for a single integration or a weekend project, but the moment an agent needs five services, or a project has to run cleanly across development and production, the maintenance catches up fast.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://corsair.dev/" rel="noopener noreferrer"&gt;Corsair&lt;/a&gt; takes the alternative approach: install the plugins you need, expose them to your agent through one consistent MCP surface, and let Corsair handle credential storage, OAuth callbacks, and tool routing underneath.&lt;/p&gt;

&lt;p&gt;It is open source, self-hostable for free, and built specifically for teams that want their agents talking to real APIs without babysitting a separate server per integration.&lt;/p&gt;

&lt;p&gt;The docs are a good next stop for seeing how a Corsair instance slots into an MCP setup you already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can One AI Agent Use Multiple MCP Servers at the Same Time?
&lt;/h3&gt;

&lt;p&gt;Yes. The MCP client that sits inside the host application, Claude Code, Cursor, Claude Desktop, or a custom app, opens a separate connection to each configured server, merges their tool lists into one set, and routes each call back to the server that owns it.&lt;/p&gt;

&lt;p&gt;The agent sees one combined toolbox and does not need to know how many servers are behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Do I Share the Same MCP Server Across Different Projects?
&lt;/h3&gt;

&lt;p&gt;Point every project's client config at the same running instance over HTTP instead of spawning a fresh local process per project, and keep credentials in environment variables or a secrets manager rather than duplicated inside each config file.&lt;/p&gt;

&lt;p&gt;For heavier reuse, centralize the server behind a hosted relay so projects share one endpoint, one callback URL, and one credential store instead of maintaining their own.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Happens if Two MCP Servers Expose Tools With the Same Name?
&lt;/h3&gt;

&lt;p&gt;The MCP specification only guarantees tool name uniqueness within a single server, not across servers, so a collision is possible whenever two connected servers happen to register the same tool name.&lt;/p&gt;

&lt;p&gt;Without handling it, the client may pick one arbitrarily or reject the duplicate, and calls intended for the other server never arrive.&lt;/p&gt;

&lt;p&gt;The standard fix is to prefix each tool name with its server identifier during the merge step before the combined list reaches the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Many MCP Servers Can a Single Agent Connect To?
&lt;/h3&gt;

&lt;p&gt;There is no protocol-level limit.&lt;/p&gt;

&lt;p&gt;The practical ceiling comes from your context window, since every tool definition from every connected server counts against the token budget, and from process or connection overhead on the host machine.&lt;/p&gt;

&lt;p&gt;Most setups stay comfortably in the five-to-fifteen-server range before the tool list gets unwieldy. An aggregator that exposes a fixed set of tools regardless of how many services are behind it removes that ceiling entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I Need Separate Credentials for Each MCP Server?
&lt;/h3&gt;

&lt;p&gt;Generally yes, since each server authenticates against a different provider, such as a GitHub token, Slack bot token, or database connection string, and MCP does not define any protocol-level credential sharing between servers.&lt;/p&gt;

&lt;p&gt;You can cut the overhead by centralizing storage rather than duplicating &lt;code&gt;.env&lt;/code&gt; files per project: use a secrets manager or an integration layer that resolves credentials for every connected service from one encrypted database.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>multiplemcp</category>
    </item>
    <item>
      <title>Google API Rate Limits: How to Keep AI Agents Reliable in Production</title>
      <dc:creator>Corsair</dc:creator>
      <pubDate>Thu, 10 Sep 2026 17:39:45 +0000</pubDate>
      <link>https://dev.to/corsairdev/google-api-rate-limits-how-to-keep-ai-agents-reliable-in-production-h0e</link>
      <guid>https://dev.to/corsairdev/google-api-rate-limits-how-to-keep-ai-agents-reliable-in-production-h0e</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqktmc3rjuf6ey4vi4sf8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqktmc3rjuf6ey4vi4sf8.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;A Google API integration that works fine in testing can start failing within days of going live once real users are attached to it, and the cause is rarely a bug. It is usually a rate limit or a quota, the ceilings Google places on how much traffic an application, and each user inside it, can send within a given window.&lt;/p&gt;

&lt;p&gt;AI agents make this more likely, not less. A single agent instruction can fan out into a burst of Gmail, Calendar, or Drive calls that a human clicking through the same task would never generate in the same few seconds, and Google has taken notice: 2026 brought a standardized quota model aimed specifically at agent-driven traffic across its Workspace APIs.&lt;/p&gt;

&lt;p&gt;This guide covers what Google API rate limits and quotas actually measure, why AI agent API integrations trip them more often than typical usage, and the practical patterns behind reliable Google API rate limit handling, from exponential backoff and jitter to caching, incremental reads, concurrency control, and ongoing monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Google API Rate Limits and Quotas
&lt;/h2&gt;

&lt;p&gt;When an application calls Gmail, Google Calendar, Google Drive, or any other Google API, that traffic is measured against limits Google sets, not limits you configure yourself. Two related but distinct mechanisms govern how much gets through: rate limits and quotas.&lt;/p&gt;

&lt;p&gt;A rate limit is a short-window throttle, a maximum number of requests allowed within a small span of time, commonly per minute or per 100 seconds. A quota is a broader allotment, often measured over a full day, that caps total usage before Google either blocks further calls or, for some APIs, begins billing for the overage.&lt;/p&gt;

&lt;p&gt;Rate limits guard against sudden bursts. Quotas guard against sustained, high-volume usage over time.&lt;/p&gt;

&lt;p&gt;Most Google APIs enforce both at two levels at once. There is a ceiling on what your entire Google Cloud project can consume, and a separate, smaller ceiling on what any single authenticated user can consume within that project.&lt;/p&gt;

&lt;p&gt;A project can be well under its total allowance and still see one user's requests rejected because that user's own slice of the quota ran out first.&lt;/p&gt;

&lt;p&gt;For APIs like Gmail and Google Drive, usage is not even counted as flat request numbers. It is measured in quota units, an abstract cost assigned to each method, so a lightweight read might cost a handful of units while sending a message or uploading a file costs many more.&lt;/p&gt;

&lt;p&gt;When a rate limit or quota is exceeded, Google typically responds with an HTTP &lt;code&gt;429 Too Many Requests&lt;/code&gt;, or a &lt;code&gt;403&lt;/code&gt; error carrying a reason described as rate limit exceeded, sometimes shown as a resource exhausted error depending on the API.&lt;/p&gt;

&lt;p&gt;None of these responses say exactly how long to wait, only that the current request did not go through.&lt;/p&gt;

&lt;p&gt;Quota configuration and live usage for a project are visible in the Google Cloud console, under its quotas and system limits section, which is also where you can request an increase for the quotas Google allows you to adjust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI Agent Workflows Hit Rate Limits in Production
&lt;/h2&gt;

&lt;p&gt;A person using Gmail through a browser reads a message, thinks for a moment, then clicks reply. That pause between actions is, without anyone intending it, a built-in rate limiter.&lt;/p&gt;

&lt;p&gt;An AI agent does not pause the same way. A single instruction like "catch me up on this thread and put a follow-up on my calendar" can translate into a search call, several message reads, a calendar availability check, and an event creation, all fired within the same second as part of one reasoning loop.&lt;/p&gt;

&lt;p&gt;Multiply that pattern across concurrent users, and an agent product can generate far more Google API traffic per minute than the equivalent number of human users ever would, even though the total useful work is identical.&lt;/p&gt;

&lt;p&gt;That is exactly the short-window traffic pattern that per-minute rate limits exist to catch.&lt;/p&gt;

&lt;p&gt;Google has already adjusted its own posture in response to this shift. After introducing dedicated agent tooling that exposes Gmail, Calendar, Drive, Chat, and Contacts as callable operations, Google rolled out a standardized tiering model for agent tools and APIs in 2026, tightening default quotas across Gmail, Calendar, and Drive specifically to guard against risks like automated abuse and large-scale data egress from agent-driven traffic.&lt;/p&gt;

&lt;p&gt;AI-generated API usage is no longer treated as simply more of the same human traffic. It sits in its own risk category with its own quota posture, which makes deliberate rate limit handling a production requirement rather than an edge case worth ignoring.&lt;/p&gt;

&lt;p&gt;Two habits tend to make the problem worse once an agent starts hitting limits.&lt;/p&gt;

&lt;p&gt;A retry that fires again immediately after a failure adds to the very burst that triggered the rejection in the first place. And in a product where many customers share one Google Cloud project, one tenant's unusually active agent session can consume enough of the shared quota to degrade the experience for every other tenant connected to that same project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Rate Limit Errors With Exponential Backoff and Jitter
&lt;/h2&gt;

&lt;p&gt;Exponential backoff is the standard response to a rate limit error, and Google's own guidance for its Workspace APIs recommends it directly for this exact scenario.&lt;/p&gt;

&lt;p&gt;Instead of retrying immediately, each failed attempt waits longer than the one before it, typically doubling the delay each time, up to a capped maximum wait.&lt;/p&gt;

&lt;p&gt;A request that fails once might retry after a second, then two seconds, then four, rather than hammering the API at the same pace that got it throttled to begin with.&lt;/p&gt;

&lt;p&gt;Jitter is what makes Google API exponential backoff hold up at scale.&lt;/p&gt;

&lt;p&gt;If every client that got rate limited at the same moment retries after exactly the same delay, they all arrive back at the API together and trigger a fresh wave of rejections.&lt;/p&gt;

&lt;p&gt;Adding a small amount of randomness to each wait time spreads those retries out, so recovery happens gradually instead of in synchronized bursts.&lt;/p&gt;

&lt;p&gt;A few points matter when building this into an agent workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cap the number of retry attempts:&lt;/strong&gt; A stuck tool call should not loop indefinitely and stall the agent's response to the user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap the maximum backoff delay:&lt;/strong&gt; A single retry sequence should not silently stretch into minutes for a user-facing action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate rate limits from permanent failures:&lt;/strong&gt; Treat a rate limit response differently from a genuine failure like invalid credentials or a missing scope, since repeatedly retrying an authentication problem wastes every attempt without fixing anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surface exhausted retries clearly:&lt;/strong&gt; Once retries are exhausted, return a clear signal rather than letting the agent silently drop the action and continue as if it succeeded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Corsair builds this pattern in directly rather than leaving it for every integration to reinvent. Its &lt;a href="https://docs.corsair.dev/concepts/error-handling" rel="noopener noreferrer"&gt;error handling supports configurable retry strategies, including exponential backoff with jitter, defined per plugin or globally across every integration&lt;/a&gt;, so a Gmail rate limit and a Calendar rate limit can be handled with the same logic instead of writing separate retry code for each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing API Requests Through Batching, Caching, and Incremental Reads
&lt;/h2&gt;

&lt;p&gt;The most reliable way to avoid a rate limit is to make fewer requests in the first place.&lt;/p&gt;

&lt;p&gt;A few approaches work well together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batching:&lt;/strong&gt; Group several related operations into one round trip instead of issuing a separate call for every item, which cuts the total request count for the same amount of work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching:&lt;/strong&gt; Store data already fetched and serve repeat reads from that local copy instead of asking Google for the same information again moments later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental reads:&lt;/strong&gt; Fetch only what changed since the last check rather than rereading a whole inbox, calendar, or file list on every pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Caching only helps if the cached data stays trustworthy, which is where a lot of homegrown solutions get shaky.&lt;/p&gt;

&lt;p&gt;Corsair addresses this by &lt;a href="https://docs.corsair.dev/concepts/database" rel="noopener noreferrer"&gt;keeping a tenant-scoped database that stays current through both API responses and webhooks&lt;/a&gt;, so a dashboard or agent can read from that local store for lists, search, and detail views instead of hitting the Google API on every page load, while writes and anything needing the freshest possible value still go straight to the source.&lt;/p&gt;

&lt;p&gt;Incremental reads are really a polling problem in disguise. Continuously asking whether anything changed is itself a source of rate limit pressure, especially across many tenants checking on their own schedule.&lt;/p&gt;

&lt;p&gt;Subscribing to change notifications instead of polling flips that pattern around: Google reports the moment something changes, rather than being asked repeatedly and mostly answering no.&lt;/p&gt;

&lt;p&gt;Corsair's &lt;a href="https://docs.corsair.dev/concepts/webhooks" rel="noopener noreferrer"&gt;triggers route every incoming webhook to a single endpoint automatically&lt;/a&gt;, including handling events that arrive out of order, so an agent learns about a new message or a moved event without a single extra API call spent checking for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Controlling Concurrency and Scheduling API Requests Across Users and Agents
&lt;/h2&gt;

&lt;p&gt;Because Google enforces quotas at both the project level and the user level, how requests get routed matters as much as how many get sent.&lt;/p&gt;

&lt;p&gt;Funneling every agent action through one shared service account concentrates all of that traffic against a single user's quota bucket, even when the underlying work belongs to many different end users.&lt;/p&gt;

&lt;p&gt;Spreading requests across each person's own connected account keeps usage within each individual allowance instead of stacking everything on one.&lt;/p&gt;

&lt;p&gt;Concurrency limits and scheduling do similar work from a different angle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Limit parallel API calls:&lt;/strong&gt; Control how many API calls run simultaneously per project or per user, so a burst of agent activity stays under the short-window rate limit instead of spiking past it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schedule non-urgent workloads:&lt;/strong&gt; Push bulk work, such as a large historical sync or backfill, into off-peak windows rather than firing it alongside live user-facing requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prioritize interactive requests:&lt;/strong&gt; Give user-facing actions priority over background jobs when both compete for the same quota, so a customer waiting on a live response is not stuck behind a batch job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolate tenants:&lt;/strong&gt; Prevent one customer's unusually active agent from consuming the quota that other customers depend on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is where credential isolation earns its keep.&lt;/p&gt;

&lt;p&gt;Corsair's &lt;a href="https://docs.corsair.dev/concepts/multi-tenancy" rel="noopener noreferrer"&gt;multi-tenancy model scopes credentials and data to each connected user automatically&lt;/a&gt;, so every tenant's Google API activity runs against its own connected account rather than a shared one, and a spike from one user's agent session does not quietly eat into a limit that every other user is also depending on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring Google API Quota Usage and Testing Rate Limit Recovery
&lt;/h2&gt;

&lt;p&gt;Rate limit handling that only gets tested when it fails in production is not really tested.&lt;/p&gt;

&lt;p&gt;Monitoring and deliberate testing turn quota management into something a team can plan around instead of something it discovers the hard way.&lt;/p&gt;

&lt;p&gt;On the monitoring side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Watch quota usage:&lt;/strong&gt; Monitor usage in the Google Cloud console before hitting a wall rather than treating a &lt;code&gt;429&lt;/code&gt; response as the first sign something is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set threshold alerts:&lt;/strong&gt; Configure alerts near a usage threshold so the team gets a warning while there is still room to react, not after every request is already failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track rate limit errors:&lt;/strong&gt; Log every rate limit error an agent hits, including which operation, integration, and tenant it belonged to, so patterns show up in the data instead of only in support tickets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the testing side, simulate rate-limited responses deliberately in a staging environment rather than waiting for real production traffic to expose a gap.&lt;/p&gt;

&lt;p&gt;Confirm that backoff timing behaves the way it is supposed to, that retries actually stop once the cap is reached, and that the agent surfaces a sensible message instead of hanging when an operation ultimately fails.&lt;/p&gt;

&lt;p&gt;A rising rate of exhausted retries over time is usually the earliest honest signal that request volume needs to come down, or that it is time to request a quota increase before growth forces the issue.&lt;/p&gt;

&lt;p&gt;Google API rate limits are not a problem solved once and forgotten. They shift as Google adjusts its quota model, as a user base grows, and as agents take on more autonomous, multi-step work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://corsair.dev/" rel="noopener noreferrer"&gt;Corsair&lt;/a&gt; handles the retry logic, caching, webhook-based reads, and tenant isolation behind every Google integration it supports, so reliable AI agent API integrations come built into the plugin rather than something a team has to maintain by hand.&lt;/p&gt;

&lt;p&gt;That leaves product logic, not quota math, as the part actually worth spending engineering time on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Is the Difference Between a Google API Rate Limit and a Quota?
&lt;/h3&gt;

&lt;p&gt;A rate limit is a short-window cap, typically enforced per minute or per 100 seconds, meant to stop a sudden burst of requests.&lt;/p&gt;

&lt;p&gt;A quota is a broader allotment, often measured per day, that caps total usage over a longer period.&lt;/p&gt;

&lt;p&gt;Google enforces both at once, and either one can reject a request even when the other still has room left.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Error Code Shows That a Google API Rate Limit Was Exceeded?
&lt;/h3&gt;

&lt;p&gt;Most Google APIs return an HTTP &lt;code&gt;429 Too Many Requests&lt;/code&gt; when a rate limit is hit, though some return a &lt;code&gt;403&lt;/code&gt; error with a reason described as rate limit exceeded, or describe it as a resource exhausted error depending on the API.&lt;/p&gt;

&lt;p&gt;None of these responses specify how long to wait before retrying, which is why backoff logic has to make that decision on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Does Exponential Backoff With Jitter Prevent Repeated Rate Limit Errors?
&lt;/h3&gt;

&lt;p&gt;Exponential backoff increases the wait time after each failed retry instead of retrying immediately, easing pressure on the API instead of adding to it.&lt;/p&gt;

&lt;p&gt;Jitter adds a small random variation to each wait, so multiple clients rate limited at the same moment do not all retry at the exact same instant and trigger a new burst together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Caching Reduce How Often AI Agents Hit Google API Rate Limits?
&lt;/h3&gt;

&lt;p&gt;Yes. Serving repeat reads from a local, regularly updated cache instead of calling the Google API again for the same data cuts a meaningful share of request volume, especially for data an agent checks often but that rarely changes between checks.&lt;/p&gt;

&lt;p&gt;Pairing caching with webhook-driven updates keeps that cached data accurate without needing to poll for changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Every User Need Their Own Google API Quota in a Multi-Tenant Agent Product?
&lt;/h3&gt;

&lt;p&gt;Google enforces quotas per user within a project, not only per project overall, so routing every tenant's traffic through one shared account concentrates all of it against a single user's allowance.&lt;/p&gt;

&lt;p&gt;Giving each tenant their own connected Google account and credentials keeps their usage scoped to their own quota instead of competing with every other tenant for the same one.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Debugging AI Agent Tool Calls: How to Trace and Fix Failures Across MCP Servers and APIs</title>
      <dc:creator>Corsair</dc:creator>
      <pubDate>Thu, 10 Sep 2026 17:36:16 +0000</pubDate>
      <link>https://dev.to/corsairdev/debugging-ai-agent-tool-calls-how-to-trace-and-fix-failures-across-mcp-servers-and-apis-4o0m</link>
      <guid>https://dev.to/corsairdev/debugging-ai-agent-tool-calls-how-to-trace-and-fix-failures-across-mcp-servers-and-apis-4o0m</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fetdgw22ovisf98ly7twr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fetdgw22ovisf98ly7twr.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;An AI agent is asked to find a document in Google Drive. It selects a search tool, sends a request, and returns a vague error. That message tells the user very little about whether the problem involves the search query, the connection, or Google Drive access.&lt;/p&gt;

&lt;p&gt;Debugging AI agent tool calls means following that request through the software responsible for executing it. Each stage supplies evidence that can narrow the cause and guide a specific fix.&lt;/p&gt;

&lt;p&gt;For applications using MCP, the essential skills are recognizing error types, collecting useful logs, and connecting activity across the agent, server, and API. A controlled example then makes it possible to verify that recovery restores the user's intended result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI Agent Tool Calls Fail: From Tool Selection to API Response
&lt;/h2&gt;

&lt;p&gt;A tool call is a request to perform an operation outside the language model, such as searching files, reading a calendar, or creating a support ticket. The agent chooses the operation, but application code carries it out.&lt;/p&gt;

&lt;p&gt;When Model Context Protocol, or MCP, connects the agent to its tools, several components participate. The agent application uses an MCP client to communicate with a server. The server runs the selected tool, which may call an external API, and returns a result.&lt;/p&gt;

&lt;p&gt;Debugging starts by identifying how far that request travelled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool selection:&lt;/strong&gt; The agent chose an operation that does not match the user's goal, or the required tool was unavailable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argument construction:&lt;/strong&gt; The tool received a missing identifier, an unsupported value, or a date in the wrong format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection and execution:&lt;/strong&gt; The server could not start, the connection failed, or the tool's application code raised an exception.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External API access:&lt;/strong&gt; The provider rejected the request or failed to respond within the allowed time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result interpretation:&lt;/strong&gt; The API returned useful data, but the integration dropped a field or the agent misunderstood the response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The integration design affects where you inspect each stage. For example, &lt;a href="https://docs.corsair.dev/mcp-adapters/mcp-adapters" rel="noopener noreferrer"&gt;Corsair's MCP adapters&lt;/a&gt; let agents discover available operations, inspect their parameters, and execute them. Those stages provide useful checkpoints when an agent selects an unexpected operation.&lt;/p&gt;

&lt;p&gt;Keep the expected outcome visible throughout the investigation. An API request can succeed technically while retrieving the wrong information for the user's task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinguishing MCP Protocol Errors From Tool Execution Failures
&lt;/h2&gt;

&lt;p&gt;MCP debugging becomes easier when you identify what kind of failure the server reported. In MCP's November 2025 specification, tool calls use two error reporting mechanisms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protocol errors:&lt;/strong&gt; Problems such as an unknown tool or a malformed request are returned as JSON RPC errors. Check the tool name, request structure, and client compatibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool execution errors:&lt;/strong&gt; The tool reports a failure through a result containing &lt;code&gt;isError: true&lt;/code&gt;. Examples include rejected input values, external API failures, and business rules that prevent an operation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction matters for invalid inputs. A malformed tool call request differs from a correctly structured call whose date or field value fails validation. The MCP tool error handling specification explains this separation.&lt;/p&gt;

&lt;p&gt;Also distinguish successful message delivery from successful execution. Receiving a valid tool response does not establish that the requested action worked. Your application must inspect the result and preserve its failure status when passing it to the agent.&lt;/p&gt;

&lt;p&gt;For effective MCP server error handling, return a safe, actionable explanation. A message that identifies the invalid field gives the application a clearer recovery path than a generic statement that something went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Log for Effective Tool Call Debugging
&lt;/h2&gt;

&lt;p&gt;Useful logs help answer three questions: what was attempted, where it failed, and what happened next. Collect enough context to reconstruct the execution without copying every prompt or response into storage.&lt;/p&gt;

&lt;p&gt;For application logs, consider recording:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Execution identifiers:&lt;/strong&gt; The agent run, individual tool call, and retry attempt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operation details:&lt;/strong&gt; The tool name, provider, and relevant server or integration version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input details:&lt;/strong&gt; Parameter names, validation outcomes, and approved values after sensitive data has been removed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing:&lt;/strong&gt; Start time, completion time, total duration, and time spent waiting to retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome:&lt;/strong&gt; Success or failure, the error category, and the provider's status code or request identifier when available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery:&lt;/strong&gt; Whether the application corrected an input, refreshed authentication, retried, or requested user action.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MCP server logging can also use structured notifications sent from the server to its client. This requires the server's logging capability and appropriate client support. The MCP logging specification defines log levels and prohibits credentials, secrets, and personally identifying information in these messages.&lt;/p&gt;

&lt;p&gt;Apply deliberate redaction to your own logs as well. Tokens, authorization headers, document contents, and email addresses can appear inside arguments and exception messages. Prefer summaries, counts, and validation results where those are sufficient.&lt;/p&gt;

&lt;p&gt;Instrumentation belongs close to the operation being observed. &lt;a href="https://docs.corsair.dev/concepts/hooks" rel="noopener noreferrer"&gt;Corsair's API hooks&lt;/a&gt; provide places to add input validation and logging around API operations. Capture exceptions through the relevant error handling path too, so a successful completion log is not your only evidence of execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracing a Tool Call Across the Agent, MCP Server, and External API
&lt;/h2&gt;

&lt;p&gt;A log records an event. A trace connects the operations involved in fulfilling a request, showing their order and duration. AI agent tool call tracing helps explain why a failure in one component appears as an error somewhere else.&lt;/p&gt;

&lt;p&gt;Use a trace identifier to connect the overall execution, with individual spans representing operations such as the MCP call and outbound API request. A span is a timed record of one operation.&lt;/p&gt;

&lt;p&gt;For a practical starting point:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Associate the agent run with a trace or correlation identifier.&lt;/li&gt;
&lt;li&gt;Record each tool invocation and its parent execution.&lt;/li&gt;
&lt;li&gt;Link the server's outbound API request to that invocation.&lt;/li&gt;
&lt;li&gt;Record each retry separately under the same logical operation.&lt;/li&gt;
&lt;li&gt;Connect the final result to the agent response the user received.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An MCP request identifier matches a protocol request to its response. It does not automatically provide a complete trace across your application and the provider. Configure instrumentation at the boundaries you control.&lt;/p&gt;

&lt;p&gt;External services may not expose their internal traces. You can still record your outbound request's duration and outcome, plus a provider request identifier if returned. Keep sensitive context out of information propagated to external services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Invalid Inputs, Authentication Failures, and Timeouts
&lt;/h2&gt;

&lt;p&gt;Once you locate the failing operation, choose a correction that addresses its cause. Repeating the same request is useful only when the conditions for success can change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invalid inputs:&lt;/strong&gt; Compare the actual arguments with the tool's published schema and the provider's requirements. Look for missing identifiers, incorrect types, unsupported options, and invalid date formats. Check any transformation between the tool arguments and the API payload. A valid agent input can become invalid if the integration maps it incorrectly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication failures:&lt;/strong&gt; First establish which connection rejected access. The agent's connection to a remote MCP server and the server's connection to an external provider can use different credentials. Check the failing connection's account, token expiry, and authorization requirements. Keep token values out of the investigation record.&lt;/p&gt;

&lt;p&gt;For Google Drive, a &lt;code&gt;401&lt;/code&gt; response can indicate an expired or invalid access token. A &lt;code&gt;403&lt;/code&gt; requires closer inspection because its reason can involve permissions or a usage limit. Read the provider's error body before choosing a fix. &lt;a href="https://developers.google.com/workspace/drive/api/guides/handle-errors" rel="noopener noreferrer"&gt;Google Drive's error documentation&lt;/a&gt; describes these cases and their recovery options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeouts:&lt;/strong&gt; Identify which component stopped waiting and compare its deadline with the durations in your trace. Check for slow API responses, connection problems, and accumulated retry delays. A timeout leaves the operation's outcome uncertain: a write may have completed even though its response never arrived.&lt;/p&gt;

&lt;p&gt;Before repeating a write, check the provider's retry guarantees and whether you can verify its outcome. Idempotent operations have the same intended effect when repeated; other actions may create duplicate records or messages. Google's &lt;a href="https://docs.cloud.google.com/storage/docs/retry-strategy" rel="noopener noreferrer"&gt;retry strategy guidance&lt;/a&gt; illustrates why both the error and the operation's idempotency matter.&lt;/p&gt;

&lt;p&gt;Where retries are appropriate, use bounded attempts and increasing delays with random variation, called jitter. Check whether your SDK already retries so the agent does not multiply those attempts. &lt;a href="https://docs.corsair.dev/concepts/error-handling" rel="noopener noreferrer"&gt;Corsair's error handling documentation&lt;/a&gt; explains configurable retry strategies and separate errors for missing credentials or connections requiring reauthorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging a Failed Tool Call: From Error Message to Verified Fix
&lt;/h2&gt;

&lt;p&gt;Consider an illustrative test scenario: an agent is asked to find a project brief in Google Drive. The selected search tool is correct, but its integration uses an expired access token and fails to refresh it. The user sees a search failure.&lt;/p&gt;

&lt;p&gt;The following walkthrough shows how to establish that cause through evidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Reproduce the Failure With a Controlled Request
&lt;/h3&gt;

&lt;p&gt;Use a test account and a document you know that account can access. Record the expected result, execution identifier, and observed error. Keep the request simple enough that each attempt follows the same search operation.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inspect the Selected Tool and Arguments
&lt;/h3&gt;

&lt;p&gt;Confirm that the agent chose the search operation and supplied the intended query. Check those arguments against the tool schema.&lt;/p&gt;

&lt;p&gt;The MCP Inspector can help you inspect available tools and test calls directly. Keep the account, permissions, and configuration equivalent when comparing direct calls with agent calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Follow the Request to the Provider
&lt;/h3&gt;

&lt;p&gt;Use the correlation records to establish whether the call reached the MCP server and whether the server contacted Google Drive.&lt;/p&gt;

&lt;p&gt;In this scenario, the API returns an authentication error. Inspect how the tool reports that failure to the agent, including whether the error status survives any response transformation.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Confirm Why Authentication Failed
&lt;/h3&gt;

&lt;p&gt;Check the stored token's expiry metadata and the credential lookup used for this request.&lt;/p&gt;

&lt;p&gt;Establish that the request used the expired token and that refresh did not complete. The status code narrows the investigation; these additional records establish the specific cause.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Correct the Authentication Path
&lt;/h3&gt;

&lt;p&gt;Fix the integration so it obtains and uses a valid access token through the supported refresh flow. Keep this work in the authentication layer.&lt;/p&gt;

&lt;p&gt;If the refresh credential is no longer usable, route the user through account reconnection instead of repeatedly submitting the same failing search.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Verify the Full Result
&lt;/h3&gt;

&lt;p&gt;Repeat the controlled search and confirm that the expected document reaches the agent. Then check that the agent's answer accurately reflects the result.&lt;/p&gt;

&lt;p&gt;Verify the failure path too: an authentication error must remain distinguishable from a successful search with no matching documents.&lt;/p&gt;

&lt;p&gt;Add a regression test for the confirmed defect. For this example, simulate an expired access token and verify refresh and recovery. Also check the outcome when reconnection is required. This turns a resolved incident into a repeatable check on future changes.&lt;/p&gt;

&lt;p&gt;Reliable agent integrations need clear execution records and tested recovery paths.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://corsair.dev/" rel="noopener noreferrer"&gt;Corsair&lt;/a&gt; provides an open-source integration layer for agents and applications. Its API hooks and error handlers offer places to add diagnostics and recovery logic.&lt;/p&gt;

&lt;p&gt;Use these capabilities with application tracing to locate failures and verify each fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Is the First Step in Debugging AI Agent Tool Calls?
&lt;/h3&gt;

&lt;p&gt;Start with one failed execution and define its expected outcome. Inspect the selected tool, supplied arguments, and returned result.&lt;/p&gt;

&lt;p&gt;Establish whether the request reached the MCP server and external API before changing prompts, credentials, or retry settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Is the Difference Between MCP Server Logging and Tool Call Tracing?
&lt;/h3&gt;

&lt;p&gt;Logging records events such as validation failures and completed requests. Tracing connects related operations and their durations across components.&lt;/p&gt;

&lt;p&gt;Together, they help explain both the details of a failure and where it occurred within the agent's overall task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can an MCP Tool Call Fail Without an External API Error?
&lt;/h3&gt;

&lt;p&gt;Yes. The agent may select an unavailable tool, the request may fail validation, or the server may encounter a local exception before contacting the API.&lt;/p&gt;

&lt;p&gt;An application can also mishandle a successful API response. Check the full execution path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should AI Agents Automatically Retry Failed Tool Calls?
&lt;/h3&gt;

&lt;p&gt;Retry only when the error is recoverable and repeating the operation is safe.&lt;/p&gt;

&lt;p&gt;Invalid inputs need correction, and access problems may require authentication recovery. For writes, account for possible duplicate actions. Set attempt limits and consider retries already performed by the SDK.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Can Teams Debug Tool Calls Without Logging Sensitive Data?
&lt;/h3&gt;

&lt;p&gt;Record operation names, timing, safe execution identifiers, error categories, and redacted input summaries.&lt;/p&gt;

&lt;p&gt;Exclude credentials and avoid capturing document or message contents by default. Restrict access to diagnostic records and use test data when reproducing failures.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Google OAuth 2.0 for Developers: Implementation, Security Best Practices, and Troubleshooting</title>
      <dc:creator>Corsair</dc:creator>
      <pubDate>Fri, 04 Sep 2026 15:46:52 +0000</pubDate>
      <link>https://dev.to/corsairdev/google-oauth-20-for-developers-implementation-security-best-practices-and-troubleshooting-39pl</link>
      <guid>https://dev.to/corsairdev/google-oauth-20-for-developers-implementation-security-best-practices-and-troubleshooting-39pl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0j42iluo9nxwaesjmgl0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0j42iluo9nxwaesjmgl0.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;Google OAuth 2.0 often looks simple at first: create credentials, redirect a user to Google, receive authorization, and start calling an API. The complexity appears when that flow has to work reliably for real users across multiple environments, sessions, permissions, and Google services.&lt;/p&gt;

&lt;p&gt;A production-ready Google OAuth implementation has to manage much more than the initial authorization screen. Developers need to configure redirect URIs correctly, request appropriate scopes, separate authentication from API authorization, store tokens securely, refresh credentials when they expire, handle sign-out behavior, and recover gracefully when authorization stops working.&lt;/p&gt;

&lt;p&gt;It is also important to understand that Google Sign-In and Google API authorization are related but different processes. One Tap and Sign In With Google establish who the user is and generally return an ID token. OAuth authorization determines what Google data your application can access and issues access tokens for Google APIs. Google explicitly separates these authentication and authorization flows in Google Identity Services.&lt;/p&gt;

&lt;p&gt;This guide walks through Google OAuth implementation from initial configuration to production security, One Tap, token management, common Google OAuth errors, and the choice between Firebase Authentication and Google Cloud Identity Platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Google OAuth 2.0: Credentials, Consent Screens, Redirect URIs, and Scopes
&lt;/h2&gt;

&lt;p&gt;Every Google OAuth implementation starts with a project in Google Cloud and an OAuth client that represents your application.&lt;/p&gt;

&lt;p&gt;For a typical web application, the authorization flow follows this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your application sends the user to Google's authorization service.&lt;/li&gt;
&lt;li&gt;Google identifies the application using its OAuth client ID.&lt;/li&gt;
&lt;li&gt;The user reviews the requested permissions.&lt;/li&gt;
&lt;li&gt;Google sends an authorization code back to an approved redirect URI.&lt;/li&gt;
&lt;li&gt;Your backend exchanges the authorization code for tokens.&lt;/li&gt;
&lt;li&gt;Your application uses the access token when calling permitted Google APIs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For server-based applications, Google recommends the authorization code flow because the application can securely exchange the code on its backend and store refresh tokens outside the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the OAuth Client
&lt;/h3&gt;

&lt;p&gt;First, create an OAuth client for the appropriate application type in Google Cloud.&lt;/p&gt;

&lt;p&gt;A web application normally receives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client ID:&lt;/strong&gt; Identifies the application to Google.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client secret:&lt;/strong&gt; Authenticates the application during server-side token operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorized redirect URIs:&lt;/strong&gt; Defines exactly where Google may return the user after authorization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your client secret should remain on your backend. It should never be included in frontend JavaScript, committed to a public repository, exposed through logs, or stored in publicly accessible configuration.&lt;/p&gt;

&lt;p&gt;Google specifically recommends protecting OAuth client secrets and keeping them outside publicly accessible source trees.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure the Consent Experience
&lt;/h3&gt;

&lt;p&gt;The consent screen tells users which application is requesting access and what that application wants permission to do.&lt;/p&gt;

&lt;p&gt;Your configuration will typically include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application name&lt;/li&gt;
&lt;li&gt;Support information&lt;/li&gt;
&lt;li&gt;Authorized domains&lt;/li&gt;
&lt;li&gt;Intended audience&lt;/li&gt;
&lt;li&gt;Requested scopes&lt;/li&gt;
&lt;li&gt;Developer contact information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Public applications requesting certain sensitive or restricted Google API scopes may also need to complete Google's verification process.&lt;/p&gt;

&lt;p&gt;The consent screen should match what your product actually does. Asking for broad access without a clear product reason increases both security exposure and user hesitation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure Redirect URIs Carefully
&lt;/h3&gt;

&lt;p&gt;Redirect URI configuration is one of the most frequent causes of Google OAuth errors.&lt;/p&gt;

&lt;p&gt;Google compares the redirect URI in the authorization request with the URI registered for the OAuth client. The values need to match exactly.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.example.com/oauth/google/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is different from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.example.com/oauth/google/callback/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Differences in scheme, capitalization, hostname, port, path, or trailing slash can result in &lt;code&gt;redirect_uri_mismatch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Google also requires HTTPS for normal production redirect URIs, with localhost receiving special treatment for development.&lt;/p&gt;

&lt;p&gt;A useful deployment practice is to define separate OAuth clients or carefully controlled redirect URI configurations for development, staging, and production rather than constantly editing one configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Request Only the Scopes You Need
&lt;/h3&gt;

&lt;p&gt;Scopes determine what the user is allowing your application to access.&lt;/p&gt;

&lt;p&gt;A calendar application might initially need permission to read calendar events. It does not automatically need permission to modify calendars, read Gmail, access Drive files, and manage contacts.&lt;/p&gt;

&lt;p&gt;This is where least privilege begins.&lt;/p&gt;

&lt;p&gt;Google recommends incremental authorization: request permissions when the user actually reaches a feature that needs them rather than requesting every possible permission during the first interaction.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A user signs into your application.&lt;/li&gt;
&lt;li&gt;The user chooses to connect Google Calendar.&lt;/li&gt;
&lt;li&gt;Your application requests the required Calendar scope.&lt;/li&gt;
&lt;li&gt;Later, the user enables a Gmail feature.&lt;/li&gt;
&lt;li&gt;Only then does your application request the necessary Gmail scope.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is a clearer consent experience and a smaller permission surface.&lt;/p&gt;

&lt;p&gt;If you are implementing several third-party connections rather than managing every authorization flow independently, Corsair's &lt;a href="https://docs.corsair.dev/concepts/oauth" rel="noopener noreferrer"&gt;OAuth 2.0 authentication documentation&lt;/a&gt; demonstrates how OAuth connections, callbacks, encrypted token storage, and token refresh can be handled through a common integration layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing Google One Tap, Automatic Sign-In, and Sign-Out Flows
&lt;/h2&gt;

&lt;p&gt;One of the most important concepts in modern Google Identity Services is the separation between authentication and authorization.&lt;/p&gt;

&lt;p&gt;Authentication answers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who is this user?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Authorization answers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Google resources has this user allowed the application to access?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One Tap belongs primarily to authentication.&lt;/p&gt;

&lt;p&gt;If your application only needs basic identity information through scopes such as &lt;code&gt;openid&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, and &lt;code&gt;profile&lt;/code&gt;, Google recommends considering Sign In With Google rather than building a broader API authorization flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing Google One Tap
&lt;/h3&gt;

&lt;p&gt;Google One Tap allows an eligible user to authenticate without navigating through a traditional sign-in page.&lt;/p&gt;

&lt;p&gt;A basic JavaScript initialization can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_GOOGLE_CLIENT_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;handleCredentialResponse&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When authentication succeeds, the credential response contains an ID token. Your application should send that credential to a trusted backend where it can be verified before creating or restoring an application session.&lt;/p&gt;

&lt;p&gt;One Tap should not be treated as the mechanism that automatically gives your application permission to read Drive files, send Gmail messages, or modify Calendar events.&lt;/p&gt;

&lt;p&gt;Those actions require a separate authorization flow and the appropriate Google API scopes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic Sign-In
&lt;/h3&gt;

&lt;p&gt;Google Identity Services can automatically select an eligible returning account in supported situations.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_GOOGLE_CLIENT_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;handleCredentialResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;auto_select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When automatic selection is enabled and the user meets Google's eligibility requirements, authentication can complete with less interaction.&lt;/p&gt;

&lt;p&gt;Developers should still treat automatic sign-in as a UX optimization rather than an assumption. Browser behavior, user settings, Google sessions, FedCM support, privacy controls, and other conditions can affect whether automatic authentication occurs.&lt;/p&gt;

&lt;p&gt;Your application should therefore continue to provide a normal Sign In With Google path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handle Sign-Out Correctly
&lt;/h3&gt;

&lt;p&gt;A subtle problem appears when your application signs someone out locally while Google Identity Services still considers that user eligible for automatic selection.&lt;/p&gt;

&lt;p&gt;The result can become a loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user signs out.&lt;/li&gt;
&lt;li&gt;Your application destroys the local session.&lt;/li&gt;
&lt;li&gt;The page reloads.&lt;/li&gt;
&lt;li&gt;Automatic sign-in immediately authenticates the same Google account again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From the user's perspective, the sign-out button appears broken.&lt;/p&gt;

&lt;p&gt;Google provides &lt;code&gt;disableAutoSelect()&lt;/code&gt; specifically for this situation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;signOut&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disableAutoSelect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Destroy your application session here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google recommends calling this method when the user signs out of your website so automatic selection does not immediately recreate the session.&lt;/p&gt;

&lt;p&gt;Remember that application sign-out, Google account sign-out, and OAuth consent revocation are three different actions.&lt;/p&gt;

&lt;p&gt;Signing out of your application should usually terminate your application session. Revoking OAuth consent is a separate decision and should normally be used when the user explicitly disconnects their Google account or removes an integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing Google OAuth Tokens: DPoP, Token Storage, Rotation, and Least Privilege Access
&lt;/h2&gt;

&lt;p&gt;A successful authorization flow is only the beginning. Token handling determines whether the integration remains secure after the user closes the consent screen.&lt;/p&gt;

&lt;p&gt;OAuth commonly involves three important credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authorization code:&lt;/strong&gt; Temporary credential exchanged by the backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access token:&lt;/strong&gt; Short-lived credential used when calling Google APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh token:&lt;/strong&gt; Longer-lived credential that can obtain new access tokens without requiring the user to complete authorization every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The authorization code flow is especially useful for applications that need Google API access when the user is not actively present because the backend can securely retain the refresh token.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep Long-Lived Tokens on the Server
&lt;/h3&gt;

&lt;p&gt;Refresh tokens are highly valuable credentials.&lt;/p&gt;

&lt;p&gt;If someone obtains a valid refresh token, they may be able to continue obtaining access tokens until the authorization is revoked or the credential otherwise becomes invalid.&lt;/p&gt;

&lt;p&gt;Good Google OAuth best practices include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store tokens in a protected server-side datastore.&lt;/li&gt;
&lt;li&gt;Encrypt sensitive credentials at rest.&lt;/li&gt;
&lt;li&gt;Restrict which application services can retrieve them.&lt;/li&gt;
&lt;li&gt;Never expose refresh tokens to an AI model prompt or browser when the backend can perform the API request instead.&lt;/li&gt;
&lt;li&gt;Prevent credentials from appearing in logs, analytics events, traces, or error-reporting systems.&lt;/li&gt;
&lt;li&gt;Separate credentials by account and tenant.&lt;/li&gt;
&lt;li&gt;Revoke credentials when a user intentionally disconnects an integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When an application serves multiple organizations or users, credential isolation becomes particularly important. Corsair's &lt;a href="https://docs.corsair.dev/concepts/multi-tenancy" rel="noopener noreferrer"&gt;multi-tenancy documentation&lt;/a&gt; shows a model in which credentials, database operations, and API calls are scoped to an individual tenant rather than sharing one global credential context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Refresh Access Tokens Instead of Reauthorizing Users
&lt;/h3&gt;

&lt;p&gt;Access tokens expire.&lt;/p&gt;

&lt;p&gt;For applications using the authorization code flow with offline access, a stored refresh token can obtain another access token without sending the user through the consent flow each time.&lt;/p&gt;

&lt;p&gt;Google client libraries can automate much of this process. If you implement token refresh yourself, your application needs to recognize expired credentials, securely call the token endpoint, persist updated token information when appropriate, and handle refresh failures.&lt;/p&gt;

&lt;p&gt;Do not solve token expiration by repeatedly asking users to reconnect unless the refresh token is genuinely unavailable, expired, revoked, or invalid.&lt;/p&gt;

&lt;p&gt;Also avoid continuously generating new refresh tokens. Google applies limits to the number of refresh tokens issued for user and client combinations, and excessive issuance can eventually cause older tokens to stop working.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understand What DPoP Adds
&lt;/h3&gt;

&lt;p&gt;DPoP, or Demonstrating Proof of Possession, adds another security property to OAuth token operations.&lt;/p&gt;

&lt;p&gt;A normal bearer credential can potentially be used by whoever possesses it. DPoP introduces a cryptographic key and requires the client to prove possession of the associated private key during supported token operations.&lt;/p&gt;

&lt;p&gt;Google currently supports optional DPoP for its web server OAuth token exchange. When DPoP is used during the exchange, the resulting refresh token is bound to the corresponding key. Subsequent refresh operations need proofs signed using that same private key.&lt;/p&gt;

&lt;p&gt;Google recommends protecting that private key with mechanisms such as hardware-backed storage where possible.&lt;/p&gt;

&lt;p&gt;An important implementation detail is that Google's access tokens still use the &lt;code&gt;Bearer&lt;/code&gt; token type even when DPoP is used. The additional protection applies to supported token endpoint interactions and the DPoP-bound refresh token rather than turning the Google access token itself into a DPoP access token.&lt;/p&gt;

&lt;p&gt;That distinction matters when designing your security model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apply Least Privilege Beyond Scopes
&lt;/h3&gt;

&lt;p&gt;Least privilege does not end after selecting OAuth scopes.&lt;/p&gt;

&lt;p&gt;You should also control what your own application can do with those permissions.&lt;/p&gt;

&lt;p&gt;Imagine an application receives permission to modify Google Calendar. That does not necessarily mean every feature, background job, AI agent, or user role should be capable of deleting events.&lt;/p&gt;

&lt;p&gt;Authorization should therefore exist at several layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Google OAuth scopes determine what the Google credential permits.&lt;/li&gt;
&lt;li&gt;Your application permissions determine which users can trigger particular operations.&lt;/li&gt;
&lt;li&gt;Your integration layer determines which tools and endpoints are exposed.&lt;/li&gt;
&lt;li&gt;Approval controls can protect destructive or sensitive operations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For developers who need to manage authentication methods, encrypted credentials, refresh behavior, and tenant-specific credentials through a common layer, the &lt;a href="https://docs.corsair.dev/concepts/auth" rel="noopener noreferrer"&gt;Corsair authentication documentation&lt;/a&gt; covers the credential lifecycle and storage model used by Corsair.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Google OAuth Errors and How to Troubleshoot Them
&lt;/h2&gt;

&lt;p&gt;Most Google OAuth errors become much easier to fix once you identify which stage of the flow failed.&lt;/p&gt;

&lt;p&gt;Was the authorization request rejected? Did the callback fail? Did the token exchange fail? Did a previously valid refresh token stop working? Did Google reject the final API request?&lt;/p&gt;

&lt;p&gt;Debugging the flow stage first prevents developers from randomly changing credentials and scopes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;redirect_uri_mismatch&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is one of the most common Google OAuth errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it means:&lt;/strong&gt; The redirect URI submitted by your application does not exactly match an authorized redirect URI associated with the OAuth client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP versus HTTPS&lt;/li&gt;
&lt;li&gt;Domain and subdomain&lt;/li&gt;
&lt;li&gt;Port&lt;/li&gt;
&lt;li&gt;Callback path&lt;/li&gt;
&lt;li&gt;Capitalization&lt;/li&gt;
&lt;li&gt;Trailing slash&lt;/li&gt;
&lt;li&gt;Environment configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google explicitly requires the redirect URI to match the registered value, including scheme, case, and trailing slash.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;invalid_client&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it means:&lt;/strong&gt; Google could not validate the OAuth client.&lt;/p&gt;

&lt;p&gt;For a server-based flow, check whether the client ID and client secret belong to the same OAuth client and environment.&lt;/p&gt;

&lt;p&gt;This commonly appears when staging credentials reach production, an old secret remains in deployment configuration, or the application is using credentials for the wrong OAuth client type.&lt;/p&gt;

&lt;p&gt;Google documents incorrect OAuth client credentials as a cause of &lt;code&gt;invalid_client&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;invalid_grant&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This error is more ambiguous because it can relate to several credential problems.&lt;/p&gt;

&lt;p&gt;The authorization code or refresh token may be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Invalid&lt;/li&gt;
&lt;li&gt;Expired&lt;/li&gt;
&lt;li&gt;Revoked&lt;/li&gt;
&lt;li&gt;Already used where reuse is not allowed&lt;/li&gt;
&lt;li&gt;Associated with a different redirect URI&lt;/li&gt;
&lt;li&gt;Otherwise inconsistent with the authorization request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google's token endpoint documentation identifies invalid, expired, revoked, or mismatched grants as common causes of &lt;code&gt;invalid_grant&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the problem involves a refresh token, determine whether the user revoked access, the token became invalid, or your application stored the wrong credential before sending the user through OAuth again.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;access_denied&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;access_denied&lt;/code&gt; can simply mean that the user declined the authorization request.&lt;/p&gt;

&lt;p&gt;Do not automatically treat this as an application failure.&lt;/p&gt;

&lt;p&gt;Your interface should return the user to a safe application state and clearly explain that the requested feature cannot work without the requested permission.&lt;/p&gt;

&lt;p&gt;Avoid creating an authorization loop that immediately opens the consent dialog again.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;admin_policy_enforced&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Google Workspace administrators can restrict applications or scopes that users inside their organization are allowed to authorize.&lt;/p&gt;

&lt;p&gt;If OAuth works for personal Google accounts but fails for users from a particular organization, administrator policy should be part of your investigation.&lt;/p&gt;

&lt;p&gt;Google documents &lt;code&gt;admin_policy_enforced&lt;/code&gt; when Workspace administrator policies prevent the requested authorization.&lt;/p&gt;

&lt;p&gt;Your application may need to provide instructions that an affected customer can share with their Workspace administrator.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;org_internal&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This error can appear when the OAuth application's audience is restricted to accounts associated with a particular Google Cloud organization.&lt;/p&gt;

&lt;p&gt;If outside users need access, review how the application's audience and OAuth configuration are defined.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;invalid_scope&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This generally means the requested scope is invalid, unknown, malformed, or inappropriate for the request.&lt;/p&gt;

&lt;p&gt;Instead of copying large lists of scopes from another implementation, define the exact Google APIs your product uses and verify the current scope identifiers for those APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Better OAuth Troubleshooting Process
&lt;/h3&gt;

&lt;p&gt;When Google OAuth errors appear in production, debug them systematically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify the exact OAuth stage that failed.&lt;/li&gt;
&lt;li&gt;Record the Google error code without logging credentials.&lt;/li&gt;
&lt;li&gt;Confirm the OAuth client ID being used.&lt;/li&gt;
&lt;li&gt;Verify the redirect URI character for character.&lt;/li&gt;
&lt;li&gt;Compare the requested scopes with the intended product capability.&lt;/li&gt;
&lt;li&gt;Check whether the user belongs to a managed Google Workspace environment.&lt;/li&gt;
&lt;li&gt;Confirm whether an existing refresh token is expired or revoked.&lt;/li&gt;
&lt;li&gt;Review recent OAuth configuration or deployment changes.&lt;/li&gt;
&lt;li&gt;Require reconnection only when the existing authorization can no longer be recovered.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Observability is valuable, but OAuth logs should contain metadata rather than secrets. Record tenant identifiers, provider names, error codes, request stages, and timestamps instead of access tokens, refresh tokens, client secrets, or authorization codes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Firebase Authentication vs Google Cloud Identity Platform: Choosing the Right Authentication Setup
&lt;/h2&gt;

&lt;p&gt;Firebase Authentication, Google Cloud Identity Platform, and direct Google OAuth implementation solve overlapping identity problems, but they are not identical choices.&lt;/p&gt;

&lt;p&gt;The right option depends on whether you are primarily authenticating users into your application or building a broader identity architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firebase Authentication
&lt;/h3&gt;

&lt;p&gt;Firebase Authentication is well suited to applications that want a straightforward way to support user authentication across web and mobile experiences.&lt;/p&gt;

&lt;p&gt;It provides SDK-based support for common authentication methods and integrates naturally with the wider Firebase ecosystem.&lt;/p&gt;

&lt;p&gt;It is often a practical choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are building a consumer application.&lt;/li&gt;
&lt;li&gt;Your application already relies heavily on Firebase.&lt;/li&gt;
&lt;li&gt;You want common sign-in methods without building your own identity backend.&lt;/li&gt;
&lt;li&gt;You do not require advanced enterprise identity capabilities.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Google Cloud Identity Platform
&lt;/h3&gt;

&lt;p&gt;Identity Platform builds on the same underlying identity technology while adding capabilities designed for more complex and enterprise-oriented applications.&lt;/p&gt;

&lt;p&gt;Google currently lists additional Identity Platform capabilities such as multi-factor authentication, blocking functions, SAML, OpenID Connect, multi-tenancy, Identity-Aware Proxy integration, and an enterprise uptime SLA.&lt;/p&gt;

&lt;p&gt;It becomes more relevant when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are operating a multi-tenant SaaS product.&lt;/li&gt;
&lt;li&gt;Enterprise customers require SAML or OIDC identity providers.&lt;/li&gt;
&lt;li&gt;Authentication workflows need additional controls.&lt;/li&gt;
&lt;li&gt;Identity infrastructure needs to fit more deeply into Google Cloud.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google describes Firebase Authentication as being aimed primarily at consumer applications while Identity Platform is positioned toward enterprise-focused SaaS applications and more advanced identity requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Direct Google OAuth
&lt;/h3&gt;

&lt;p&gt;There is another important distinction.&lt;/p&gt;

&lt;p&gt;Neither Firebase Authentication nor Identity Platform automatically replaces Google OAuth authorization when your application needs to act on a user's Google data.&lt;/p&gt;

&lt;p&gt;Signing a user into your application is different from receiving permission to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read their Google Calendar&lt;/li&gt;
&lt;li&gt;Send Gmail messages&lt;/li&gt;
&lt;li&gt;Access Google Drive&lt;/li&gt;
&lt;li&gt;Modify Google Sheets&lt;/li&gt;
&lt;li&gt;Call other protected Google APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the product needs those capabilities, you still need to design the appropriate authorization flow and obtain access tokens with the necessary scopes.&lt;/p&gt;

&lt;p&gt;A useful way to make the decision is therefore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Need user identity:&lt;/strong&gt; Consider Sign In With Google, Firebase Authentication, or Identity Platform depending on the broader authentication architecture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need access to Google APIs:&lt;/strong&gt; Implement OAuth authorization with the appropriate scopes and token lifecycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need both:&lt;/strong&gt; Separate authentication from authorization so users understand when they are signing into your application and when they are granting access to their Google data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building Google OAuth for Production
&lt;/h2&gt;

&lt;p&gt;Google OAuth implementation is not difficult because of the redirect to Google itself. The real engineering work is everything around that redirect: scope design, callback security, credential storage, token refresh, account isolation, error recovery, and protecting sensitive operations after authorization succeeds.&lt;/p&gt;

&lt;p&gt;Start with the smallest permissions your application needs. Keep sensitive credentials on trusted infrastructure. Separate Sign In With Google from Google API authorization. Expect tokens to expire and permissions to change. Most importantly, design reconnect and troubleshooting paths before users encounter failures in production.&lt;/p&gt;

&lt;p&gt;If your application needs to connect Google APIs alongside other services, &lt;a href="https://corsair.dev/" rel="noopener noreferrer"&gt;Corsair&lt;/a&gt; provides an open-source integration layer for handling application integrations, OAuth, credentials, token refresh, and multi-tenant connections.&lt;/p&gt;

&lt;p&gt;Instead of rebuilding the same authentication infrastructure for every provider, developers can use a common integration model while keeping credentials within their application infrastructure.&lt;/p&gt;

&lt;p&gt;That becomes increasingly useful as a product expands from one Google integration to multiple Google services and third-party APIs.&lt;/p&gt;

&lt;p&gt;The goal is not to hide OAuth, but to reduce the repeated infrastructure required to operate it safely at production scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. What Is the Difference Between Google Sign-In and Google OAuth 2.0?
&lt;/h3&gt;

&lt;p&gt;Google Sign-In primarily authenticates the user and tells your application who they are. Google OAuth authorization allows your application to request permission to access Google APIs on the user's behalf.&lt;/p&gt;

&lt;p&gt;Modern Google Identity Services deliberately separates these authentication and authorization flows.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Why Am I Getting &lt;code&gt;redirect_uri_mismatch&lt;/code&gt; in Google OAuth?
&lt;/h3&gt;

&lt;p&gt;The redirect URI sent by your application does not exactly match one registered for the OAuth client.&lt;/p&gt;

&lt;p&gt;Check the protocol, hostname, port, callback path, capitalization, and trailing slash. Even a small difference can cause the request to fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Where Should Google OAuth Refresh Tokens Be Stored?
&lt;/h3&gt;

&lt;p&gt;Refresh tokens should generally be stored in secure server-side storage rather than frontend JavaScript or browser-accessible storage.&lt;/p&gt;

&lt;p&gt;Protect them with encryption at rest, restrict application access, prevent them from entering logs, and isolate credentials between users or tenants.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Should an Application Request All Google OAuth Scopes During Initial Sign-In?
&lt;/h3&gt;

&lt;p&gt;Usually no. Google recommends incremental authorization so applications can request additional scopes when users access features that actually require them.&lt;/p&gt;

&lt;p&gt;This supports least privilege and gives users clearer context for each permission request.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. What Should an Application Do When a Google OAuth Refresh Token Stops Working?
&lt;/h3&gt;

&lt;p&gt;First determine why the token became invalid rather than immediately restarting OAuth.&lt;/p&gt;

&lt;p&gt;The user may have revoked access, the token may have expired or become invalid, or the application may be using the wrong credential. If the authorization can no longer be refreshed, ask the user to reconnect their Google account and create a new valid authorization.&lt;/p&gt;

</description>
      <category>oauth</category>
    </item>
    <item>
      <title>Corsair vs Composio: Why Corsair Is Better for Production AI Agent Integrations</title>
      <dc:creator>Corsair</dc:creator>
      <pubDate>Fri, 04 Sep 2026 15:23:05 +0000</pubDate>
      <link>https://dev.to/corsairdev/corsair-vs-composio-why-corsair-is-better-for-production-ai-agent-integrations-c2b</link>
      <guid>https://dev.to/corsairdev/corsair-vs-composio-why-corsair-is-better-for-production-ai-agent-integrations-c2b</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjmg8z877du21qd4zqcqv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjmg8z877du21qd4zqcqv.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;Giving an AI agent access to Gmail, Slack, GitHub, Notion, Salesforce, or hundreds of other applications is relatively easy in a prototype. Making those integrations reliable enough for a production product is a different problem.&lt;/p&gt;

&lt;p&gt;Once real customers are involved, developers have to think beyond whether an agent can successfully call a tool. They need to decide where OAuth credentials live, how customer data is isolated, what happens when external data changes, which actions require approval, how integrations are used outside the agent, and how execution costs behave when autonomous agents start making thousands of calls.&lt;/p&gt;

&lt;p&gt;That makes the &lt;strong&gt;Corsair vs Composio&lt;/strong&gt; decision less about who has the longest integration catalog and more about the architecture behind those integrations. Composio provides a large managed ecosystem for discovering, authenticating, and executing tools. Corsair takes a different approach: integrations run as part of your application, credentials and synced data remain under your control, and the same integration layer can serve agents, backend services, workflows, and customer facing product features.&lt;/p&gt;

&lt;p&gt;For teams evaluating a &lt;strong&gt;Composio alternative&lt;/strong&gt; for &lt;strong&gt;production AI agent integrations&lt;/strong&gt;, these architectural differences become increasingly important as an application moves from experimentation to serving real users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Source Integration Logic vs Managed Execution Infrastructure
&lt;/h2&gt;

&lt;p&gt;Open source can mean different things in an &lt;strong&gt;AI agent integration platform&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Composio's SDK is itself open source under the MIT license, and its TypeScript package is intentionally inspectable. Composio also publishes provider adapters for frameworks such as OpenAI, Anthropic, Vercel AI SDK, LangChain, and others. So describing the entire Composio platform as closed source would be inaccurate.&lt;/p&gt;

&lt;p&gt;The more meaningful difference is where the actual integration implementation and execution responsibility sit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Corsair:&lt;/strong&gt; Integration plugins are open TypeScript packages that developers can inspect and modify. Corsair runs inside the developer's own application, allowing teams to understand how an API operation, authentication flow, webhook handler, or data synchronization process works rather than treating the integration layer purely as an external service. Corsair also allows developers to extend missing integrations rather than waiting exclusively on a vendor roadmap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composio:&lt;/strong&gt; The SDK is open source, but its standard architecture relies heavily on Composio managed services for connected accounts, tool execution, sessions, triggers, and its remote sandbox. Tool execution is generally routed through Composio's infrastructure rather than running entirely as application owned integration code.&lt;/p&gt;

&lt;p&gt;That distinction matters when debugging production failures.&lt;/p&gt;

&lt;p&gt;If a provider changes an endpoint, returns an unexpected payload, or introduces a new authentication requirement, having access to the integration implementation can make it easier to inspect exactly what happened and adapt the behavior.&lt;/p&gt;

&lt;p&gt;Open source integration logic also reduces a different kind of dependency. Instead of asking whether a vendor currently supports a required endpoint, a development team can ask whether it has enough control to implement the endpoint itself.&lt;/p&gt;

&lt;p&gt;For organizations that consider code ownership part of their infrastructure strategy, Corsair's approach to &lt;strong&gt;open source AI integrations&lt;/strong&gt; can therefore be more attractive than relying primarily on a managed execution layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customer Controlled Credentials and Data vs Third Party Hosted Integration State
&lt;/h2&gt;

&lt;p&gt;Credentials are one of the most important architectural decisions in an agent system.&lt;/p&gt;

&lt;p&gt;An AI agent might eventually connect to email accounts, CRMs, internal documents, payment providers, support systems, calendars, and developer infrastructure. The OAuth tokens behind those connections can provide significant access to a customer's business.&lt;/p&gt;

&lt;p&gt;Corsair is designed around keeping those credentials inside the customer's infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Corsair:&lt;/strong&gt; OAuth tokens are stored in the application's own database and encrypted using the application's Key Encryption Key. Corsair Hub can assist with OAuth callbacks and token refresh, but Corsair states that access and refresh tokens are stored within customer infrastructure rather than retained by Hub. API calls are made from the customer's application to the underlying provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composio:&lt;/strong&gt; Its standard connected account model stores user credentials against a Composio user ID and manages token refresh for those accounts. Composio supports customer supplied OAuth applications and other authentication configurations, but connected accounts remain part of the Composio platform's credential lifecycle.&lt;/p&gt;

&lt;p&gt;Neither model automatically makes an application secure. Developers still need proper encryption, access controls, infrastructure security, scope management, auditing, and credential rotation.&lt;/p&gt;

&lt;p&gt;The difference is ownership.&lt;/p&gt;

&lt;p&gt;With Corsair, your application remains the primary location for integration credentials and synchronized data. This can be important for companies with strict requirements around data residency, security reviews, customer isolation, or minimizing the number of systems that hold sensitive integration credentials.&lt;/p&gt;

&lt;p&gt;This model also extends beyond tokens.&lt;/p&gt;

&lt;p&gt;Corsair can persist data returned through API calls and webhook events in the application's own database. That means external integration data can become part of the same data architecture that already powers the rest of the product.&lt;/p&gt;

&lt;p&gt;For a production AI product, that changes the question from:&lt;/p&gt;

&lt;p&gt;"Which service stores my integration?"&lt;/p&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;p&gt;"How does this integration become part of my application's own infrastructure?"&lt;/p&gt;

&lt;h2&gt;
  
  
  A Unified Integration SDK vs Framework Specific Integration Packages
&lt;/h2&gt;

&lt;p&gt;AI products rarely remain pure chat interfaces.&lt;/p&gt;

&lt;p&gt;An integration that starts as an agent tool often needs to appear elsewhere in the product.&lt;/p&gt;

&lt;p&gt;A support agent might read Zendesk tickets, while the support dashboard displays the same ticket information.&lt;/p&gt;

&lt;p&gt;A sales agent might create CRM records, while a scheduled backend process synchronizes those accounts every night.&lt;/p&gt;

&lt;p&gt;A calendar agent might schedule meetings, while the application's normal interface needs to display upcoming events.&lt;/p&gt;

&lt;p&gt;This is where an integration layer needs to serve the entire product rather than only the model.&lt;/p&gt;

&lt;p&gt;Corsair's core design allows integrations to be called directly from application code while also being exposed to agents through adapters. The &lt;a href="https://docs.corsair.dev/introduction" rel="noopener noreferrer"&gt;Corsair documentation&lt;/a&gt; describes the SDK as an integration layer for both apps and agents, with OAuth, token refresh, webhooks, rate limits, and provider operations handled through a consistent syntax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Corsair:&lt;/strong&gt; The same underlying integration client can support application logic, backend jobs, customer interfaces, database reads, workflows, and agent execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composio:&lt;/strong&gt; Composio provides a core SDK plus provider adapters that convert Composio tools into formats expected by different agent frameworks. Its open source repository includes separate integrations for OpenAI, Anthropic, Vercel AI SDK, LangChain, LlamaIndex, and other environments.&lt;/p&gt;

&lt;p&gt;Both approaches can work.&lt;/p&gt;

&lt;p&gt;The advantage Corsair emphasizes is that agents do not have to become the center of the integration architecture. The underlying integration remains normal application infrastructure that an agent can use when necessary.&lt;/p&gt;

&lt;p&gt;This becomes particularly valuable when the product evolves.&lt;/p&gt;

&lt;p&gt;You might start by giving an agent the ability to search Slack. Later, you may want the same Slack integration to populate a dashboard, trigger a workflow, run from a scheduled backend job, or support a normal "Send to Slack" button.&lt;/p&gt;

&lt;p&gt;With an application centric integration layer, those use cases can continue using the same integration infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persistent Syncs, Webhooks, and Workflows Beyond One Off Tool Execution
&lt;/h2&gt;

&lt;p&gt;Modern agent integrations cannot depend entirely on live API calls.&lt;/p&gt;

&lt;p&gt;Imagine an agent that needs to answer:&lt;/p&gt;

&lt;p&gt;"Which customer issues appeared in Slack, GitHub, and HubSpot during the last month?"&lt;/p&gt;

&lt;p&gt;Calling every provider API during the reasoning loop would increase latency, consume API quotas, and require the model to repeatedly process large responses.&lt;/p&gt;

&lt;p&gt;Persistent integration data provides another approach.&lt;/p&gt;

&lt;p&gt;Corsair can store API responses and incoming webhook data in the application's database. Its database abstraction allows product code to query synchronized entities locally instead of making another provider request every time the data is needed. Corsair documents that API calls and webhook events can update the same local entity data used by the application.&lt;/p&gt;

&lt;p&gt;This creates two complementary access patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live API access:&lt;/strong&gt; Use the provider API when the application needs current data or wants to create, update, or delete something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local synchronized access:&lt;/strong&gt; Use the application's database when the product repeatedly needs previously synchronized information.&lt;/p&gt;

&lt;p&gt;Composio also supports triggers. Its trigger system can receive events from connected applications through webhooks or provider polling and forward structured events to an application's webhook endpoint. It would therefore be inaccurate to say Composio has no webhook support.&lt;/p&gt;

&lt;p&gt;The architectural difference is that Corsair connects these events directly with an application owned persistence model.&lt;/p&gt;

&lt;p&gt;Corsair is also expanding this model into durable workflows. &lt;a href="https://docs.corsair.dev/workflows/overview" rel="noopener noreferrer"&gt;Corsair Workflows&lt;/a&gt; can chain integration operations across multiple services, pause between steps, react to webhook events, and resume after retries while execution occurs inside the customer's application. The workflow system is currently documented as beta.&lt;/p&gt;

&lt;p&gt;For example, a production workflow could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Receive a new CRM opportunity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retrieve information about the account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a task in Linear.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Notify the relevant Slack channel.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wait for another event.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Continue when new customer information arrives.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent may initiate or participate in that workflow, but it does not have to personally orchestrate every infrastructure operation through repeated reasoning steps.&lt;/p&gt;

&lt;p&gt;That separation becomes increasingly useful as AI products become more autonomous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built In Multi Tenant Isolation for Customer Facing AI Products
&lt;/h2&gt;

&lt;p&gt;A single user prototype can get away with one Slack token and one Gmail connection.&lt;/p&gt;

&lt;p&gt;A SaaS product cannot.&lt;/p&gt;

&lt;p&gt;Once hundreds or thousands of customers connect their applications, every operation needs to resolve the correct credentials, data, webhooks, permissions, and account context.&lt;/p&gt;

&lt;p&gt;Corsair makes tenant context explicit.&lt;/p&gt;

&lt;p&gt;When multi tenant mode is enabled, operations are scoped through &lt;code&gt;withTenant()&lt;/code&gt;. Corsair's documentation states that API operations, database queries, credentials, and incoming webhook data are scoped using that tenant context. Direct plugin access is prevented when multi tenant mode is active, which helps make forgotten tenant scoping visible during development.&lt;/p&gt;

&lt;p&gt;For example, when an application runs an operation for Customer A, the integration layer must ensure that it cannot accidentally resolve Customer B's credentials.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but autonomous agents increase the number of places where this boundary needs to hold.&lt;/p&gt;

&lt;p&gt;An agent may:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Receive a user instruction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Search stored integration data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select a tool.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retrieve credentials.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Execute an external API request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Process a webhook later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Resume a workflow hours later.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tenant context needs to survive every one of those stages.&lt;/p&gt;

&lt;p&gt;Composio also provides multi user isolation. Sessions are created for a &lt;code&gt;userID&lt;/code&gt;, connected accounts are associated with users, and private connections are restricted to their owners by default. Composio additionally supports shared connections with access control lists.&lt;/p&gt;

&lt;p&gt;The distinction is therefore not that Composio lacks multi user support.&lt;/p&gt;

&lt;p&gt;Corsair's advantage is the way tenant scoping is incorporated into its local application and database architecture. The same tenant context governs credentials, locally stored entities, API operations, approvals, and webhooks.&lt;/p&gt;

&lt;p&gt;For customer facing &lt;strong&gt;production AI agent integrations&lt;/strong&gt;, that consistency can simplify the security model developers need to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human Approval and Permission Gates for High Risk Agent Actions
&lt;/h2&gt;

&lt;p&gt;Authentication answers one question:&lt;/p&gt;

&lt;p&gt;"Can this user access Gmail?"&lt;/p&gt;

&lt;p&gt;Authorization needs to answer another:&lt;/p&gt;

&lt;p&gt;"Should this agent be allowed to send this particular email right now?"&lt;/p&gt;

&lt;p&gt;That difference becomes crucial when agents move from reading information to taking action.&lt;/p&gt;

&lt;p&gt;Reading a calendar event is not equivalent to deleting one.&lt;/p&gt;

&lt;p&gt;Searching a CRM is not equivalent to removing a customer record.&lt;/p&gt;

&lt;p&gt;Drafting an email is not equivalent to sending it.&lt;/p&gt;

&lt;p&gt;Production agents therefore need permission controls closer to the actual tool execution layer.&lt;/p&gt;

&lt;p&gt;Corsair gives integrations risk aware permission modes. Endpoints can be classified around read, write, and destructive behavior, while policies determine whether an operation is allowed, denied, or requires approval.&lt;/p&gt;

&lt;p&gt;When approval is required, Corsair can create a pending permission record and block execution until the action is approved. Approved actions are single use, and Corsair Hub can provide an approval URL to the user. The &lt;a href="https://docs.corsair.dev/concepts/permissions" rel="noopener noreferrer"&gt;Corsair permissions documentation&lt;/a&gt; also supports per endpoint overrides for more granular policies.&lt;/p&gt;

&lt;p&gt;This enables a practical pattern:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read:&lt;/strong&gt; Execute automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Normal write:&lt;/strong&gt; Execute automatically or require approval depending on policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sensitive write:&lt;/strong&gt; Ask for approval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Destructive action:&lt;/strong&gt; Require approval or block completely.&lt;/p&gt;

&lt;p&gt;Composio also provides important safety controls. Sessions can restrict enabled toolkits, exact tool slugs, and behavioral tags such as destructive actions. These restrictions are enforced during execution. Developers can also create approval behavior using execution modifiers or supported framework hooks.&lt;/p&gt;

&lt;p&gt;The distinction is that Corsair provides the approval lifecycle as a first class part of its integration layer, including stored permission requests, approval states, tenant context, expiry behavior, and hosted or custom review flows.&lt;/p&gt;

&lt;p&gt;For agents performing consequential actions, this gives developers a clear separation between what an agent wants to do and what the system ultimately permits it to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Predictable Integration Costs as Agent Tool Usage Scales
&lt;/h2&gt;

&lt;p&gt;Agent usage patterns are different from traditional SaaS API usage.&lt;/p&gt;

&lt;p&gt;A human may click one button and generate one request.&lt;/p&gt;

&lt;p&gt;An agent may perform tool discovery, inspect several resources, retrieve more context, execute an action, validate the result, and continue reasoning.&lt;/p&gt;

&lt;p&gt;A single user instruction can therefore generate many integration operations.&lt;/p&gt;

&lt;p&gt;That makes per call pricing an architectural consideration rather than simply a procurement detail.&lt;/p&gt;

&lt;p&gt;Corsair's current pricing lists unlimited tool calls across its plans. The free Hobby plan currently includes up to 50 connections and 100,000 webhook events, while the Pro plan lists unlimited tool calls, connections, and webhooks.&lt;/p&gt;

&lt;p&gt;Composio's current pricing includes 100,000 monthly tool calls on its free plan. Its published overage rate for tool calls is currently $0.0003 per call, with additional usage rates applying to certain features and execution paths.&lt;/p&gt;

&lt;p&gt;That does not automatically mean one platform will always cost less.&lt;/p&gt;

&lt;p&gt;A team should evaluate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent behavior:&lt;/strong&gt; How many integration calls does a normal user request generate?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User growth:&lt;/strong&gt; How quickly will connected accounts increase?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger volume:&lt;/strong&gt; How many incoming events does the application process?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution architecture:&lt;/strong&gt; Which operations run through an external platform versus inside the application?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Usage variability:&lt;/strong&gt; Can autonomous loops unexpectedly multiply execution volume?&lt;/p&gt;

&lt;p&gt;For a product where tool execution becomes extremely frequent, unlimited tool calls can make costs easier to forecast. This is particularly relevant when agentic workflows execute repeatedly without a human manually initiating each action.&lt;/p&gt;

&lt;p&gt;Pricing can change, so teams should always evaluate current plans before making a purchasing decision. The larger architectural question is whether the pricing model scales in the same direction as the product's expected agent behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, Is Corsair Better Than Composio for Production AI Agent Integrations?
&lt;/h2&gt;

&lt;p&gt;There is no universal answer for every AI application.&lt;/p&gt;

&lt;p&gt;Composio offers a large toolkit catalog, managed authentication, agent sessions, triggers, tool discovery, remote sandbox execution, and integrations with many popular agent frameworks. For teams that prioritize managed infrastructure and rapid access to a broad tool ecosystem, that can be attractive.&lt;/p&gt;

&lt;p&gt;Corsair becomes especially compelling when the integration layer needs to become a permanent part of the product's architecture.&lt;/p&gt;

&lt;p&gt;Choose Corsair when you value:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code ownership:&lt;/strong&gt; Integration implementations are open and extensible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure control:&lt;/strong&gt; Integrations execute within your application environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credential ownership:&lt;/strong&gt; User credentials remain within your database architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persistent data:&lt;/strong&gt; API responses and webhook events can become locally queryable application data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product reuse:&lt;/strong&gt; The same integrations can support agents, UI features, backend jobs, and workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tenant isolation:&lt;/strong&gt; Credentials, database records, webhooks, and operations remain scoped to the appropriate customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent safety:&lt;/strong&gt; Sensitive operations can be gated behind explicit permission policies and approval flows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predictable execution economics:&lt;/strong&gt; Tool calls are not metered individually under Corsair's current plans.&lt;/p&gt;

&lt;p&gt;The larger lesson from the Corsair vs Composio comparison is that choosing an &lt;strong&gt;AI agent integration platform&lt;/strong&gt; should not stop at counting connectors. The platform becomes part of your authentication architecture, security boundary, application data model, and eventually your product infrastructure.&lt;/p&gt;

&lt;p&gt;As agents gain more autonomy, developers need integration infrastructure that remains understandable and controllable even when the model itself is making more decisions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://corsair.dev/" rel="noopener noreferrer"&gt;&lt;strong&gt;Corsair&lt;/strong&gt;&lt;/a&gt; is built around that idea: open source integrations that run alongside your application rather than turning your entire integration layer into an external black box. It gives developers one foundation for connecting agents, backend systems, customer facing features, synchronized data, permissions, and workflows. For teams moving from experiments to production AI products, that architecture can provide more control over how credentials, data, and actions flow through the system. Explore Corsair to see how an application owned integration layer can simplify the path from the first connected tool to a full production integration stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. What is the main difference between Corsair and Composio?
&lt;/h3&gt;

&lt;p&gt;The biggest difference is architectural. Composio provides a managed platform for authentication, tool discovery, connected accounts, sessions, and execution. Corsair runs its integration SDK within your application and is designed to keep credentials and synchronized integration data under your control. Both can connect AI agents to external applications, but Corsair places greater emphasis on application owned integration infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Is Corsair a Composio alternative for production AI agents?
&lt;/h3&gt;

&lt;p&gt;Yes. Corsair can serve as a &lt;strong&gt;Composio alternative&lt;/strong&gt; for developers building production agents that need OAuth management, API integrations, persistent data, webhooks, workflows, multi tenant isolation, and permission controls. It can also power non agent product features using the same integration layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Is Composio closed source?
&lt;/h3&gt;

&lt;p&gt;Not entirely. Composio's SDK and provider adapters are open source under the MIT license. However, many of its standard capabilities depend on Composio managed infrastructure for connected accounts, sessions, tool execution, triggers, and remote sandbox functionality. Corsair differs by making its integration implementations open source while running the SDK and integration execution inside the developer's application.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. How does Corsair protect sensitive AI agent actions?
&lt;/h3&gt;

&lt;p&gt;Corsair can classify integration operations by risk and apply permission policies that allow, deny, or require approval before execution. Sensitive actions can generate a review request, while developers can configure individual operation overrides. This allows an agent to perform safe reads automatically while placing additional controls around destructive or consequential actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Which platform is better for multi tenant AI applications?
&lt;/h3&gt;

&lt;p&gt;Both platforms support multiple users and isolated connections. Composio scopes connected accounts and sessions using user identities, while Corsair can enable multi tenant mode and require operations to run through a tenant scoped client. Corsair's model is particularly useful when developers want tenant isolation to extend across credentials, API calls, locally synchronized data, webhooks, permissions, and other application infrastructure.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>integration</category>
      <category>corsair</category>
    </item>
    <item>
      <title>Google Cloud Authentication for AI Agents: ADC, Workload Identity Federation, and Secure Identity Patterns</title>
      <dc:creator>Corsair</dc:creator>
      <pubDate>Fri, 04 Sep 2026 15:15:58 +0000</pubDate>
      <link>https://dev.to/corsairdev/google-cloud-authentication-for-ai-agents-adc-workload-identity-federation-and-secure-identity-38me</link>
      <guid>https://dev.to/corsairdev/google-cloud-authentication-for-ai-agents-adc-workload-identity-federation-and-secure-identity-38me</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyqteeltl56o8sfsfja4n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyqteeltl56o8sfsfja4n.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;An AI agent that reads a spreadsheet, updates a BigQuery table, or triggers a Cloud Run job needs to prove who it is before Google Cloud lets it do any of that. For a person, proving identity means a login screen. For an agent running unattended, sometimes on a laptop, sometimes inside a container, sometimes on infrastructure that isn't Google Cloud at all, there's no screen and no one around to type a password when a token expires.&lt;/p&gt;

&lt;p&gt;Google Cloud already has a mature answer to this problem. It was built for backend services long before agents existed, and most of it applies directly: Application Default Credentials discover the right identity automatically based on where code is running, Workload Identity Federation lets systems outside Google Cloud authenticate without a static key, and service account impersonation hands out short-lived, narrowly scoped tokens instead of long-lived secrets.&lt;/p&gt;

&lt;p&gt;The pieces exist. What's less obvious is how to combine them correctly for a system that acts autonomously, chains tool calls together, and processes untrusted input as part of its job.&lt;/p&gt;

&lt;p&gt;This guide walks through how Google Cloud authentication for AI agents actually works in practice: how Google Cloud ADC resolves credentials across local, cloud, and hybrid environments, how Workload Identity Federation and service account impersonation remove static keys from the picture, and how to design access that stays least privilege even when the one asking for it is an agent instead of a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Google Cloud Authentication Works for AI Agents Across Local, Cloud, and Hybrid Environments
&lt;/h2&gt;

&lt;p&gt;An AI agent that calls a Google Cloud API is, from Google's point of view, just another caller that needs to prove its identity before every request. What makes agents different from a typical backend service is that they run in more places, act with less direct supervision, and often chain many calls together in a single task.&lt;/p&gt;

&lt;p&gt;Google Cloud authentication for AI agents is built on the same primitives used for any workload: OAuth 2.0 access tokens, service accounts, Application Default Credentials for discovery, and Workload Identity Federation for anything running outside Google Cloud.&lt;/p&gt;

&lt;p&gt;An agent isn't locked into one method. Its authentication resolves differently depending on where it happens to be running.&lt;/p&gt;

&lt;p&gt;Three environments account for most of the pattern differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local development:&lt;/strong&gt; The agent runs on a developer's machine, and Google Cloud auth typically falls back to user credentials issued through the Google Cloud CLI, scoped to whatever that developer's own Google Account can access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud-native:&lt;/strong&gt; The agent runs inside Compute Engine, Cloud Run, GKE, or Cloud Functions, and Google Cloud auth resolves an attached service account automatically from the environment's metadata server, with no key files anywhere in the deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid or multi-cloud:&lt;/strong&gt; The agent runs on AWS, Azure, on-premises infrastructure, or inside a third-party runtime, and Google Cloud auth uses Workload Identity Federation to exchange an external identity token for a short-lived Google credential, without ever creating a static key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The authentication method should follow the deployment target, not the other way around. Hardcoding a service account key into an agent so it "just works everywhere" is exactly the pattern the rest of this guide argues against.&lt;/p&gt;

&lt;p&gt;It matters more for agents than for ordinary services, since an agent that acts autonomously, retries failed calls, and processes untrusted input needs a credential surface that stays small and predictable.&lt;/p&gt;

&lt;p&gt;Frameworks that connect agents to third-party services increasingly treat this as infrastructure rather than a one-off implementation detail, similar to how an integration layer that &lt;a href="https://docs.corsair.dev/concepts/auth" rel="noopener noreferrer"&gt;handles OAuth, API keys, and bot tokens automatically&lt;/a&gt; keeps credential logic consistent across every provider an agent talks to, not just Google Cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application Default Credentials: Credential Discovery, Precedence, and Deployment Behavior
&lt;/h2&gt;

&lt;p&gt;Application Default Credentials, often shortened to Google Cloud ADC, is the strategy Google's client libraries use to find credentials automatically based on the environment, so the same application code can run in development and production without conditional authentication logic.&lt;/p&gt;

&lt;p&gt;Rather than a credential type of its own, ADC is a lookup process, and understanding its precedence matters more than most teams assume.&lt;/p&gt;

&lt;p&gt;ADC checks for credentials in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/code&gt; environment variable, which points to a credential file. That file can be a service account key, an external account configuration for Workload Identity Federation or Workforce Identity Federation, or an authorized user file.&lt;/li&gt;
&lt;li&gt;A credential file created locally by running the Google Cloud CLI's application default login command, stored at a fixed path that depends on the operating system.&lt;/li&gt;
&lt;li&gt;The attached service account returned by the environment's metadata server, when the code runs on Compute Engine, Cloud Run, GKE, Cloud Functions, or App Engine's flexible environment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Google's own documentation is explicit that this order is a lookup sequence, not a ranking of trust or preference.&lt;/p&gt;

&lt;p&gt;That distinction matters in practice: a stray &lt;code&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/code&gt; variable left behind from local testing will silently take precedence in a production container, causing an agent to authenticate as the wrong identity without any error being thrown.&lt;/p&gt;

&lt;p&gt;It's worth explicitly checking for that variable during deployment rather than assuming ADC will resolve to the "obvious" identity.&lt;/p&gt;

&lt;p&gt;Deployment behavior follows from this precedence. In local development, ADC usually resolves broad user credentials, often wider in scope than what the agent should have once it's live.&lt;/p&gt;

&lt;p&gt;On Compute Engine, Cloud Run, and GKE, ADC resolves the attached service account with no files or environment variables to manage, and Google rotates that credential automatically behind the scenes.&lt;/p&gt;

&lt;p&gt;In CI/CD pipelines or on other clouds, ADC resolves through &lt;code&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/code&gt; pointing at a Workload Identity Federation configuration file, which is the pattern covered next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workload Identity Federation for Keyless Authentication Across Multi-Cloud and On-Premises Agents
&lt;/h2&gt;

&lt;p&gt;Workload Identity Federation lets Google Cloud trust credentials issued by an external identity provider—AWS, Azure, on-premises Active Directory, or any OpenID Connect or SAML-compliant identity provider—and exchange them for short-lived Google credentials.&lt;/p&gt;

&lt;p&gt;No service account key ever needs to be created or stored for this to work.&lt;/p&gt;

&lt;p&gt;Two building blocks make it up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;workload identity pool&lt;/strong&gt;, which is a container for external identities.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;workload identity pool provider&lt;/strong&gt;, which defines the trust relationship with a specific identity provider through its issuer, audience, and attribute mappings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From there, teams generally choose between two access patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct resource access:&lt;/strong&gt; IAM roles are granted straight to the federated principal, so the external identity calls Google Cloud resources under its own identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service account impersonation:&lt;/strong&gt; The federated identity is granted the Workload Identity User role and uses it to impersonate a Google service account, inheriting that service account's permissions instead of holding its own broad grants.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most agent frameworks lean toward the impersonation pattern, since it keeps the permission model centralized on a small number of service accounts rather than sprawling across many external principals.&lt;/p&gt;

&lt;p&gt;It is also the pattern with the fewest surprises when a Google Cloud API has limitations around directly federated tokens.&lt;/p&gt;

&lt;p&gt;This matters for AI agents specifically because agents frequently run somewhere other than Google Cloud: built on infrastructure from another provider, invoked from inside a CI pipeline, or triggered from a workflow tool that has nothing to do with Google.&lt;/p&gt;

&lt;p&gt;Workload Identity Federation removes the temptation to paste a downloaded JSON key into a config file, where it would sit valid indefinitely until someone remembered to rotate it. External tokens exchanged through federation typically live minutes to hours instead.&lt;/p&gt;

&lt;p&gt;Google Kubernetes Engine has its own variant of this pattern, Workload Identity Federation for GKE, which is covered in more detail in the containers section below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service Account Impersonation and Short-Lived Credentials for Safer Agent Access
&lt;/h2&gt;

&lt;p&gt;Service account impersonation lets one identity—a person, a CI system, or another service account—request a temporary credential for a target service account without ever holding that target account's long-lived key.&lt;/p&gt;

&lt;p&gt;It's the mechanism underneath both Workload Identity Federation and a lot of everyday local development.&lt;/p&gt;

&lt;p&gt;The mechanics are straightforward: the calling identity needs the Service Account Token Creator role on the target service account, then calls the IAM Service Account Credentials API's &lt;code&gt;generateAccessToken&lt;/code&gt; method to receive a working OAuth 2.0 access token.&lt;/p&gt;

&lt;p&gt;A few details are worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lifetime:&lt;/strong&gt; Tokens default to 3,600 seconds, or one hour, and can be extended up to 43,200 seconds, or twelve hours, for workloads that genuinely need a longer window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No refresh token:&lt;/strong&gt; Unlike a typical OAuth flow, an expired impersonated token can't be refreshed. The caller has to repeat the impersonation request. That's a deliberate design choice: it forces every credential to be reissued against current IAM policy instead of persisting unchecked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegation chains:&lt;/strong&gt; Impersonation can be chained across multiple service accounts, where each hop needs the Token Creator role granted on the account ahead of it. This is useful for separating an agent's everyday identity from a higher-privilege identity it's only occasionally allowed to assume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an agent, this means its baseline identity can stay low privilege for routine tool calls, and step up to a more privileged service account only for specific, auditable operations.&lt;/p&gt;

&lt;p&gt;Each impersonation call shows up as its own entry in Cloud Audit Logs tied to both identities involved, which is a far clearer trail than a single static credential reused for everything an agent does.&lt;/p&gt;

&lt;p&gt;Most agent frameworks need an equivalent pattern for every provider they connect to, not only Google Cloud: somewhere to &lt;a href="https://docs.corsair.dev/guides/plugin-credentials" rel="noopener noreferrer"&gt;store the specific credentials each integration requires&lt;/a&gt; without handing them to the agent directly, which is close to how a dedicated integration layer keeps plugin credentials scoped and rotated behind the API surface an agent actually calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Least Privilege and Zero Trust Access for Autonomous AI Agents
&lt;/h2&gt;

&lt;p&gt;Least privilege is harder to enforce for agents than for people, since agents don't usually request access when they hit something new. They just attempt the call.&lt;/p&gt;

&lt;p&gt;That means overprovisioning tends to stay invisible until something goes wrong.&lt;/p&gt;

&lt;p&gt;A few approaches hold up well in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Split service accounts by capability, not by agent:&lt;/strong&gt; Separate identities for read-heavy work, such as querying BigQuery or reading from Cloud Storage, from anything that writes, mutates, or deletes, so a compromised read-only path can't escalate into a write path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use IAM Conditions to narrow access further:&lt;/strong&gt; Time-bound bindings, resource-tag-based bindings, or request attribute checks can scope a role tighter than the role definition alone would allow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer custom roles over broad predefined roles for agent service accounts:&lt;/strong&gt; An agent rarely needs Editor or Owner. It needs the three or four permissions its specific tool calls actually use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat any agent action beyond a simple read as something that deserves a policy check:&lt;/strong&gt; A valid token should not automatically mean unrestricted execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero trust, applied to agents, means not trusting an identity just because it authenticated successfully.&lt;/p&gt;

&lt;p&gt;What it's asking to do still needs to be checked against policy at call time, which is the real difference between "can this identity obtain a token" and "should this identity be allowed to run this specific action right now."&lt;/p&gt;

&lt;p&gt;One pattern worth building in directly is a human approval step for sensitive or destructive calls, where the agent's request is queued for a person to approve or deny before it executes, rather than relying on authentication alone as the only gate.&lt;/p&gt;

&lt;p&gt;This is the same reasoning behind letting teams &lt;a href="https://docs.corsair.dev/concepts/permissions" rel="noopener noreferrer"&gt;gate sensitive actions behind human approval before they execute&lt;/a&gt; for any connected integration, not just Google Cloud resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing Agent Credentials Against Prompt Injection, Token Theft, and Credential Exfiltration
&lt;/h2&gt;

&lt;p&gt;Agents carry a risk that ordinary backend services don't: their inputs—a document they read, a page they fetch, an email they process—can contain instructions crafted to make the agent take an action it shouldn't, including leaking its own credentials.&lt;/p&gt;

&lt;p&gt;A few concrete risks are worth naming directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt injection:&lt;/strong&gt; An attacker convinces an agent to print its own environment variables or configuration. If &lt;code&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/code&gt; points at a key file, that file's contents can end up in an agent's output or in a log a bad actor later reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token theft through logs or traces:&lt;/strong&gt; Access tokens, even short-lived ones, can end up captured by verbose debug logging, observability tooling, or crash reports. If the token is still valid when it's read, it can be replayed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential exfiltration through chained tool calls:&lt;/strong&gt; An agent tricked into passing a token or key as a parameter to an external tool can leak it straight outside the intended trust boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mitigations aren't prompt-level fixes. They're architectural.&lt;/p&gt;

&lt;p&gt;Avoiding long-lived keys in the first place, through ADC, Workload Identity Federation, and impersonation, means there's nothing durable to steal, and a leaked short-lived token has a far smaller blast radius since it expires within the hour.&lt;/p&gt;

&lt;p&gt;Credential resolution should also sit outside the agent's own reasoning loop entirely. The code path that fetches and applies a token shouldn't be something the agent's generated output can influence, which is an architecture decision more than a prompting one.&lt;/p&gt;

&lt;p&gt;Tokens should be scoped tightly per call rather than reused broadly across a session, and Cloud Audit Logs are worth monitoring specifically for unusual impersonation events, since that audit trail is something a short-lived token gives you that a static key never does.&lt;/p&gt;

&lt;p&gt;This is also the reasoning behind keeping raw credentials out of an agent's context entirely: an integration layer that resolves credentials at call time so an agent only ever sees method names and results is a more defensible boundary than trusting the agent to handle a token responsibly, which is the same principle behind a hosted relay that &lt;a href="https://docs.corsair.dev/hub/overview" rel="noopener noreferrer"&gt;stores none of your credentials&lt;/a&gt; in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the Right Authentication Pattern for Containers, Kubernetes, and Air-Gapped Systems
&lt;/h2&gt;

&lt;p&gt;The right pattern depends heavily on where the agent's runtime actually sits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Containers on Compute Engine or Cloud Run:&lt;/strong&gt; ADC resolves the attached service account automatically from the metadata server. This is the simplest case. Nothing needs to be explicitly configured inside the container image itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GKE:&lt;/strong&gt; Use Workload Identity Federation for GKE rather than mounting service account key files as Kubernetes secrets. It binds a Kubernetes ServiceAccount to a Google identity so a pod authenticates automatically, and on Autopilot clusters it's enabled by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-managed Kubernetes outside Google Cloud, on-premises, or on another provider:&lt;/strong&gt; Use Workload Identity Federation with the cluster's own OIDC issuer as the identity provider. This is the same underlying pattern used for AWS or Azure, just pointed at the cluster's own token issuer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Air-gapped or fully disconnected systems:&lt;/strong&gt; This is the honest edge case. Workload Identity Federation and impersonation both depend on reaching Google's token exchange and IAM Credentials endpoints over the network, so an agent with no outbound connectivity to Google Cloud can't use either.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For air-gapped systems, options narrow to private connectivity, a VPN or Interconnect combined with Private Google Access, so the disconnected network can still reach Google's APIs privately, or accepting that a fully offline agent needs a proxy or relay component with real connectivity to broker those calls on its behalf.&lt;/p&gt;

&lt;p&gt;The practical way to decide is to sort by connectivity first, privilege second.&lt;/p&gt;

&lt;p&gt;If the runtime can reach Google Cloud's APIs at all, prefer ADC with an attached service account, or Workload Identity Federation, over anything involving a static key.&lt;/p&gt;

&lt;p&gt;If it genuinely can't reach them, a relay or gateway component becomes necessary, and that component now holds the credential the agent doesn't, which deserves its own security review.&lt;/p&gt;

&lt;p&gt;Google Cloud gives AI agents a strong foundation for identity, but most agents don't only talk to Google Cloud. They also need Slack, Notion, GitHub, Stripe, and dozens of other services, each with its own auth quirks and token lifecycles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://corsair.dev/" rel="noopener noreferrer"&gt;Corsair&lt;/a&gt; is an open-source integration layer built for exactly this problem: it handles OAuth, API keys, and credential rotation across hundreds of plugins so agents authenticate consistently everywhere, not only inside Google Cloud.&lt;/p&gt;

&lt;p&gt;Teams can self-host it for free or run it through Corsair's hosted Hub, which never stores customer credentials. The same principles that apply to designing least-privilege, short-lived access for Google Cloud apply to every other integration an agent touches too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between Application Default Credentials and a service account key?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ADC is a discovery strategy that looks for credentials in a fixed order: an environment variable, a local credential file from the Google Cloud CLI, or the attached service account from the metadata server. It isn't a credential type by itself.&lt;/p&gt;

&lt;p&gt;A service account key is one specific, long-lived credential type that ADC can pick up if &lt;code&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/code&gt; points at it. Google recommends avoiding key files where possible, since an attached service account or Workload Identity Federation can usually provide the same access without a static file to protect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do AI agents need Workload Identity Federation if they already run inside Google Cloud?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not for the parts of an agent that run natively on Compute Engine, Cloud Run, or GKE, since ADC already resolves the attached service account automatically there.&lt;/p&gt;

&lt;p&gt;Workload Identity Federation becomes relevant the moment part of the agent's workflow runs outside Google Cloud, such as a CI pipeline, another cloud provider, or an on-premises system that still needs to call a Google Cloud API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long do impersonated service account credentials last?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By default, an access token generated through service account impersonation lasts 3,600 seconds, or one hour, and can be configured up to 43,200 seconds, or twelve hours, for workloads that need a longer window.&lt;/p&gt;

&lt;p&gt;There's no refresh token involved. Once it expires, the caller has to request a new one, which keeps every credential reissued against current IAM policy rather than persisting unchecked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can an AI agent be tricked into leaking its own Google Cloud credentials through prompt injection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a real risk if the agent's runtime holds a long-lived key and its reasoning loop has any path to reading environment variables, configuration files, or verbose logs.&lt;/p&gt;

&lt;p&gt;The fix isn't a prompt-level patch. It's architectural: keep credential resolution in trusted application code outside the agent's context, and prefer short-lived tokens over static keys so even a successful leak has a small, time-limited blast radius.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the simplest way to authenticate an AI agent running in a container on Google Cloud?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the container runs on Compute Engine, Cloud Run, or GKE, attach a dedicated service account with only the permissions the agent needs and let ADC resolve it automatically from the metadata server, with no key files or environment variables to manage.&lt;/p&gt;

&lt;p&gt;On GKE specifically, use Workload Identity Federation for GKE to bind the pod's Kubernetes ServiceAccount to that Google identity.&lt;/p&gt;

</description>
      <category>googlecloud</category>
    </item>
    <item>
      <title>What Is Corsair? A Complete Guide to the Integration Platform for Apps and AI Agents</title>
      <dc:creator>Corsair</dc:creator>
      <pubDate>Tue, 01 Sep 2026 14:15:24 +0000</pubDate>
      <link>https://dev.to/corsairdev/what-is-corsair-a-complete-guide-to-the-integration-platform-for-apps-and-ai-agents-3mba</link>
      <guid>https://dev.to/corsairdev/what-is-corsair-a-complete-guide-to-the-integration-platform-for-apps-and-ai-agents-3mba</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Facaghsgb9g1pyn2plu25.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Facaghsgb9g1pyn2plu25.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Building an app or AI agent is one thing. Connecting it to all the tools people actually use is another.&lt;/p&gt;

&lt;p&gt;Gmail, Slack, HubSpot, GitHub, Notion, Stripe, and hundreds of other services each come with their own APIs, authentication, permissions, and integration requirements. &lt;strong&gt;Corsair brings those connections into one open-source integration layer, giving developers a simpler way to connect apps and AI agents to real-world tools.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But what exactly is Corsair, and where does it fit in a modern application stack?&lt;/p&gt;

&lt;p&gt;This guide explores the &lt;strong&gt;Corsair platform from end to end&lt;/strong&gt;: what Corsair is and does, the apps, APIs, tools, and MCP integrations it supports, how the platform works, who it is built for, its pricing and plans, and what developers can build with it across real-world teams and industries.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Corsair?
&lt;/h2&gt;

&lt;p&gt;Corsair is an open-source integration layer for apps and AI agents. It connects to 200+ third-party services—things like Slack, Gmail, GitHub, Google Calendar, Notion, HubSpot, Stripe, and Airtable—through one consistent, typed syntax instead of a different SDK and auth flow for every provider.&lt;/p&gt;

&lt;p&gt;Rather than asking a team to hand-build OAuth screens, token refresh logic, and rate-limit handling for each app it needs to reach, &lt;a href="https://corsair.dev/" rel="noopener noreferrer"&gt;Corsair&lt;/a&gt; takes on that repetitive plumbing so developers can focus on the part of the integration that is actually specific to their product.&lt;/p&gt;

&lt;p&gt;The project is Y Combinator backed and released under the Apache 2.0 license, so the full SDK, including its permission system and multi-tenant credential storage, can be self-hosted for free. A hosted version, Corsair Hub, is also available for teams that would rather not run that infrastructure themselves.&lt;/p&gt;

&lt;p&gt;Either way, Corsair positions itself as a genuine AI agent integration platform rather than a thin wrapper around a single protocol, which is why it works equally well for an autonomous agent, a backend service, or a customer-facing dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does Corsair Do? From App Connections to Secure Tool Execution
&lt;/h2&gt;

&lt;p&gt;At its core, Corsair does the unglamorous work that every integration needs and almost nobody wants to write twice. It manages OAuth and API key-based authentication for each connected service, encrypts stored credentials, and refreshes access tokens automatically before they expire.&lt;/p&gt;

&lt;p&gt;It normalizes the very different auth flows, schemas, and error handling that every provider ships on its own terms into one predictable, typed interface.&lt;/p&gt;

&lt;p&gt;It also keeps data current. Incoming updates arrive through webhooks and scheduled polling, landing in a local database partitioned per tenant, so a repeated read does not have to hit the third-party API and burn through a rate limit every single time.&lt;/p&gt;

&lt;p&gt;Security is built into execution itself, not bolted on afterward. You can set a permission mode per integration, so a read-only lookup runs freely while a destructive or sensitive action—sending an email, deleting a record—requires explicit approval before it executes.&lt;/p&gt;

&lt;p&gt;Credentials are resolved internally at the moment a call runs, which means an agent only ever sees the method it invoked and the result that came back, never a raw API key or token. That is what turns a list of AI agent tools into something you can actually put in front of real customers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Can You Connect With Corsair? Apps, APIs, Tools, and MCP
&lt;/h2&gt;

&lt;p&gt;Corsair ships 200+ integrations as installable plugin packages, each covering a different category of work: communication tools like Slack and Discord, productivity apps like Notion, Google Calendar, and Google Sheets, CRM and sales tools like HubSpot, support platforms like Zendesk, payments through Stripe, operational data in Airtable, and analytics through PostHog, with new plugins added regularly.&lt;/p&gt;

&lt;p&gt;Every plugin follows the same shape once installed: typed API calls, optional webhook support, and, where it makes sense, a locally synced database layer for that provider's data.&lt;/p&gt;

&lt;p&gt;For AI agent tools specifically, Corsair supports MCP integrations directly, so any MCP-compatible agent, including Claude, can call these same plugins as tools without extra glue code.&lt;/p&gt;

&lt;p&gt;It also ships adapters for popular agent frameworks, so teams already building on the Claude Agent SDK, OpenAI's Agents SDK, the Vercel AI SDK, or Mastra can wire Corsair in without switching stacks. Because the underlying layer is a REST API rather than an MCP-only implementation, the exact same integration also works from a plain backend route or a button in a customer dashboard, not only from inside an agent loop.&lt;/p&gt;

&lt;p&gt;If a service you need is not covered yet, the open-source model means you are not stuck waiting on a roadmap. You can scaffold a new plugin, open a pull request, or fork the project and build exactly what you need, all covered in the &lt;a href="https://docs.corsair.dev/introduction" rel="noopener noreferrer"&gt;Corsair documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features of Corsair
&lt;/h2&gt;

&lt;p&gt;Corsair's value shows up most clearly once a product has more than one or two integrations to maintain. As a developer integration platform, it is built to keep that maintenance flat as you add more connections rather than letting it grow with every new app.&lt;/p&gt;

&lt;p&gt;Here is what makes Corsair AI agent integrations dependable once a product is live, not just in a demo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open-source core:&lt;/strong&gt; The full SDK, including every plugin and the permission system, is released under Apache 2.0 on &lt;a href="https://github.com/corsairdev/corsair" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, so you can inspect, fork, or extend it rather than trust a closed black box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-host for free:&lt;/strong&gt; Run Corsair on your own infrastructure at no cost, with no per-seat pricing and no markup on the API calls you are already paying for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant OAuth:&lt;/strong&gt; Turn on multi-tenancy and every call is automatically scoped to the right tenant's credentials, built for products that serve many customers who each connect their own accounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission modes by default:&lt;/strong&gt; Assign a permission level per integration so sensitive or destructive actions pause for explicit approval instead of executing silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic token refresh and caching:&lt;/strong&gt; Expiring tokens are renewed quietly in the background, and repeated reads come from a synced local database instead of hitting the third-party API every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP-native, not MCP-only:&lt;/strong&gt; A constant, small set of MCP tools covers setup, discovery, and execution no matter how many plugins are installed, keeping an agent's context lean as your integration list grows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typed developer experience:&lt;/strong&gt; Every call is a typed method with editor autocomplete, not a hand-assembled HTTP request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Corsair Works
&lt;/h2&gt;

&lt;p&gt;Getting from zero to a working integration follows a short, repeatable pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install the packages you need.&lt;/strong&gt; Add the core &lt;code&gt;corsair&lt;/code&gt; package plus the plugin for each service, for example Slack or GitHub, through npm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure one Corsair instance.&lt;/strong&gt; Pass in your plugins, a database connection, and an encryption key that protects stored credentials at rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call it directly, or hand it to an agent.&lt;/strong&gt; Use it as a typed SDK in your own backend code, or expose it as an MCP server so an agent can call it as a tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the agent discover what is available.&lt;/strong&gt; When Corsair acts as MCP for AI agents, it exposes four tools regardless of plugin count: one to check what is connected and request missing credentials, two to discover available operations and inspect their parameters, and one to actually run the call. That footprint stays the same whether five integrations are connected or two hundred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data stays fresh underneath.&lt;/strong&gt; Webhooks and polling keep a tenant-partitioned database in sync, so reads are fast and writes still reach the live service immediately.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A minimal setup looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createCorsair&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;corsair&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slack&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@corsair-dev/slack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@corsair-dev/github&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;corsair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCorsair&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;github&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;kek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CORSAIR_KEK&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, calls like &lt;code&gt;corsair.slack.api.messages.post(...)&lt;/code&gt; or &lt;code&gt;corsair.github.api.issues.create(...)&lt;/code&gt; behave like any other typed function in your codebase, with framework-specific guides available for Next.js, Node, Express, Hono, SvelteKit, Remix, and Astro.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Is Corsair For?
&lt;/h2&gt;

&lt;p&gt;Corsair fits anywhere a product needs to reach outside its own walls reliably, but it tends to show up most in a few recurring situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Teams building AI agents that need to act, not just answer.&lt;/strong&gt; If an assistant is meant to actually send the email or update the ticket rather than describe what someone else should do, it needs the kind of AI agent infrastructure Corsair provides underneath it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SaaS companies offering their own customers a "connect your apps" experience.&lt;/strong&gt; Multi-tenant OAuth and per-tenant credential isolation are built in, so you are not designing that system from scratch for every new integration you support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal tools and operations teams&lt;/strong&gt; automating repetitive cross-app work such as sales call prep, order lookups, support triage, or team notifications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solo developers and small teams&lt;/strong&gt; prototyping on the free Hobby tier, scaling up to production teams on Pro, and enterprises with custom compliance or volume needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anyone maintaining hand-rolled integrations today&lt;/strong&gt; who is tired of chasing token refreshes, provider deprecations, and schema changes across a dozen separate codebases.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Can You Build With Corsair? Real-World Use Cases Across Teams and Industries
&lt;/h2&gt;

&lt;p&gt;Because Corsair connects the same way whether it is called by an agent, a script, or a UI button, the use cases span far beyond a single team:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sales:&lt;/strong&gt; An assistant that checks a rep's calendar, drafts and sends a meeting invite, and pulls together a short call brief from CRM notes before a call starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operations:&lt;/strong&gt; Pulling every unshipped order out of Airtable or a spreadsheet, flagging exceptions, and keeping that view synced without a manual export each morning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customer support:&lt;/strong&gt; A helpdesk-connected agent that triages incoming tickets and drafts replies, with sensitive responses held for approval before they send.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineering:&lt;/strong&gt; Filing a GitHub issue, updating a Linear ticket, and posting a Slack summary automatically the moment a build fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-team visibility:&lt;/strong&gt; Alerting a channel the instant a file lands in a shared drive or a deal changes stage, instead of relying on someone to notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product-led growth:&lt;/strong&gt; A vertical SaaS company embedding its own "connect your tools" dashboard so customers can link Slack, HubSpot, or a calendar without the vendor building a bespoke integration for each request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Across industries, the pattern repeats: wherever a team already relies on a handful of SaaS tools, there is a use case for letting an agent or a workflow reach into them safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Corsair for Developers: Typed Integrations, Authentication, Permissions, Triggers, and Real-Time Tool Calling
&lt;/h2&gt;

&lt;p&gt;This is the section developers tend to care about most, so it is worth breaking down each piece:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typed integrations:&lt;/strong&gt; Every plugin ships as a typed client, so your editor autocompletes available methods and parameters instead of you guessing at field names from someone else's API docs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication:&lt;/strong&gt; OAuth and API key flows are handled per plugin, credentials are encrypted with your own key, and in multi-tenant setups a call like &lt;code&gt;corsair.withTenant(teamId)&lt;/code&gt; scopes everything to that tenant's credentials automatically, which is the backbone of doing API integrations for AI agents at scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions:&lt;/strong&gt; Assign a permission mode per integration so low-risk reads run freely while destructive or sensitive actions pause for a human approval link before they execute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triggers:&lt;/strong&gt; Webhook hooks on incoming provider events let your product react the moment something changes—a new ticket, an upload, a status change—instead of polling on a fixed schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time tool calling:&lt;/strong&gt; Because reads come from a locally synced, per-tenant database and writes execute immediately against the live service, an agent can look something up and act on it inside a single reasoning step rather than waiting for the next scheduled sync.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Framework support extends beyond agents too. Adapters exist for the Claude Agent SDK, OpenAI's Agents SDK, the Vercel AI SDK, and Mastra, alongside standard web frameworks like Next.js, Node, Express, Hono, SvelteKit, Remix, and Astro, so Corsair fits into a stack you already have rather than requiring a rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Corsair Pricing: Plans, Features, and What You Get at Each Tier
&lt;/h2&gt;

&lt;p&gt;Corsair pricing is built around how much of the integration layer you want Corsair to run for you, and no plan requires a credit card to start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hobby — $0 per month:&lt;/strong&gt; Built for small and side projects. Includes unlimited tool calls, up to 50 connections, 100,000 webhook events, unlimited managed permission and auth pages, up to 3 team members, a Corsair-branded consent screen, and community support through Discord.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro — $200 per month:&lt;/strong&gt; Built for teams running in production and currently the most popular plan. Includes unlimited tool calls, connections, webhooks, permission pages, and team members, a custom-branded consent screen, direct support through Slack, and custom integrations built by the Corsair team when you need one that does not exist yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise — Custom pricing:&lt;/strong&gt; Built for organizations with specific scale, compliance, or support requirements. Everything in Pro, with connections, webhooks, team size, branding, support, and custom integration work tailored to the account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the core SDK is open source, self-hosting remains free at any scale for teams that would rather run Corsair on their own infrastructure than pay for the hosted option. Full details, including what counts toward each limit, are on the &lt;a href="https://corsair.dev/#pricing" rel="noopener noreferrer"&gt;Corsair pricing page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Corsair AI agent integrations exist to remove the part of building with AI that has nothing to do with your actual product: the OAuth screens, the token refresh jobs, and the permission checks that every team ends up writing anyway.&lt;/p&gt;

&lt;p&gt;Whether you are shipping a single internal automation or a multi-tenant platform used by thousands of customers, the underlying problem is the same, and it has already been solved once, openly, so you do not have to solve it again from scratch.&lt;/p&gt;

&lt;p&gt;The project is open source, self-hosting is free, and a hosted option is ready the moment you would rather not run that infrastructure yourself. Explore &lt;a href="https://corsair.dev/" rel="noopener noreferrer"&gt;Corsair&lt;/a&gt; and connect your first integration in minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Corsair open source?
&lt;/h3&gt;

&lt;p&gt;Yes. The core SDK and its plugins are released under the Apache 2.0 license, so you can self-host the entire platform, including the permission system and multi-tenant credential storage, on your own infrastructure at no cost. A hosted version, Corsair Hub, runs the same codebase if you would rather not manage that infrastructure yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Corsair support MCP for AI agents?
&lt;/h3&gt;

&lt;p&gt;Yes. Corsair exposes a small, constant set of MCP tools covering setup, discovery, schema inspection, and execution, no matter how many plugins are connected, so an agent's context does not grow just because more integrations are added. Adapters for popular agent frameworks are also available alongside raw MCP support.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Corsair without an AI agent?
&lt;/h3&gt;

&lt;p&gt;Yes. Every integration is also a typed library you can call directly from your own backend, for example wiring a create-calendar-invite button or a scheduled sync-from-Airtable job. An AI agent is one way to use Corsair, not a requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does my agent ever see raw API keys or tokens?
&lt;/h3&gt;

&lt;p&gt;No. Corsair resolves credentials internally at the moment a call executes, so an agent only ever sees the method it called and the result that came back, never the underlying token or key.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Is the Difference Between Self-Hosting Corsair and Using Corsair Hub?
&lt;/h3&gt;

&lt;p&gt;Self-hosting runs the full open-source SDK, including every plugin, the permission system, and multi-tenant storage, on your own infrastructure for free. Corsair Hub is the hosted version of that same codebase, managing OAuth callbacks, connect pages, and webhook infrastructure for you, which is useful if you would rather not run that part yourself.&lt;/p&gt;

</description>
      <category>corsair</category>
    </item>
    <item>
      <title>How to Build Durable Long Running AI Agent Tasks Across External APIs</title>
      <dc:creator>Corsair</dc:creator>
      <pubDate>Sat, 29 Aug 2026 13:14:53 +0000</pubDate>
      <link>https://dev.to/corsairdev/how-to-build-durable-long-running-ai-agent-tasks-across-external-apis-1n2g</link>
      <guid>https://dev.to/corsairdev/how-to-build-durable-long-running-ai-agent-tasks-across-external-apis-1n2g</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7c4m7fpl7yfb24dnkaj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7c4m7fpl7yfb24dnkaj.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;Most AI agent demos run inside a single request and response cycle: ask a question, get an answer, done. Production agents rarely work that way. They kick off tasks that can span minutes, hours, or days, call a dozen external APIs along the way, wait on a human decision, and need to survive a server restart without losing their place.&lt;/p&gt;

&lt;p&gt;That gap between a demo and a durable AI agent workflow is where most teams building long-running AI agents get stuck, since the very things that make agents useful—chaining tools, calling real APIs, acting over time—are exactly what expose gaps in reliability.&lt;/p&gt;

&lt;p&gt;This guide covers the architecture choices, state design, and failure handling behind real AI agent task orchestration: picking between durable workflows and asynchronous queues, keeping execution state separate from agent reasoning, making external API integration calls safe to retry, pausing tasks without holding a worker open, gating risky actions behind durable approval, and watching for the quieter failure modes that only show up once an agent is running in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the Right Architecture for Long-Running AI Agent Tasks: Durable Workflows vs Asynchronous Queues
&lt;/h2&gt;

&lt;p&gt;The first decision in AI agent task orchestration is what actually runs the task once it leaves the initial request. Two patterns cover most cases: a durable workflow engine, or a simpler asynchronous queue.&lt;/p&gt;

&lt;p&gt;A durable workflow engine checkpoints progress at every step. If the process crashes or a deployment restarts it, the workflow replays from the last completed step instead of starting over, and it can hold a "sleep" for days or weeks without any external scheduler or cron job watching it. Temporal, Inngest, Trigger.dev, and Hatchet all work this way.&lt;/p&gt;

&lt;p&gt;An asynchronous queue simply moves work off the request path so a job runs later. That is enough for a lot of tasks, but a queue does not give you checkpointing, replay, or durable timers for free. You end up building that layer yourself on top of it.&lt;/p&gt;

&lt;p&gt;A few points help decide which one fits a given task:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An asynchronous queue is usually enough when the task is a single-hop, fire-and-forget action, when it does not need to survive a wait of more than a few minutes, and when you are comfortable owning your own retry logic.&lt;/li&gt;
&lt;li&gt;A durable workflow is worth adopting when the task spans multiple steps that must survive a crash or restart, when it needs to pause for hours or days without keeping a worker open, or when you want checkpointing and durable timers built in rather than hand-rolled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Corsair does not try to replace either option; it plugs into whichever one a team already runs. Its Temporal guide shows how to &lt;a href="https://docs.corsair.dev/guides/temporal" rel="noopener noreferrer"&gt;start a Temporal workflow directly from a Corsair webhook event&lt;/a&gt;, so Corsair handles the integration auth and webhook plumbing while Temporal owns durability, retries, and the long-running execution itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoupling Agent Reasoning From Execution State So Tasks Can Survive Crashes and Restarts
&lt;/h2&gt;

&lt;p&gt;The reasoning loop, meaning the model call that decides what happens next, and the execution state, meaning which steps have already run and what they returned, are conceptually different things. Plenty of early agent builds blur them together and keep both in the memory of a single process.&lt;/p&gt;

&lt;p&gt;That works fine until the process crashes, gets redeployed, or scales down. When it does, both the reasoning context and the record of what already happened disappear together, forcing the task to restart from scratch. Restarting is not just slow; it risks duplicating real-world side effects: sending the same email twice, filing the same ticket twice, charging a card twice.&lt;/p&gt;

&lt;p&gt;The fix is to persist execution state independently of the reasoning process. Every completed step, every tool result, and every decision the agent made gets written somewhere durable before the agent moves on, so a fresh process can resume by reading that state rather than by rerunning the reasoning from the beginning.&lt;/p&gt;

&lt;p&gt;This is why solid AI agent task orchestration tends to look more like an event log or state machine, with a task ID, current step, inputs, outputs, and status, rather than a single long-lived function call holding everything in variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making External API and Tool Calls Durable With Idempotency, Timeouts, Retries, and Circuit Breakers
&lt;/h2&gt;

&lt;p&gt;Every external API call an agent makes is a point of failure outside your control, and durability at the task level does not help if the calls themselves are fragile. A few practices cover most of the risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency&lt;/strong&gt; keeps retries safe. If a step might run more than once, an idempotency key or an existence check ensures a repeated "create invoice" call is recognized as the same operation instead of producing a second invoice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeouts&lt;/strong&gt; stop a hanging provider from stalling an entire task indefinitely. Every external call needs a bound, paired with a defined fallback for what happens when that bound is hit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retries&lt;/strong&gt; need judgment, not just a loop. Backoff with jitter avoids hammering a struggling API, a retry cap avoids burning budget on something that will never succeed, and distinguishing retryable errors like rate limits and timeouts from permanent ones like invalid auth or a bad request keeps the agent from repeating a call that was never going to work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Circuit breakers&lt;/strong&gt; protect the rest of the system. When one external API keeps failing, cutting off calls to it for a cooldown window, rather than retrying endlessly, protects other steps and other tenants sharing the same integration.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of plumbing worth pushing into the integration layer instead of rewriting per provider. Corsair routes every failure through a &lt;a href="https://docs.corsair.dev/concepts/error-handling" rel="noopener noreferrer"&gt;hierarchical error handling system with configurable retry strategies like exponential backoff with jitter&lt;/a&gt;, checked at the plugin level first, then a root-level handler, then sensible defaults, so a rate-limited call to one service does not need custom retry code written from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Agent Continuations to Pause and Resume Tasks Without Keeping Workers Running
&lt;/h2&gt;

&lt;p&gt;Some steps in an agent task have to wait on something external: a long-running batch job, an incoming webhook, or a human decision. Keeping a process, and its whole reasoning context, alive in memory for the entire wait is wasteful and fragile, especially when that wait stretches into hours or days.&lt;/p&gt;

&lt;p&gt;A continuation pattern solves this differently. Instead of blocking, a step returns immediately, its execution state gets persisted, and the worker is freed to do other work or shut down entirely.&lt;/p&gt;

&lt;p&gt;When the awaited event finally arrives, whether that is a webhook, a timer, or an approval, the task resumes from exactly that point, rehydrating only the state it needs rather than replaying the entire reasoning history from the start.&lt;/p&gt;

&lt;p&gt;This is a meaningfully different shape from polling in a loop, which still ties up a process checking again and again. A true continuation releases the resource completely and gets woken back up by the event itself, which is what lets a task span days without a single worker sitting idle the whole time footing the bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Durable Human-in-the-Loop Checkpoints for High-Risk Agent Actions
&lt;/h2&gt;

&lt;p&gt;Some actions carry enough risk that no amount of confidence in the agent's reasoning should skip a human sign-off: deleting a production resource, emailing a large customer list, or moving money. The question is how to make that checkpoint durable rather than just a dialog box that disappears if anything crashes.&lt;/p&gt;

&lt;p&gt;A durable checkpoint needs a few properties. The pending action and its exact arguments get frozen in storage at the moment the checkpoint is created, not just described in a chat transcript, so approval executes precisely what was reviewed rather than a fresh reconstruction of it.&lt;/p&gt;

&lt;p&gt;The approval itself needs an expiry, so a stale request cannot be approved long after the surrounding context has changed. And the checkpoint needs to survive a crash or restart the same way the rest of the task's execution state does, or a server hiccup could quietly drop a pending high-risk action.&lt;/p&gt;

&lt;p&gt;Whether that checkpoint blocks synchronously or resolves asynchronously depends on the situation: synchronous works well when a person is already watching a live review screen, while asynchronous fits background tasks better, since the agent can surface a review link, keep working on anything else that is safe, and pick the blocked action back up once it is approved.&lt;/p&gt;

&lt;p&gt;Corsair's permission layer builds this in directly, mapping each action to a &lt;a href="https://docs.corsair.dev/concepts/permissions" rel="noopener noreferrer"&gt;read, write, or destructive risk tier with a policy per tier, plus single-use approvals and configurable timeouts&lt;/a&gt;, so a high-risk action cannot execute, or accidentally replay, without a durable, reviewable record behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Production Observability to Detect Tool Failures, Agent Loops, and Semantic Degradation
&lt;/h2&gt;

&lt;p&gt;Durable execution solves the crash problem, but it does not automatically tell you when something is quietly going wrong. An agent can get stuck retrying the same failing tool call, a tool can return a technically valid but semantically wrong result, and a task can finish with no error at all while still producing an outcome nobody actually wanted.&lt;/p&gt;

&lt;p&gt;Real AI agent reliability depends on catching all three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool-level logging&lt;/strong&gt; is the foundation: capture every call, its arguments, its latency, and its result, so a failure is visible immediately instead of three steps later when the task has already gone sideways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loop detection&lt;/strong&gt; catches the second failure mode. Tracking repeated identical calls or repeated task states within a single run, and flagging or halting once a threshold is crossed, stops an agent from silently burning through budget on a call that is never going to succeed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic monitoring&lt;/strong&gt; catches the third and hardest one. Sampling completed tasks and checking whether the final state actually matches the intended outcome surfaces the cases where an agent technically finished the job but did the wrong thing, which no amount of retry logic will ever flag on its own.&lt;/p&gt;

&lt;p&gt;Corsair's hooks make the first layer straightforward to add without touching core agent logic. &lt;a href="https://docs.corsair.dev/concepts/hooks" rel="noopener noreferrer"&gt;Before and after hooks wrap every API call and every webhook event&lt;/a&gt;, so logging, auditing, or alerting can sit alongside the integration itself instead of scattered through application code.&lt;/p&gt;

&lt;p&gt;Durability, retries, checkpoints, and human approval rarely show up in a demo, but they decide whether an agent survives contact with real users and real APIs.&lt;/p&gt;

&lt;p&gt;Corsair handles a good share of that plumbing directly: hierarchical retry and error handling per integration, permission gating for high-risk actions, hooks for logging and observability, and native adapters for Temporal, Inngest, Trigger.dev, and Hatchet so a long-running task can pause and resume without holding a worker open the whole time.&lt;/p&gt;

&lt;p&gt;If reliability is the part of your agent stack you would rather not rebuild from scratch, &lt;a href="https://corsair.dev/" rel="noopener noreferrer"&gt;corsair.dev&lt;/a&gt; is worth exploring before your next integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between a durable workflow engine and a simple task queue for AI agents?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A task queue moves a job off the request path and runs it later, which is enough for short single-step actions, but it does not automatically give a running task checkpoints, replay, or durable timers. A durable workflow engine persists progress at each step, so a task can pause for hours or days and resume exactly where it left off after a crash or restart, without an engineer bolting checkpoint logic on top of the queue by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you avoid duplicate side effects when a long-running agent task retries a step?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most reliable approach is idempotency: attach a unique key to the operation, such as an invoice ID or a request hash, so a retried step is recognized as the same operation rather than a new one. Combined with clear rules for which errors are safe to retry, like rate limits and timeouts, versus which are not, like invalid input or expired auth, idempotency keeps retries safe instead of turning a single failure into duplicated real-world actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do AI agents get stuck in loops, and how can that be detected in production?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Loops usually happen when an agent repeats a tool call expecting a different result, often because the underlying error was never surfaced clearly, or because its plan does not account for a tool that keeps failing. Catching this in production means tracking repeated identical calls or repeated task states within a single run and flagging or halting once a threshold is crossed, rather than letting the task consume budget indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What makes a human-in-the-loop checkpoint durable rather than just a confirmation dialog?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A durable checkpoint freezes the exact pending action and its arguments in storage rather than just describing it in a chat transcript, so approval executes precisely what was reviewed. It also needs an expiry so a stale request cannot be approved long after the surrounding context changed, and it needs to survive a crash or restart the same way the rest of the task's execution state does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is asynchronous consent always better than blocking synchronously for agent tasks?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not always. Synchronous blocking works well when a person is already watching a live review screen and wants the agent to continue the moment they approve something. Asynchronous handling fits background tasks better, letting the agent surface a review link, move on to other safe work in the meantime, and resume the blocked action later without tying up a worker the whole time.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Why OAuth Gets Complicated When AI Agents Act on Behalf of Users</title>
      <dc:creator>Corsair</dc:creator>
      <pubDate>Sat, 29 Aug 2026 13:00:01 +0000</pubDate>
      <link>https://dev.to/corsairdev/why-oauth-gets-complicated-when-ai-agents-act-on-behalf-of-users-bhi</link>
      <guid>https://dev.to/corsairdev/why-oauth-gets-complicated-when-ai-agents-act-on-behalf-of-users-bhi</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm3136h4h3tsxwotqbsjl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm3136h4h3tsxwotqbsjl.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
OAuth has quietly handled delegated access across the internet for almost two decades, and the model has held up well: a user clicks allow once, an application receives a scoped token, and everyone moves on with reasonable confidence about what that application can and cannot do. AI agents are quietly breaking that assumption in ways most teams only notice once something is already in production.&lt;/p&gt;

&lt;p&gt;An agent does not use a token once for one predictable job. It decides in real time what to do next, often chaining several services together with no human reviewing each step. That shift is forcing a rethink of AI agent authentication, AI agent authorization, and what delegated authorization even means once the party holding the token is capable of making its own decisions.&lt;/p&gt;

&lt;p&gt;This guide walks through where traditional OAuth delegation breaks down for autonomous agents, why scopes alone fall short of real AI agent permissions, and what it actually takes to manage identity, tokens, and consent once an agent, not a person, is the one acting on a user's behalf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Traditional OAuth Delegation Breaks Down for Autonomous AI Agents
&lt;/h2&gt;

&lt;p&gt;OAuth 2.0 was built for a specific kind of delegation: a human clicks allow once, a specific application receives a token scoped to a specific job, and that application behaves in a fairly predictable, bounded way. A photo backup tool reads a photo library. A calendar app reads and writes events. The consent screen describes the job well enough because the job does not change on its own.&lt;/p&gt;

&lt;p&gt;AI agents do not fit that pattern. An agent is not a static integration waiting for one instruction; it is a decision maker that plans its own next step, often across several tools and services without a human checking each call. OAuth delegation authorizes a category of access, not a plan of action, and that mismatch is the root of most complications around OAuth for AI agents.&lt;/p&gt;

&lt;p&gt;Consider a simple case: a user connects a scheduling agent to their calendar so it can find a good time for a team sync. The token grants calendar access, but the agent might reasonably decide to also email attendees, reschedule a conflicting meeting, or pull in a second service to check someone's availability. None of that was described on the original consent screen, yet all of it happens under the same authorized token. Traditional AI agent authentication and AI agent authorization models were never designed to account for a client that improvises.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dual Identity Problem: How Do You Track Both the User and the Agent Acting on Their Behalf?
&lt;/h2&gt;

&lt;p&gt;Classic delegated authorization keeps the identity model simple: a resource owner grants access, a client receives a token, and a resource server checks that token before responding. There is one identity that matters: the user who consented.&lt;/p&gt;

&lt;p&gt;Agents add a second identity that has to be tracked separately: the specific agent instance, run, or subagent actually making the call right now. A single user might have several agents, or several concurrent runs of the same agent, acting under credentials tied to their account. When something goes wrong, "the user authorized this" is not enough information. You also need to know which agent, which task, and which tool call actually performed the action.&lt;/p&gt;

&lt;p&gt;This becomes sharper in multi-tenant systems, where one platform runs agents on behalf of many different users, each of whom connected their own Slack, Gmail, or CRM account. If credentials are not cleanly isolated per user, one tenant's token or data can end up reachable from another tenant's context, and a permissions bug turns into a data breach.&lt;/p&gt;

&lt;p&gt;Corsair handles this by &lt;a href="https://docs.corsair.dev/concepts/multi-tenancy" rel="noopener noreferrer"&gt;scoping every connection, and every database read or write, to its own tenant&lt;/a&gt; automatically, so a user's credentials can never be reached outside their own context, even when many agents run at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OAuth Scopes Aren't Granular Enough for AI Agent Permissions
&lt;/h2&gt;

&lt;p&gt;OAuth scopes were designed for human-level decisions: read email, send email, access calendar, manage repositories. That granularity works when a person decides once whether to trust an application with a whole category of data.&lt;/p&gt;

&lt;p&gt;An autonomous agent needs a finer question answered: within that category, which specific operations are safe to run without asking anyone first? Reading a message is very different from deleting one. Drafting a reply is very different from sending it to a customer. A scope that says "manage repositories" does not distinguish between opening an issue and deleting the repository itself, yet those two actions carry very different risk.&lt;/p&gt;

&lt;p&gt;This is why AI agent permissions need to sit on top of OAuth scopes rather than replace them. The scope still decides which data class an agent can reach. A separate permission layer then decides which operations inside that scope run automatically and which require a human to approve first.&lt;/p&gt;

&lt;p&gt;A workable pattern here is mapping every endpoint to a risk tier: read, write, or destructive, then setting a policy per tier: allow, deny, or require approval. Corsair's &lt;a href="https://docs.corsair.dev/concepts/permissions" rel="noopener noreferrer"&gt;permission modes map each of those risk tiers to a policy per integration&lt;/a&gt;, so a broad OAuth scope does not automatically mean unrestricted autonomous action.&lt;/p&gt;

&lt;h2&gt;
  
  
  From User Intent to Agent Intent: Why Static OAuth Tokens Struggle With Dynamic Agent Actions
&lt;/h2&gt;

&lt;p&gt;A static access token does not know anything about the plan currently running against it. It is a blunt credential: either it is valid and in scope, or it is not. It has no concept of the task an agent is midway through, or how far that task has drifted from what the user actually asked for.&lt;/p&gt;

&lt;p&gt;That gap between user intent and agent intent is where most surprises happen. A user who asks an agent to clean up their inbox has a loose mental picture in mind, not a list of every archive, label, and delete operation the agent might decide to run to get there. The token authorizes the agent to touch the inbox at all, but it says nothing about which of those specific actions the user would actually be comfortable with.&lt;/p&gt;

&lt;p&gt;Because of this, teams building serious agent products are moving toward task-scoped or session-scoped credentials rather than one long-lived token reused across everything an agent ever does. Constraints get attached to the task itself: this vendor only, this record only, this time window only, rather than relying on a scope string to carry all of that nuance.&lt;/p&gt;

&lt;p&gt;Delegated authorization for agents increasingly needs to describe a bounded plan, not just a bucket of allowed data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Token Expiry, Refresh Rotation, and Long-Running AI Agent Tasks
&lt;/h2&gt;

&lt;p&gt;OAuth access tokens are intentionally short-lived, often around an hour, with a refresh token used behind the scenes to mint a new one. That design works well for a typical web request that finishes in milliseconds. It gets much harder for an agent running a workflow that spans hours or days: watching an inbox, waiting on a webhook, or pausing for a human approval before continuing.&lt;/p&gt;

&lt;p&gt;If refresh handling is not automatic, an agent can fail silently partway through a task, or worse, keep retrying with an expired token until the provider rate limits or locks the account.&lt;/p&gt;

&lt;p&gt;Refresh token rotation, where a provider issues a brand-new refresh token on every use and immediately invalidates the old one, adds a second failure mode: any system that does not persist the new token instantly, or that triggers two refreshes at once, can permanently lock itself out of a user's connected account.&lt;/p&gt;

&lt;p&gt;This is plumbing that should not be rewritten for every integration a product adds. Corsair &lt;a href="https://docs.corsair.dev/concepts/oauth" rel="noopener noreferrer"&gt;checks token expiry before every API call and refreshes automatically using the stored refresh token&lt;/a&gt;, so a long-running agent task does not need its own retry and refresh logic bolted on for each service it touches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solving the Asynchronous Consent Gap When AI Agents Need New Permissions Mid-Task
&lt;/h2&gt;

&lt;p&gt;Classic OAuth consent happens once, up front, before an application does anything at all. Agents routinely break that assumption by discovering, midway through a task, that they need a permission nobody granted yet. A user asks an agent to find and cancel their old subscriptions, and the agent finds one running through a service it was never connected to in the first place.&lt;/p&gt;

&lt;p&gt;Nobody is sitting there watching every tool call, so pausing the entire task for a synchronous popup does not match how agents actually run. What works better is asynchronous consent: the agent pauses only the one action that needs approval, surfaces a request through a review link, a Slack message, or an email, and continues anything else it can safely do while it waits.&lt;/p&gt;

&lt;p&gt;Once approved, it resumes that specific action instead of restarting the whole run from scratch.&lt;/p&gt;

&lt;p&gt;This is a meaningfully different shape than a redirect-based OAuth consent screen. It looks more like a queued approval system sitting next to OAuth. Corsair's Hub, for example, &lt;a href="https://docs.corsair.dev/hub/overview" rel="noopener noreferrer"&gt;hosts an approve or deny page for gated permissions&lt;/a&gt;, so a blocked action can be reviewed and released without the agent losing the context of the task it was already working through.&lt;/p&gt;

&lt;p&gt;None of this means OAuth is the wrong foundation for AI agents. It just means the layer sitting on top of it now has to do considerably more work: tracking dual identities, enforcing permissions finer than a scope string, refreshing tokens reliably through long-running tasks, and handling consent as an ongoing conversation rather than a one-time screen.&lt;/p&gt;

&lt;p&gt;Corsair was built to carry that weight so individual teams are not rebuilding the same plumbing for every new integration. It wraps OAuth, multi-tenant credential storage, automatic token refresh, and permission gating into a single open-source layer that plugs into an existing app. Anyone building an agent that needs to act across Gmail, Slack, GitHub, or any of the hundreds of other services people rely on can see how it fits together at &lt;a href="https://corsair.dev/" rel="noopener noreferrer"&gt;corsair.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between AI agent authentication and AI agent authorization?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Authentication confirms an agent, or the backend running it, is who it claims to be, usually through an API key or a signed token issued to your application. Authorization is the separate question of what that authenticated agent is allowed to do on a specific user's behalf: which services it can reach and which operations inside those services are permitted. An agent can be fully authenticated and still be authorized for almost nothing, which is exactly the separation OAuth delegation is meant to enforce.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can one OAuth token be shared safely across multiple AI agents or subagents?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Generally not without careful scoping. Sharing a single token across several agents removes the ability to tell which agent instance performed which action, which makes auditing and revocation much harder later. A cleaner pattern is retrieving credentials per tenant and per task, so each agent run operates in its own traceable context even when several agents work on behalf of the same user at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How should a system handle an AI agent that needs a permission it was not originally granted?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The safer pattern blocks only the one action that needs approval rather than the entire task, and routes it through an explicit review step: a hosted approval link, a Slack message, or an email. Once approved, the system resumes that specific action instead of restarting the whole workflow. This asynchronous consent gap is one of the clearest differences between human-facing OAuth flows and agent-facing ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do AI agents need shorter-lived OAuth tokens than typical web apps?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not necessarily shorter, since token lifetime is usually set by the provider rather than the application. What matters more for agents is refresh reliability, since their tasks can run far longer than a typical web session. A token expiring midway through a multi-hour workflow should never cause a silent failure if refresh and rotation are handled automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it safe to give an AI agent full OAuth scopes just to avoid permission errors mid-task?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a common shortcut, and it carries real risk, since a scope grants access to an entire category of data or actions rather than the specific operations an agent actually needs. A safer approach layers finer-grained AI agent permissions on top of the scope itself: reads can be allowed freely while writes and destructive actions get gated behind human approval, so a broad scope never quietly becomes unrestricted autonomous access.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building Multi-Tenant Tool Access for AI Agents</title>
      <dc:creator>Corsair</dc:creator>
      <pubDate>Sat, 29 Aug 2026 12:47:21 +0000</pubDate>
      <link>https://dev.to/corsairdev/building-multi-tenant-tool-access-for-ai-agents-1md</link>
      <guid>https://dev.to/corsairdev/building-multi-tenant-tool-access-for-ai-agents-1md</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw5vo7oy3lh2gscwfxone.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw5vo7oy3lh2gscwfxone.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
An agent that flawlessly handles one Slack workspace and one Gmail inbox in a demo tells you almost nothing about whether it can serve a thousand different customers safely. The moment a product moves from a single test account to real tenants, the question stops being whether the agent can reach a tool, and becomes whether it reaches the right tool, with the right credentials, scoped to the right tenant, without ever touching data that belongs to someone else.&lt;/p&gt;

&lt;p&gt;Multi-tenant AI agents raise a specific version of a problem that multi-tenant software has dealt with for years, made harder by the fact that agents decide at runtime which tools to call and in what order. That runtime decision making means tenant boundaries have to hold at every call an agent might make, not just the handful an engineer thought to test. &lt;/p&gt;

&lt;p&gt;This guide walks through building that access layer deliberately: how to design agent identity and delegated authentication, how to build authorization and policy enforcement granular enough to matter, how a centralized MCP registry keeps tool discovery and credentials manageable as your integration count grows, how tenant isolation needs to be enforced at the database level, and how to sandbox agent code execution safely once agents start writing and running their own code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Multi-Tenant Tool Access Gets Harder When AI Agents Move From Prototype to Production
&lt;/h3&gt;

&lt;p&gt;Most AI agent prototypes start with one set of credentials. A single Slack bot token, one Google account, one API key sitting in an environment variable. That setup works fine for a demo because there is only one tenant in the room: whoever is running the test. The moment a product signs its second customer, that assumption breaks, and it keeps breaking in ways that are easy to miss until they show up as a support ticket or a security incident.&lt;/p&gt;

&lt;p&gt;The core problem is that AI agents behave differently from the applications multi-tenant architecture was originally designed around. A traditional web app calls a small, fixed set of endpoints in a predictable order, so tenant scoping can be checked at a handful of well understood boundaries. An agent decides at runtime which tool to call and sometimes chains several calls together to complete one request. Every one of those decisions is a new place where AI agent tool access needs to be scoped correctly for the tenant making the request, not just the paths a developer happened to test.&lt;/p&gt;

&lt;p&gt;Add multiple tenants into that picture and the failure modes multiply. A calendar invite gets drafted using the wrong customer's Gmail account. An agent retrieves a document scoped to the wrong workspace because the underlying tool call never checked which workspace it was supposed to run against. None of this requires malicious intent. It is simply what happens when tool access is not designed for more than one tenant from the start. Getting this right is a matter of AI agent security as much as it is architecture, and it only gets more expensive to fix the longer it waits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Agent Identity, Delegated Authentication, and Tenant Isolation for Secure Tool Access
&lt;/h2&gt;

&lt;p&gt;An agent is not the same identity as the end user it is acting for, and it is not the same identity as the developer's own backend service either. Treating all three as one identity is where a lot of AI agent authentication problems start. The end user has an account with your product. The tenant is the organization or workspace that user belongs to. The agent is a separate actor that needs permission to act on behalf of that user, inside that tenant, for a specific set of tools, and nothing more.&lt;/p&gt;

&lt;p&gt;Delegated authentication is the mechanism that keeps those three layers connected without collapsing them into one. Instead of an agent holding its own broad credentials for Gmail or Slack, it receives a token issued on behalf of a specific tenant, scoped to specific actions, tied to an authorization the user actually granted. When the agent calls a tool, the system resolves which tenant's credentials apply at that moment, rather than trusting the agent to keep track of whose data it is currently touching. This is the same pattern behind standard OAuth delegation, applied consistently across every tool an agent might call instead of one integration at a time. Corsair's &lt;a href="https://docs.corsair.dev/concepts/auth" rel="noopener noreferrer"&gt;authentication documentation&lt;/a&gt; shows this pattern applied directly: an agent operates through a tenant scoped call, and the underlying OAuth token, API key, or bot token is resolved and refreshed automatically behind it, without the agent ever handling the raw credential itself.&lt;/p&gt;

&lt;p&gt;Tenant isolation follows naturally once identity is designed this way. If every credential lookup is keyed by tenant, and the agent never sees a raw token, only a resolved capability to call a method, there is no code path where one tenant's session can accidentally reach another tenant's account. That guarantee has to live below the agent's reasoning, in the layer that actually executes tool calls, because an agent's own judgment is not a security boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Granular Authorization and Policy Enforcement for Every Agent, Tool, Function, and Action
&lt;/h2&gt;

&lt;p&gt;Authentication answers who is calling. Authorization answers what they are allowed to do once they are in, and for AI agents that question needs an answer at four separate levels: the agent itself, the tool it is calling, the specific function within that tool, and the individual action that function is about to take. Collapsing these into a single yes or no permission check is how agents end up either blocked from harmless reads or, worse, cleared to run destructive writes they were never meant to touch.&lt;/p&gt;

&lt;p&gt;A practical AI agent authorization model starts with scoping access per integration and per tenant, so a workspace that only ever needed read access to a CRM cannot suddenly write to it just because the underlying plugin technically supports writes. From there, functions within a tool get their own scope. Reading a calendar and creating an event are different permissions even though both live inside the same integration. Individual actions with real world consequences, particularly sending an email or deleting a record, deserve a policy check of their own, often one that requires a human to approve before the call executes rather than trusting the agent's confidence that it made the right call.&lt;/p&gt;

&lt;p&gt;Where this policy enforcement actually lives matters as much as how granular it is. It cannot sit inside the model's reasoning, because a language model can be persuaded, confused, or simply wrong about whether an action is safe. It has to sit in the layer between the agent's decision and the actual API call, so the same check runs regardless of how the agent arrived at that decision, and regardless of whether the agent reaches the tool through MCP, a direct SDK call, or a hosted API. Corsair's &lt;a href="https://docs.corsair.dev/concepts/permissions" rel="noopener noreferrer"&gt;permissions documentation&lt;/a&gt; shows one way to implement this: every endpoint carries a risk level of read, write, or destructive, and a permission mode maps each level to an outcome of allow, deny, or require approval, so a destructive call can sit blocked until a human signs off before it ever reaches the provider's API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing a Centralized MCP Registry for Dynamic Tool Discovery, Credentials, and Reliability
&lt;/h2&gt;

&lt;p&gt;Once an agent needs more than a handful of tools, wiring up separate MCP servers for each one starts to show its limits fast. Every new server means another OAuth flow to configure, another set of credentials to store, and another schema competing for space in the agent's context window. Teams that go this route often discover the problem only after the fact: an agent given direct access to forty tool schemas at once starts hallucinating which tool to call, simply because there is too much to reason over in a single request.&lt;/p&gt;

&lt;p&gt;A centralized MCP registry solves this from the opposite direction. Instead of every tool being wired in individually, tools are registered once in a catalog the agent queries dynamically. Rather than injecting every available schema upfront, the registry surfaces only the tools relevant to the current request, which keeps AI agent tool access fast and keeps the context window from filling up with methods the agent will never call in that session. Credentials are resolved behind that same layer, scoped to whichever tenant is making the request, so the agent only ever sees method names and results, never a raw token. This is also the point where MCP tool access gets monitored and rate limited consistently, instead of that logic being reimplemented differently inside each individual tool call.&lt;/p&gt;

&lt;p&gt;Reliability is the other half of what a registry buys you. Rate limits, retries, and the quiet API changes that break integrations without warning all get handled once, centrally, instead of being reimplemented inside every tool call an agent makes. Corsair's MCP adapters work this way in practice: an agent calls a small, fixed set of meta tools, list operations, get schema, run script, and every registered plugin becomes reachable through those same calls, with no additional wiring needed as new tools are added to the catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforcing Database Level Tenant Isolation, Data Residency, and Zero Retention Data Flows
&lt;/h2&gt;

&lt;p&gt;Application level checks are necessary but not sufficient. If a query can technically reach another tenant's row and the only thing stopping it is a conditional in your application code, one missed check away from a data leak is closer than it feels. Database level tenant isolation, through row level security, per tenant schemas, or partitioned tables keyed by tenant ID, means the database itself refuses the query rather than relying on every code path remembering to filter correctly.&lt;/p&gt;

&lt;p&gt;Credentials deserve the same treatment as data. Encrypting each tenant's stored tokens with its own data encryption key, rather than one shared secret for the whole system, means a compromise of one tenant's credentials never cascades into every other tenant's accounts. This is worth getting right early, since retrofitting per tenant encryption after credentials are already stored under a shared key is considerably more painful than designing for it from the start. Corsair's &lt;a href="https://docs.corsair.dev/concepts/multi-tenancy" rel="noopener noreferrer"&gt;multi-tenancy documentation&lt;/a&gt; shows what this looks like at the query level: every insert is tagged with a tenant ID automatically, every read is scoped with a matching where clause, and there is no code path in the normal API that can accidentally cross that boundary.&lt;/p&gt;

&lt;p&gt;Data residency adds another layer for teams selling into regulated industries or specific geographies, where a customer's data needs to physically stay within a jurisdiction rather than simply being logically separated from other tenants. Zero retention data flows matter for what happens after a tool call completes. An agent that resolves a credential, makes a call, and returns a result should not be leaving a copy of the raw payload sitting in a log file or a prompt cache longer than it needs to. Syncing data through webhooks and refreshing it on demand, rather than storing full copies indefinitely, keeps the surface area of what could leak proportional to what the agent actually needs at any given moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing Agent Code Execution With Runtime Sandboxing, Isolation Boundaries, and Progressive Trust
&lt;/h2&gt;

&lt;p&gt;Tool calls are one category of risk. Letting an agent write and execute its own code is a different one, because at that point you are handing over compute, not just an API method. Runtime sandboxing exists for exactly this reason: an isolated environment where agent generated code runs without reaching the host filesystem, the network beyond what is explicitly allowed, or another tenant's session running alongside it.&lt;/p&gt;

&lt;p&gt;The isolation boundaries that matter here are the same ones that matter in any multi-tenant compute environment, just applied to a much less predictable caller. Filesystem access should be scoped to a workspace the sandbox owns and nothing outside it. Network access should default to blocked and get opened only for the specific destinations a task requires. Resource limits on memory and CPU keep one runaway agent loop from degrading the environment every other tenant's agent is also running in. Every sandbox should be ephemeral by default, torn down after use rather than left running and accumulating state nobody is actively reviewing.&lt;/p&gt;

&lt;p&gt;Progressive trust is the piece that often gets skipped in a rush to ship. A new agent, or an agent operating in a context it has not proven itself in yet, should start in the most restrictive sandbox available: no network, minimal filesystem, tight resource caps. Trust should expand only as the agent demonstrates reliable behavior over real usage, the same way you would extend more access to a new hire once their judgment has actually been tested, not on day one. Treating sandbox permissions as something that only ever loosens, and rarely gets revisited once granted, is how a reasonable initial setup quietly turns into an oversized attack surface a year later.&lt;/p&gt;

&lt;p&gt;Corsair handles most of what this guide covers as infrastructure rather than something your team builds from scratch: multi-tenant credential isolation, scoped authorization per tool and per action, a centralized registry for MCP tool access, and encrypted storage keyed per tenant. It is open source and can be self-hosted, so you can inspect exactly how tenant isolation and delegated authentication are implemented rather than trusting a closed system with your users' credentials. If you are building an agent that needs to serve more than one customer safely, corsair.dev is worth a look before you build this layer yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What does multi-tenant tool access mean for AI agents?&lt;/strong&gt;&lt;br&gt;
It means an agent can call the same set of tools, like Gmail, Slack, or a CRM, on behalf of many different customers, while guaranteeing that each customer's credentials, data, and permissions stay completely separate from every other customer's. The tools themselves are shared. The access to them is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How is AI agent authentication different from regular user authentication?&lt;/strong&gt;&lt;br&gt;
Regular user authentication verifies a person logging into a product. AI agent authentication verifies an autonomous process acting on behalf of that person or their organization, usually through a delegated token scoped to specific tools and actions rather than a full login session. The agent's identity, the user's identity, and the tenant it belongs to are tracked as three separate things, not folded into one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between AI agent authentication and AI agent authorization?&lt;/strong&gt;&lt;br&gt;
Authentication confirms which agent, tenant, or user is making a request. Authorization determines what that verified identity is actually allowed to do once inside, down to the level of individual tools, functions, and actions. An agent can be correctly authenticated and still be authorized for almost nothing, which is usually the safer default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why use a centralized MCP registry instead of separate MCP servers per tool?&lt;/strong&gt;&lt;br&gt;
Wiring up a separate MCP server for every tool means repeating OAuth setup, credential storage, and schema maintenance for each one, and it floods the agent's context with every available method whether it needs them or not. A centralized registry handles credential resolution and tool discovery in one place, surfacing only relevant tools per request and keeping MCP tool access consistent as the tool catalog grows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you stop one tenant's data from reaching another tenant's agent session?&lt;/strong&gt;&lt;br&gt;
Isolation has to exist at more than one layer: scoped credentials resolved per tenant at call time, database level checks like row level security that reject cross tenant queries outright, and per tenant encryption keys so a single compromised credential cannot expose other tenants. Relying on application code alone to remember the tenant filter on every query is the most common way this isolation quietly fails.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
