>

MCP authentication: managing credentials, OAuth, and token lifecycle across a team

MCP authentication: managing credentials, OAuth, and token lifecycle across a team

Last updated: June 2026

MCP authentication has two dimensions that most teams don't separate clearly until they run into problems: how a server authenticates connections and who owns the credential being used. Getting both right is what lets you have one team member authenticate with their personal GitHub token while everyone queries Snowflake through a shared service account.

This chapter covers:

  • The auth types MCP supports

  • When to use shared versus personal credentials

  • How the OAuth flow works in a gateway context

  • How to keep tokens healthy across your team without manual intervention

Solutions to consider

The most common approach teams take before implementing centralized credential management is ad hoc per-user configuration: each engineer adds MCP servers to their own Claude setup using their own credentials. For bearer token auth, this means token values in personal config files. For OAuth, each user goes through the authorization flow individually. Nobody has a central view of what credentials exist, and credential rotation is manual and frequently forgotten.

A step up is using environment variables or a secrets manager for shared credentials, with team members referencing the same value. This reduces duplication but doesn't solve the problems of per-user personal credentials, OAuth lifecycle management, or knowing which credential a given tool call used.

Aptible's solution: two-dimensional credential management

The model we built separates the credential problem along two axes.

Auth type determines how the connection authenticates: bearer token, OAuth, custom headers, or none. This is a property of the server: GitHub uses OAuth, an internal tool might use a custom header, and a read-only public server might need no auth at all.

Auth mode determines who owns the credential: the organization (shared) or the individual user (personal). This is a separate decision from auth type. You can have OAuth with shared credentials (one service account authorizes on behalf of the org) or OAuth with personal credentials (each user authorizes individually with their own account).

These two axes combine to create four patterns:


Shared credentials

Personal credentials

Bearer

Service account API key; everyone connects through the same token

Each user brings their own API key; they see their own data

OAuth

Organization-level OAuth; one service account authorizes

Each user goes through OAuth; they see their own data

The correct combination depends on whether individual identity matters for the data being accessed. A Snowflake analytics instance where everyone needs the same data would make sense as shared bearer or shared OAuth. A GitHub instance where commit attribution and personal repos matter: personal OAuth.

When shared credentials make sense vs. personal credentials

The decision is mostly about data access semantics, not security.

Use shared credentials when: the upstream server doesn't need to distinguish between users (everyone should see the same data), you're connecting to a service account rather than a personal account, or the server's rate limits are per-application rather than per-user. Snowflake data warehouses and internal databases are common cases.

Use personal credentials when: the server personalizes data by the authenticated user (GitHub issues assigned to me, Shortcut tickets in my queue, Granola notes from meetings I’ve had), you need the upstream service to track actions by individual user, or the server's features depend on the user's role in that system. The tradeoff is onboarding friction: each new team member needs to authenticate before they can use personal-mode servers.

The onboarding friction can be minimized when done correctly. A new engineer joins the team, gets access to the gateway, and can immediately use all shared-credential servers. Personal-credential servers show up in their tool list only after they connect their own account. Building a clear onboarding flow for this (i.e., "here are the servers you still need to authorize") is worth the investment once your team has more than a handful of personal-mode servers.

How the OAuth flow works when a gateway is the OAuth client

In a standard OAuth flow, your application is the OAuth client; it redirects the user, handles the callback, and stores the token. When a gateway handles OAuth, the gateway plays that role instead. The user still authorizes through the same provider flow, but the resulting token is stored and managed by the gateway, not the application.

For shared-mode OAuth: an admin registers the server, provides the client credentials (client ID and client secret), and authorizes the gateway to connect on behalf of the organization. The resulting access token and refresh token are stored encrypted against the server record. All team members' tool calls use the shared access token.

For personal-mode OAuth: each user authorizes individually. When a user connects a personal-mode server, they go through the OAuth authorization flow in the gateway UI. The resulting token is stored encrypted against their personal credential record, not the server record. Their tool calls use their personal access token.

Managing token expiry: automatic refresh and reauth handling

OAuth access tokens expire. Bearer tokens can be revoked. A credential management layer needs to handle both without requiring manual intervention for the normal case.

The approach we use: a background task runs periodically and refreshes any OAuth token that will expire within a 5-minute window. This covers both shared server tokens and personal user credentials.

REFRESH_WINDOW = timedelta(minutes=5)

@shared_task(max_retries=0)
def refresh_oauth_tokens() -> None:
    cutoff = timezone.now() + REFRESH_WINDOW

    # Refresh expiring shared-mode server tokens
    servers = McpServer.objects.filter(
        is_active=True,
        auth_type="oauth",
        oauth_token_expires_at__lt=cutoff,
    ).exclude(oauth_refresh_token="")
    for server in servers:
        try:
            oauth.refresh(server)
        except oauth.RefreshTokenRevoked as exc:
            server.oauth_needs_reauth = True
            server.save(update_fields=["oauth_needs_reauth", "updated_at"])
        except oauth.RefreshFailed as exc:
            logger.warning(f"OAuth refresh failed for {server.name}: {exc}")

    # Refresh expiring personal credentials
    credentials = McpUserCredential.objects.filter(
        oauth_needs_reauth=False,
        oauth_token_expires_at__lt=cutoff,
    ).exclude(oauth_refresh_token="")
    for credential in credentials:
        try:
            oauth.refresh(credential)
        except oauth.RefreshTokenRevoked as exc:
            credential.oauth_needs_reauth = True
            credential.save(update_fields=["oauth_needs_reauth", "updated_at"])

The oauth_needs_reauth flag is the key mechanism for handling permanent failures. When a refresh token is revoked (the user has de-authorized the application, the token has expired beyond refresh, or the OAuth server has invalidated it), a transient retry won't help. Setting oauth_needs_reauth = True stops the proxy from attempting to use the dead credential and surfaces the need for re-authorization either to the admin for shared credentials, or to the individual user for personal ones.

The proxy checks oauth_needs_reauth before returning a server as available:

# In the proxy's server resolution logic
if server.oauth_needs_reauth:
    return None   # Server not available; user or admin needs to re-authorize

This is a general pattern applicable to any OAuth implementation: distinguish between refresh failures (transient, retry with backoff) and revoked tokens (permanent, require human intervention), and surface the difference clearly rather than silently failing.

How to store MCP credentials securely at rest

All credential values (bearer tokens, OAuth access tokens, refresh tokens, client secrets, and custom headers) are stored encrypted at rest using the same EncryptedCharField / EncryptedJSONField fields described in Encrypting arguments at rest. The same key derivation and encryption approach applies.

Never store raw credential values in your database, your application logs, or anywhere other than the encrypted credential store. In particular:

  • Don't log token values during OAuth flows (even at debug level)

  • Don't include credential values in error messages

  • Don't cache decrypted tokens in application memory beyond the request scope

Bearer tokens for personal-mode servers should be treated as equivalent to passwords: unique per service, rotated when a team member leaves, and never shared outside the credential management system.

FAQs

What's the difference between auth type and auth mode?

Auth type is about the protocol: how the gateway authenticates to the upstream server (bearer token, OAuth, custom headers, or nothing). Auth mode is about ownership: whether the credential belongs to the organization as a whole (shared) or to an individual user (personal). They're independent choices; you can have personal bearer tokens or shared OAuth.

When should I use OAuth instead of bearer tokens?

OAuth is preferable when: the server needs to identify the individual user (not just the application), token rotation happens automatically via refresh, or the server issues scoped tokens that limit what the credential can access. Bearer tokens are simpler to set up and appropriate when individual identity doesn't matter and you're comfortable with manual rotation.

What happens when a user leaves the team?

For shared credentials, nothing changes immediately. The shared token continues to work, and the departed user's tool calls can no longer be made because their gateway access is revoked. For personal credentials, their McpUserCredential record should be soft-deleted as part of offboarding. If their personal OAuth token was also used as a shared credential somewhere (which is a configuration mistake worth auditing for), that token needs to be rotated at the upstream service.

What does oauth_needs_reauth mean in practice?

It means the credential's refresh token has been permanently invalidated and automatic renewal is no longer possible. For shared credentials, an admin needs to re-authorize the gateway. For personal credentials, the individual user needs to reconnect their account. The flag prevents the proxy from attempting to use a dead credential, which would result in confusing upstream errors rather than a clear "your authorization has expired" message.

Can personal-mode servers have different tool sets per user?

Yes, and this is sometimes the right answer. The set of tools a personal OAuth credential can access depends on what scopes were granted during authorization. A user who authorized with read-only GitHub scopes won't have access to write tools even if a grant would otherwise permit them. The available_tools field on McpUserCredential is populated from what the upstream server actually exposes for that credential, not from the server's full tool list.

Next steps

If you need agents to authenticate with their own credentials rather than borrowing a human's: Agent identity and robot users: scoped API keys for non-human principals, with their own role assignments

If you're ready to deploy credential management across your whole team: Deploying MCP for your whole team: from individual setup to org-wide deployment with MDM

If you need to understand how credentials interact with the access control layer: Tool-level access control: how auth mode affects which tools are available to a given user

Aptible MCP Gateway gives engineering teams tool-level access control, audit logging, and centralized credential management for MCP without building the proxy infrastructure yourself. Deployed alongside Aptible AI Gateway, it covers both LLM and tool call governance in one place. Join the MCP Gateway waitlist →

>

text

MCP authentication: managing credentials, OAuth, and token lifecycle across a team

Last updated: June 2026

MCP authentication has two dimensions that most teams don't separate clearly until they run into problems: how a server authenticates connections and who owns the credential being used. Getting both right is what lets you have one team member authenticate with their personal GitHub token while everyone queries Snowflake through a shared service account.

This chapter covers:

  • The auth types MCP supports

  • When to use shared versus personal credentials

  • How the OAuth flow works in a gateway context

  • How to keep tokens healthy across your team without manual intervention

Solutions to consider

The most common approach teams take before implementing centralized credential management is ad hoc per-user configuration: each engineer adds MCP servers to their own Claude setup using their own credentials. For bearer token auth, this means token values in personal config files. For OAuth, each user goes through the authorization flow individually. Nobody has a central view of what credentials exist, and credential rotation is manual and frequently forgotten.

A step up is using environment variables or a secrets manager for shared credentials, with team members referencing the same value. This reduces duplication but doesn't solve the problems of per-user personal credentials, OAuth lifecycle management, or knowing which credential a given tool call used.

Aptible's solution: two-dimensional credential management

The model we built separates the credential problem along two axes.

Auth type determines how the connection authenticates: bearer token, OAuth, custom headers, or none. This is a property of the server: GitHub uses OAuth, an internal tool might use a custom header, and a read-only public server might need no auth at all.

Auth mode determines who owns the credential: the organization (shared) or the individual user (personal). This is a separate decision from auth type. You can have OAuth with shared credentials (one service account authorizes on behalf of the org) or OAuth with personal credentials (each user authorizes individually with their own account).

These two axes combine to create four patterns:


Shared credentials

Personal credentials

Bearer

Service account API key; everyone connects through the same token

Each user brings their own API key; they see their own data

OAuth

Organization-level OAuth; one service account authorizes

Each user goes through OAuth; they see their own data

The correct combination depends on whether individual identity matters for the data being accessed. A Snowflake analytics instance where everyone needs the same data would make sense as shared bearer or shared OAuth. A GitHub instance where commit attribution and personal repos matter: personal OAuth.

When shared credentials make sense vs. personal credentials

The decision is mostly about data access semantics, not security.

Use shared credentials when: the upstream server doesn't need to distinguish between users (everyone should see the same data), you're connecting to a service account rather than a personal account, or the server's rate limits are per-application rather than per-user. Snowflake data warehouses and internal databases are common cases.

Use personal credentials when: the server personalizes data by the authenticated user (GitHub issues assigned to me, Shortcut tickets in my queue, Granola notes from meetings I’ve had), you need the upstream service to track actions by individual user, or the server's features depend on the user's role in that system. The tradeoff is onboarding friction: each new team member needs to authenticate before they can use personal-mode servers.

The onboarding friction can be minimized when done correctly. A new engineer joins the team, gets access to the gateway, and can immediately use all shared-credential servers. Personal-credential servers show up in their tool list only after they connect their own account. Building a clear onboarding flow for this (i.e., "here are the servers you still need to authorize") is worth the investment once your team has more than a handful of personal-mode servers.

How the OAuth flow works when a gateway is the OAuth client

In a standard OAuth flow, your application is the OAuth client; it redirects the user, handles the callback, and stores the token. When a gateway handles OAuth, the gateway plays that role instead. The user still authorizes through the same provider flow, but the resulting token is stored and managed by the gateway, not the application.

For shared-mode OAuth: an admin registers the server, provides the client credentials (client ID and client secret), and authorizes the gateway to connect on behalf of the organization. The resulting access token and refresh token are stored encrypted against the server record. All team members' tool calls use the shared access token.

For personal-mode OAuth: each user authorizes individually. When a user connects a personal-mode server, they go through the OAuth authorization flow in the gateway UI. The resulting token is stored encrypted against their personal credential record, not the server record. Their tool calls use their personal access token.

Managing token expiry: automatic refresh and reauth handling

OAuth access tokens expire. Bearer tokens can be revoked. A credential management layer needs to handle both without requiring manual intervention for the normal case.

The approach we use: a background task runs periodically and refreshes any OAuth token that will expire within a 5-minute window. This covers both shared server tokens and personal user credentials.

REFRESH_WINDOW = timedelta(minutes=5)

@shared_task(max_retries=0)
def refresh_oauth_tokens() -> None:
    cutoff = timezone.now() + REFRESH_WINDOW

    # Refresh expiring shared-mode server tokens
    servers = McpServer.objects.filter(
        is_active=True,
        auth_type="oauth",
        oauth_token_expires_at__lt=cutoff,
    ).exclude(oauth_refresh_token="")
    for server in servers:
        try:
            oauth.refresh(server)
        except oauth.RefreshTokenRevoked as exc:
            server.oauth_needs_reauth = True
            server.save(update_fields=["oauth_needs_reauth", "updated_at"])
        except oauth.RefreshFailed as exc:
            logger.warning(f"OAuth refresh failed for {server.name}: {exc}")

    # Refresh expiring personal credentials
    credentials = McpUserCredential.objects.filter(
        oauth_needs_reauth=False,
        oauth_token_expires_at__lt=cutoff,
    ).exclude(oauth_refresh_token="")
    for credential in credentials:
        try:
            oauth.refresh(credential)
        except oauth.RefreshTokenRevoked as exc:
            credential.oauth_needs_reauth = True
            credential.save(update_fields=["oauth_needs_reauth", "updated_at"])

The oauth_needs_reauth flag is the key mechanism for handling permanent failures. When a refresh token is revoked (the user has de-authorized the application, the token has expired beyond refresh, or the OAuth server has invalidated it), a transient retry won't help. Setting oauth_needs_reauth = True stops the proxy from attempting to use the dead credential and surfaces the need for re-authorization either to the admin for shared credentials, or to the individual user for personal ones.

The proxy checks oauth_needs_reauth before returning a server as available:

# In the proxy's server resolution logic
if server.oauth_needs_reauth:
    return None   # Server not available; user or admin needs to re-authorize

This is a general pattern applicable to any OAuth implementation: distinguish between refresh failures (transient, retry with backoff) and revoked tokens (permanent, require human intervention), and surface the difference clearly rather than silently failing.

How to store MCP credentials securely at rest

All credential values (bearer tokens, OAuth access tokens, refresh tokens, client secrets, and custom headers) are stored encrypted at rest using the same EncryptedCharField / EncryptedJSONField fields described in Encrypting arguments at rest. The same key derivation and encryption approach applies.

Never store raw credential values in your database, your application logs, or anywhere other than the encrypted credential store. In particular:

  • Don't log token values during OAuth flows (even at debug level)

  • Don't include credential values in error messages

  • Don't cache decrypted tokens in application memory beyond the request scope

Bearer tokens for personal-mode servers should be treated as equivalent to passwords: unique per service, rotated when a team member leaves, and never shared outside the credential management system.

FAQs

What's the difference between auth type and auth mode?

Auth type is about the protocol: how the gateway authenticates to the upstream server (bearer token, OAuth, custom headers, or nothing). Auth mode is about ownership: whether the credential belongs to the organization as a whole (shared) or to an individual user (personal). They're independent choices; you can have personal bearer tokens or shared OAuth.

When should I use OAuth instead of bearer tokens?

OAuth is preferable when: the server needs to identify the individual user (not just the application), token rotation happens automatically via refresh, or the server issues scoped tokens that limit what the credential can access. Bearer tokens are simpler to set up and appropriate when individual identity doesn't matter and you're comfortable with manual rotation.

What happens when a user leaves the team?

For shared credentials, nothing changes immediately. The shared token continues to work, and the departed user's tool calls can no longer be made because their gateway access is revoked. For personal credentials, their McpUserCredential record should be soft-deleted as part of offboarding. If their personal OAuth token was also used as a shared credential somewhere (which is a configuration mistake worth auditing for), that token needs to be rotated at the upstream service.

What does oauth_needs_reauth mean in practice?

It means the credential's refresh token has been permanently invalidated and automatic renewal is no longer possible. For shared credentials, an admin needs to re-authorize the gateway. For personal credentials, the individual user needs to reconnect their account. The flag prevents the proxy from attempting to use a dead credential, which would result in confusing upstream errors rather than a clear "your authorization has expired" message.

Can personal-mode servers have different tool sets per user?

Yes, and this is sometimes the right answer. The set of tools a personal OAuth credential can access depends on what scopes were granted during authorization. A user who authorized with read-only GitHub scopes won't have access to write tools even if a grant would otherwise permit them. The available_tools field on McpUserCredential is populated from what the upstream server actually exposes for that credential, not from the server's full tool list.

Next steps

If you need agents to authenticate with their own credentials rather than borrowing a human's: Agent identity and robot users: scoped API keys for non-human principals, with their own role assignments

If you're ready to deploy credential management across your whole team: Deploying MCP for your whole team: from individual setup to org-wide deployment with MDM

If you need to understand how credentials interact with the access control layer: Tool-level access control: how auth mode affects which tools are available to a given user

Aptible MCP Gateway gives engineering teams tool-level access control, audit logging, and centralized credential management for MCP without building the proxy infrastructure yourself. Deployed alongside Aptible AI Gateway, it covers both LLM and tool call governance in one place. Join the MCP Gateway waitlist →

>

text

MCP authentication: managing credentials, OAuth, and token lifecycle across a team

Last updated: June 2026

MCP authentication has two dimensions that most teams don't separate clearly until they run into problems: how a server authenticates connections and who owns the credential being used. Getting both right is what lets you have one team member authenticate with their personal GitHub token while everyone queries Snowflake through a shared service account.

This chapter covers:

  • The auth types MCP supports

  • When to use shared versus personal credentials

  • How the OAuth flow works in a gateway context

  • How to keep tokens healthy across your team without manual intervention

Solutions to consider

The most common approach teams take before implementing centralized credential management is ad hoc per-user configuration: each engineer adds MCP servers to their own Claude setup using their own credentials. For bearer token auth, this means token values in personal config files. For OAuth, each user goes through the authorization flow individually. Nobody has a central view of what credentials exist, and credential rotation is manual and frequently forgotten.

A step up is using environment variables or a secrets manager for shared credentials, with team members referencing the same value. This reduces duplication but doesn't solve the problems of per-user personal credentials, OAuth lifecycle management, or knowing which credential a given tool call used.

Aptible's solution: two-dimensional credential management

The model we built separates the credential problem along two axes.

Auth type determines how the connection authenticates: bearer token, OAuth, custom headers, or none. This is a property of the server: GitHub uses OAuth, an internal tool might use a custom header, and a read-only public server might need no auth at all.

Auth mode determines who owns the credential: the organization (shared) or the individual user (personal). This is a separate decision from auth type. You can have OAuth with shared credentials (one service account authorizes on behalf of the org) or OAuth with personal credentials (each user authorizes individually with their own account).

These two axes combine to create four patterns:


Shared credentials

Personal credentials

Bearer

Service account API key; everyone connects through the same token

Each user brings their own API key; they see their own data

OAuth

Organization-level OAuth; one service account authorizes

Each user goes through OAuth; they see their own data

The correct combination depends on whether individual identity matters for the data being accessed. A Snowflake analytics instance where everyone needs the same data would make sense as shared bearer or shared OAuth. A GitHub instance where commit attribution and personal repos matter: personal OAuth.

When shared credentials make sense vs. personal credentials

The decision is mostly about data access semantics, not security.

Use shared credentials when: the upstream server doesn't need to distinguish between users (everyone should see the same data), you're connecting to a service account rather than a personal account, or the server's rate limits are per-application rather than per-user. Snowflake data warehouses and internal databases are common cases.

Use personal credentials when: the server personalizes data by the authenticated user (GitHub issues assigned to me, Shortcut tickets in my queue, Granola notes from meetings I’ve had), you need the upstream service to track actions by individual user, or the server's features depend on the user's role in that system. The tradeoff is onboarding friction: each new team member needs to authenticate before they can use personal-mode servers.

The onboarding friction can be minimized when done correctly. A new engineer joins the team, gets access to the gateway, and can immediately use all shared-credential servers. Personal-credential servers show up in their tool list only after they connect their own account. Building a clear onboarding flow for this (i.e., "here are the servers you still need to authorize") is worth the investment once your team has more than a handful of personal-mode servers.

How the OAuth flow works when a gateway is the OAuth client

In a standard OAuth flow, your application is the OAuth client; it redirects the user, handles the callback, and stores the token. When a gateway handles OAuth, the gateway plays that role instead. The user still authorizes through the same provider flow, but the resulting token is stored and managed by the gateway, not the application.

For shared-mode OAuth: an admin registers the server, provides the client credentials (client ID and client secret), and authorizes the gateway to connect on behalf of the organization. The resulting access token and refresh token are stored encrypted against the server record. All team members' tool calls use the shared access token.

For personal-mode OAuth: each user authorizes individually. When a user connects a personal-mode server, they go through the OAuth authorization flow in the gateway UI. The resulting token is stored encrypted against their personal credential record, not the server record. Their tool calls use their personal access token.

Managing token expiry: automatic refresh and reauth handling

OAuth access tokens expire. Bearer tokens can be revoked. A credential management layer needs to handle both without requiring manual intervention for the normal case.

The approach we use: a background task runs periodically and refreshes any OAuth token that will expire within a 5-minute window. This covers both shared server tokens and personal user credentials.

REFRESH_WINDOW = timedelta(minutes=5)

@shared_task(max_retries=0)
def refresh_oauth_tokens() -> None:
    cutoff = timezone.now() + REFRESH_WINDOW

    # Refresh expiring shared-mode server tokens
    servers = McpServer.objects.filter(
        is_active=True,
        auth_type="oauth",
        oauth_token_expires_at__lt=cutoff,
    ).exclude(oauth_refresh_token="")
    for server in servers:
        try:
            oauth.refresh(server)
        except oauth.RefreshTokenRevoked as exc:
            server.oauth_needs_reauth = True
            server.save(update_fields=["oauth_needs_reauth", "updated_at"])
        except oauth.RefreshFailed as exc:
            logger.warning(f"OAuth refresh failed for {server.name}: {exc}")

    # Refresh expiring personal credentials
    credentials = McpUserCredential.objects.filter(
        oauth_needs_reauth=False,
        oauth_token_expires_at__lt=cutoff,
    ).exclude(oauth_refresh_token="")
    for credential in credentials:
        try:
            oauth.refresh(credential)
        except oauth.RefreshTokenRevoked as exc:
            credential.oauth_needs_reauth = True
            credential.save(update_fields=["oauth_needs_reauth", "updated_at"])

The oauth_needs_reauth flag is the key mechanism for handling permanent failures. When a refresh token is revoked (the user has de-authorized the application, the token has expired beyond refresh, or the OAuth server has invalidated it), a transient retry won't help. Setting oauth_needs_reauth = True stops the proxy from attempting to use the dead credential and surfaces the need for re-authorization either to the admin for shared credentials, or to the individual user for personal ones.

The proxy checks oauth_needs_reauth before returning a server as available:

# In the proxy's server resolution logic
if server.oauth_needs_reauth:
    return None   # Server not available; user or admin needs to re-authorize

This is a general pattern applicable to any OAuth implementation: distinguish between refresh failures (transient, retry with backoff) and revoked tokens (permanent, require human intervention), and surface the difference clearly rather than silently failing.

How to store MCP credentials securely at rest

All credential values (bearer tokens, OAuth access tokens, refresh tokens, client secrets, and custom headers) are stored encrypted at rest using the same EncryptedCharField / EncryptedJSONField fields described in Encrypting arguments at rest. The same key derivation and encryption approach applies.

Never store raw credential values in your database, your application logs, or anywhere other than the encrypted credential store. In particular:

  • Don't log token values during OAuth flows (even at debug level)

  • Don't include credential values in error messages

  • Don't cache decrypted tokens in application memory beyond the request scope

Bearer tokens for personal-mode servers should be treated as equivalent to passwords: unique per service, rotated when a team member leaves, and never shared outside the credential management system.

FAQs

What's the difference between auth type and auth mode?

Auth type is about the protocol: how the gateway authenticates to the upstream server (bearer token, OAuth, custom headers, or nothing). Auth mode is about ownership: whether the credential belongs to the organization as a whole (shared) or to an individual user (personal). They're independent choices; you can have personal bearer tokens or shared OAuth.

When should I use OAuth instead of bearer tokens?

OAuth is preferable when: the server needs to identify the individual user (not just the application), token rotation happens automatically via refresh, or the server issues scoped tokens that limit what the credential can access. Bearer tokens are simpler to set up and appropriate when individual identity doesn't matter and you're comfortable with manual rotation.

What happens when a user leaves the team?

For shared credentials, nothing changes immediately. The shared token continues to work, and the departed user's tool calls can no longer be made because their gateway access is revoked. For personal credentials, their McpUserCredential record should be soft-deleted as part of offboarding. If their personal OAuth token was also used as a shared credential somewhere (which is a configuration mistake worth auditing for), that token needs to be rotated at the upstream service.

What does oauth_needs_reauth mean in practice?

It means the credential's refresh token has been permanently invalidated and automatic renewal is no longer possible. For shared credentials, an admin needs to re-authorize the gateway. For personal credentials, the individual user needs to reconnect their account. The flag prevents the proxy from attempting to use a dead credential, which would result in confusing upstream errors rather than a clear "your authorization has expired" message.

Can personal-mode servers have different tool sets per user?

Yes, and this is sometimes the right answer. The set of tools a personal OAuth credential can access depends on what scopes were granted during authorization. A user who authorized with read-only GitHub scopes won't have access to write tools even if a grant would otherwise permit them. The available_tools field on McpUserCredential is populated from what the upstream server actually exposes for that credential, not from the server's full tool list.

Next steps

If you need agents to authenticate with their own credentials rather than borrowing a human's: Agent identity and robot users: scoped API keys for non-human principals, with their own role assignments

If you're ready to deploy credential management across your whole team: Deploying MCP for your whole team: from individual setup to org-wide deployment with MDM

If you need to understand how credentials interact with the access control layer: Tool-level access control: how auth mode affects which tools are available to a given user

Aptible MCP Gateway gives engineering teams tool-level access control, audit logging, and centralized credential management for MCP without building the proxy infrastructure yourself. Deployed alongside Aptible AI Gateway, it covers both LLM and tool call governance in one place. Join the MCP Gateway waitlist →