# Call External APIs from MCP

Your MCP tools often need to call external APIs as the authenticated user: GitHub for issues and pull requests, Google for calendar or Drive, Slack for messages, or your own internal APIs. Delegated access is the pattern that lets the tool do that without shared service accounts, copied API keys, or blanket OAuth grants.

Keycard handles the token exchange: you write tools, Keycard manages OAuth flows, credential exchange, policy checks, audit, and per-user refresh.

> **Tip:** Complete the [Add Auth to Custom MCP](/guides/mcp-server/) guide first. This guide builds on that pattern.

## How delegated access works

Token exchange lets your MCP server act as both a **protected resource** (receiving authenticated requests from AI agents) and an **API client** (calling upstream APIs on behalf of the authenticated user).

<div class="wb-strip not-content">
  <div class="wb-item">
    <div class="wb-icon">
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style={{display:'inline-block',height:'16px',maxWidth:'none'}}><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
    </div>
    <span class="wb-label">User</span>
    <span class="wb-desc">authenticated via Keycard</span>
  </div>
  <div class="wb-arrow"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style={{display:'inline-block',height:'14px',maxWidth:'none'}}><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg></div>
  <div class="wb-item">
    <div class="wb-icon">
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style={{display:'inline-block',height:'16px',maxWidth:'none'}}><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>
    </div>
    <span class="wb-label">AI Agent</span>
    <span class="wb-desc">routes tool calls</span>
  </div>
  <div class="wb-arrow"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style={{display:'inline-block',height:'14px',maxWidth:'none'}}><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg></div>
  <div class="wb-item">
    <div class="wb-icon">
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style={{display:'inline-block',height:'16px',maxWidth:'none'}}><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.84Z"/><path d="m6.08 9.5-3.5 1.6a1 1 0 0 0 0 1.81l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9a1 1 0 0 0 0-1.83l-3.5-1.59"/></svg>
    </div>
    <span class="wb-label">Your MCP Server</span>
    <span class="wb-desc">exchanges token</span>
  </div>
  <div class="wb-arrow"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style={{display:'inline-block',height:'14px',maxWidth:'none'}}><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg></div>
  <div class="wb-item">
    <div class="wb-icon">
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style={{display:'inline-block',height:'16px',maxWidth:'none'}}><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/></svg>
    </div>
    <span class="wb-label">Keycard</span>
    <span class="wb-desc">scopes &amp; issues token</span>
  </div>
  <div class="wb-arrow"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style={{display:'inline-block',height:'14px',maxWidth:'none'}}><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg></div>
  <div class="wb-item">
    <div class="wb-icon">
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style={{display:'inline-block',height:'16px',maxWidth:'none'}}><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
    </div>
    <span class="wb-label">External API</span>
    <span class="wb-desc">GitHub, Google, etc.</span>
  </div>
</div>

<style>{`
  .wb-strip {
    display: flex;
    align-items: stretch;
    justify-content: center;
    gap: 6px;
    margin: 20px 0 8px;
    flex-wrap: nowrap;
  }
  .wb-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    padding: 10px 12px 8px;
    border: 1px solid var(--border-color-faint);
    border-radius: 6px;
    background: var(--color-background-accent);
    flex: 1 1 0;
    min-width: 0;
    max-width: 140px;
  }
  .wb-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
  }
  .wb-icon svg { color: var(--text-muted); }
  .wb-label {
    font-size: 12px;
    font-weight: 600;
    color: var(--stl-color-foreground);
    text-align: center;
    line-height: 1.25;
  }
  .wb-desc {
    font-size: 11px;
    color: var(--text-muted);
    text-align: center;
    line-height: 1.35;
  }
  .wb-arrow {
    display: flex;
    align-items: center;
    align-self: center;
    flex-shrink: 0;
  }
  .wb-arrow svg { color: color-mix(in srgb, var(--stl-color-purple) 50%, transparent); }
  @media (max-width: 720px) {
    .wb-strip { flex-direction: column; gap: 0; max-width: 220px; margin-left: auto; margin-right: auto; }
    .wb-item { max-width: 100%; width: 100%; min-width: 0; }
    .wb-arrow { transform: rotate(90deg); padding: 4px 0; }
  }
`}</style>

1. User authenticates to your MCP server with a Keycard token
2. Your MCP server exchanges that token for an external API token scoped to the user
3. Your MCP server calls the external API with the exchanged token
4. The user's data is accessed with their own permissions, not a shared service account

> **Note: Is the user present?** Delegated access exchanges the user's token from the incoming request, so it assumes the user is in the loop. If the user isn't present (scheduled jobs, queue workers, long-running agents), see [Act on Behalf of Absent Users](/guides/act-on-behalf-of-absent-users/) instead.

## Keycard setup

These steps are the same regardless of which external API you're integrating. Complete them before following a provider-specific guide.

1. **Copy your Keycard redirect URL**

   In [Keycard Console](https://console.keycard.ai), open **Settings** → **Connection** and copy the **Redirect URL**. You will need this when creating the OAuth App at your provider.

2. **Create an OAuth App at your provider**

   In your provider's developer console (e.g., GitHub Developer Settings, Google Cloud Console), create an OAuth App. Set the authorization callback / redirect URI to the URL you copied in step 1. Note the **Client ID** and **Client Secret**. You will need them in the next step.

3. **Add the API from the Catalog**

   In Keycard Console, navigate to **Resources** -> **Add Resource** -> **Explore Resources** and add the API you want to integrate. Enter the Client ID and Client Secret from step 2.

   > **Note:** For APIs not in the catalog, you can [manually create providers and resources](/concepts/resources/) in Keycard Console - any OAuth 2.0 provider works.

4. **Register your MCP server Resource**

   Navigate to **Resources** → **Add Resource** → **Add Manually**:

   | Field | Value |
   | --- | --- |
   | **Resource Name** | Your MCP Server |
   | **Resource Identifier** | `http://localhost:8000/mcp` |
   | **Credential Provider** | Zone Provider |

   > **Note:** The Resource Identifier must match the URL where your MCP server is reachable.

5. **Create an Application**

   Navigate to **Applications** → **Add Application**, give it a name, and click **Create Application**.

   Then, on the application details page:
   - On the **Provides** tab, click **Add provided resource** and select your MCP Server.
   - On the **Dependencies** tab, click **Add dependency** and select the external API Resource(s).

   After creating the Application, generate **client credentials** (Client ID + Client Secret) and save them. These go into your environment as `KEYCARD_CLIENT_ID` and `KEYCARD_CLIENT_SECRET`. Your MCP server passes them to `AuthProvider` so it can exchange tokens on behalf of users.

## The Grant Pattern

Every third-party integration follows the same steps:

1. **Add from Resource Catalog** (or [manually create](/concepts/resources/) the provider and resource for any OAuth 2.0 API)
2. **Register your MCP server Resource**: set Credential Provider to **Zone Provider**
3. **Create an Application**: name it, click **Create Application**, then on the **Provides** tab add your MCP server and on the **Dependencies** tab add the external APIs
4. **Generate client credentials**: Client ID + Client Secret for your Application
5. **Use the grant pattern** in code:

**Python:**

```python
@auth_provider.grant("https://api.example.com")
async def my_tool(ctx: Context):
    access_context: AccessContext = await ctx.get_state("keycardai")
    token = access_context.access("https://api.example.com").access_token
    # Call API with token
```

**TypeScript:**

```typescript
app.get("/api/endpoint", authProvider.grant("https://api.example.com"), async (req, res) => {
  const { accessContext } = req as DelegatedRequest;
  const token = accessContext.access("https://api.example.com").accessToken;
  // Call API with token
});
```

**Go:**

```go
// Wrap your MCP handler with the grant middleware
handler := authProvider.Grant([]string{"https://api.example.com"})(mcpHandler)

// Inside a tool handler:
ac := mcp.AccessContextFromContext(ctx)
token, _ := ac.Access("https://api.example.com")
// Call API with token.AccessToken
```

**Ruby:**

```ruby
# Wrap your MCP endpoint with the grant middleware
use auth_provider.grant("https://api.example.com")

# Inside the handler:
access_context = Keycardai::MCP.access_context(env)
token = access_context.access("https://api.example.com").access_token
# Call API with token
```

This works for any OAuth 2.0 provider: Slack, Linear, Notion, and more.

## Provider Guides

Follow a provider-specific guide to see the full implementation:

  - [GitHub](/guides/delegated-access/github/)
  - [Google Workspace](/guides/delegated-access/google/)

## Production Notes

When deploying to production:

- **Update Resource Identifiers** in Keycard Console to your production URLs (must use HTTPS)
- **Update OAuth redirect URIs** in GitHub/Google to match your production Keycard zone
- **Set environment variables** securely: never commit client secrets to source control
- **Token caching and refresh** is handled automatically by Keycard

> **Tip:** For infrastructure-as-code deployments, use the <ExternalLink href="https://registry.terraform.io/providers/keycardai/keycard/latest/docs">Keycard Terraform Provider</ExternalLink> to programmatically configure Providers, Resources, and Applications.

## Troubleshooting

### Token Exchange Fails

**Symptom**: `access_context.has_errors()` returns `True` (Python) / `accessContext.hasErrors()` returns `true` (TypeScript) / `ac.HasErrors()` returns `true` (Go) / `context.errors?` returns `true` (Ruby)

- Verify the external API Resource is added as a **Dependency** on your Application
- Check that the OAuth provider credentials (in Resource Catalog) are correct
- Ensure the user has completed the OAuth consent flow for the external API
- Confirm your MCP server is reachable at the registered Resource Identifier URL

### Invalid or Expired Token

**Symptom**: External API returns 401 Unauthorized

- Token refresh is automatic. If this persists, check provider configuration
- Verify the scopes in Keycard Console match what the API requires
- The user may need to re-authorize if they revoked access

### Scope Mismatch

**Symptom**: External API returns 403 Forbidden or missing data

- Ensure scopes in Keycard Console match what the API requires
- For Google: verify the required APIs are enabled in Google Cloud Console
- User may need to disconnect and reconnect to pick up new scopes
