Multiple Authentication Methods

Connectors

Store and manage multiple sets of credentials for a single connector, then call tools with specific credentials to control access at runtime.

This cookbook covers two authentication patterns:

  • Bearer token — static tokens (GitHub PATs), stored directly via the API.
  • OAuth2 — delegated auth flows (Microsoft accounts), initiated via get_auth_url.

API status: Credentials management uses client.beta.connectors. This is a beta endpoint and may change.


Table of Contents


Part 1: Multiple Bearer Token Credentials (GitHub MCP)

Prerequisites (Bearer)

MISTRAL_API_KEY=your-mistral-api-key
GITHUB_PAT_FULL=ghp_yourFullAccessToken
GITHUB_PAT_LIMITED=ghp_yourLimitedOrInvalidToken
  • GITHUB_PAT_FULL — a PAT with repo read scope, used to successfully list issues.
  • GITHUB_PAT_LIMITED — a PAT with no scopes or an invalid value, used to demonstrate a rejected call.

Create GitHub personal access tokens in your GitHub developer settings.

Script: python/src/scripts/07_multiple_bearer_authentication.py


When to Use Multiple Bearer Credentials

  • Test access tiers — verify that a restricted token cannot reach resources a full token can.
  • Rotate credentials safely — add the new credentials, promote it to default, then delete the old one with no downtime.
  • Scope tool calls explicitly — pass credentials_name to call_tool to choose which identity executes the request.

1. Initialize the Client

Python:

import os
from mistralai import Mistral

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

curl:

export MISTRAL_API_KEY="your-api-key"
export BASE_URL="https://api.mistral.ai"

2. Create a GitHub MCP Connector

You can skip this step if you use an existing connector with bearer authentication.

Python:

import asyncio
import json
import os
import subprocess

BASE_URL = "https://api.mistral.ai"
API_KEY = os.environ["MISTRAL_API_KEY"]


async def main() -> None:
    result = subprocess.run(
        [
            "curl", "-s", "-X", "POST",
            f"{BASE_URL}/v1/connectors",
            "-H", f"Authorization: Bearer {API_KEY}",
            "-H", "Content-Type: application/json",
            "-d", json.dumps({
                "name": "my_github",
                "description": "GitHub MCP connector for issue and PR management",
                "server": "https://api.githubcopilot.com/mcp/",
                "visibility": "private",
                "auth_scheme": {"type": "http", "scheme": "Bearer"},
            }),
        ],
        capture_output=True,
        text=True,
        check=True,
    )
    connector = json.loads(result.stdout)
    if "id" not in connector:
        raise RuntimeError(f"Failed to create connector: {result.stdout}")
    print(f"ID:   {connector['id']}")
    print(f"Name: {connector['name']}")


asyncio.run(main())

curl:

curl -X POST "${BASE_URL}/v1/connectors" \
  -H "Authorization: Bearer ${MISTRAL_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my_github",
    "description": "GitHub MCP connector for issue and PR management",
    "server": "https://api.githubcopilot.com/mcp/",
    "visibility": "private",
    "auth_scheme": {"type": "http", "scheme": "Bearer"}
  }'

Output:

ID:   a1b2c3d4-5678-90ab-cdef-1234567890ab
Name: my_github
ErrorCauseFix
409 ConflictA connector named my_github already existsChoose a different name or delete the existing one first

3. Get Authentication Methods

Goal: Discover which authentication schemes the connector supports before storing credentials.

Note: the connector_id_or_name is a bit ugly and will be replaced by connector_ref in future versions.

Python:

import asyncio
from mistralai import Mistral

client = Mistral(api_key="your-api-key")


async def main() -> None:
    methods = await client.beta.connectors.get_authentication_methods_async(
        connector_id_or_name="my_github",
    )
    for method in methods:
        print(f"Auth type: {method.method_type}")


asyncio.run(main())

curl:

curl -X GET "${BASE_URL}/v1/connectors/my_github/authentication_methods" \
  -H "Authorization: Bearer ${MISTRAL_API_KEY}"

Example output:

Auth type: bearer

4. Store Bearer Credentials

Goal: Store named bearer-token credentials on the connector.

Credentials can be stored at three scopes:

ScopeSDK methodWho can use it
usercreate_or_update_user_credentialsOnly the authenticated user
workspacecreate_or_update_workspace_credentialsEveryone in the workspace
organizationcreate_or_update_organization_credentialsEveryone in the organization

Python:

import asyncio
import os
from mistralai import Mistral

client = Mistral(api_key="your-api-key")


async def main() -> None:
    # Credentials A — full repo read access, set as default
    result = await client.beta.connectors.create_or_update_user_credentials_async(
        connector_id_or_name="my_github",
        name="github-pat-full",
        credentials={"bearer_token": os.environ["GITHUB_PAT_FULL"]},
        is_default=True,
    )
    print(result.message)

    # Credentials B — no scopes / invalid token
    result = await client.beta.connectors.create_or_update_user_credentials_async(
        connector_id_or_name="my_github",
        name="github-pat-limited",
        credentials={"bearer_token": os.environ["GITHUB_PAT_LIMITED"]},
    )
    print(result.message)


asyncio.run(main())

curl:

# Credentials A — full repo read access (set as default)
curl -X POST "${BASE_URL}/v1/connectors/my_github/user/credentials" \
  -H "Authorization: Bearer ${MISTRAL_API_KEY}" \
  -H "Content-Type: application/json" \
  -d "{
    \"name\": \"github-pat-full\",
    \"credentials\": {\"bearer_token\": \"${GITHUB_PAT_FULL}\"},
    \"is_default\": true
  }"

# Credentials B — no scopes / invalid token
curl -X POST "${BASE_URL}/v1/connectors/my_github/user/credentials" \
  -H "Authorization: Bearer ${MISTRAL_API_KEY}" \
  -H "Content-Type: application/json" \
  -d "{
    \"name\": \"github-pat-limited\",
    \"credentials\": {\"bearer_token\": \"${GITHUB_PAT_LIMITED}\"}
  }"

Output:

Credentials 'github-pat-full' saved successfully
Credentials 'github-pat-limited' saved successfully

How it works:

  • is_default: true marks the credentials as default — calls that omit credentials_name will use it.
  • Calling the same endpoint again with an existing name updates the stored token in place.
  • The raw token is never returned by list or get endpoints.
ErrorCauseFix
400 Bad RequestEmpty credentials objectProvide at least bearer_token
422 Unprocessable EntityInvalid credentials nameUse alphanumeric names with hyphens only

5. List Credentials

Python:

import asyncio
from mistralai import Mistral

client = Mistral(api_key="your-api-key")


async def main() -> None:
    response = await client.beta.connectors.list_user_credentials_async(
        connector_id_or_name="my_github",
    )
    for cred in response.credentials:
        default_marker = " (default)" if cred.is_default else ""
        print(f"  {cred.name}  [{cred.authentication_type}]{default_marker}")


asyncio.run(main())

curl:

curl -X GET "${BASE_URL}/v1/connectors/my_github/user/credentials" \
  -H "Authorization: Bearer ${MISTRAL_API_KEY}"

Output:

  github-pat-full  [bearer] (default)
  github-pat-limited  [bearer]

6. Call a Tool with Specific Credentials

Python:

import asyncio
from mistralai import Mistral

client = Mistral(api_key="your-api-key")


async def main() -> None:
    # Call with the full-access credentials — should succeed
    result = await client.beta.connectors.call_tool_async(
        connector_id_or_name="my_github",
        tool_name="list_issues",
        arguments={"owner": "octocat", "repo": "hello-world", "state": "open"},
        credentials_name="github-pat-full",
    )
    print(f"[github-pat-full] {result.content[:200]}")

    # Call with the limited/invalid credentials — access error is in the response content
    result = await client.beta.connectors.call_tool_async(
        connector_id_or_name="my_github",
        tool_name="list_issues",
        arguments={"owner": "octocat", "repo": "hello-world", "state": "open"},
        credentials_name="github-pat-limited",
    )
    print(f"[github-pat-limited] {result.content[:200]}")


asyncio.run(main())

curl:

curl -X POST "${BASE_URL}/v1/connectors/my_github/call_tool" \
  -H "Authorization: Bearer ${MISTRAL_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "tool_name": "list_issues",
    "arguments": {"owner": "octocat", "repo": "hello-world", "state": "open"},
    "credentials_name": "github-pat-full"
  }'

Example output:

[github-pat-full] [{"number": 42, "title": "Fix typo in README", "state": "open", ...}]
[github-pat-limited] {"error": "Bad credentials", "status": 401}

How it works:

  • credentials_name selects which stored credentials the MCP server receives. Omit it to use the default.
  • If the named credentials does not exist, the call returns a 404.

7. Delete Credentials

Note: You cannot delete the default credentials while other credentials exist. Promote a different credentials to default first, then delete the old one.

Python:

import asyncio
from mistralai import Mistral

client = Mistral(api_key="your-api-key")


async def main() -> None:
    result = await client.beta.connectors.delete_user_credentials_async(
        connector_id_or_name="my_github",
        credentials_name="github-pat-limited",
    )
    print(result.message)


asyncio.run(main())

curl:

curl -X DELETE "${BASE_URL}/v1/connectors/my_github/user/credentials/github-pat-limited" \
  -H "Authorization: Bearer ${MISTRAL_API_KEY}"

Output:

Credentials 'github-pat-limited' deleted successfully
ErrorCauseFix
404 Not FoundCredentials name does not existCheck the name with list_user_credentials first
409 ConflictTrying to delete the current default while others existPromote another credentials to default first

Part 2: Multiple OAuth2 Credentials (ex: Outlook Calendar MCP)

Prerequisites (OAuth2)

MISTRAL_API_KEY=your-mistral-api-key
  • The outlook_calendar connector must be enabled in your workspace. Enable it in the Mistral Console.
  • You need two Microsoft accounts to authenticate separately.

Script: python/src/scripts/08_multiple_oauth_authentication.py


When to Use Multiple OAuth2 Credentials

  • Multi-account access — let a single user authenticate with multiple identities (e.g., work and personal Microsoft accounts) and switch between them at call time.
  • Per-user delegation — each user in a workspace authenticates their own account; credentials_name routes tool calls to the right one.
  • Safe rotation — authenticate the new account under a new name, promote it to default, then revoke the old one.

1. Get the Outlook Calendar Connector

Python:

import asyncio
from mistralai import Mistral

client = Mistral(api_key="your-api-key")


async def main() -> None:
    connector = await client.beta.connectors.get_async(
        connector_id_or_name="outlook_calendar",
    )
    print(f"ID:   {connector.id}")
    print(f"Name: {connector.name}")


asyncio.run(main())

curl:

curl -X GET "${BASE_URL}/v1/connectors/outlook_calendar" \
  -H "Authorization: Bearer ${MISTRAL_API_KEY}"

2. Get Authentication Methods

Python:

import asyncio
from mistralai import Mistral

client = Mistral(api_key="your-api-key")


async def main() -> None:
    methods = await client.beta.connectors.get_authentication_methods_async(
        connector_id_or_name="outlook_calendar",
    )
    for method in methods:
        print(f"Auth type: {method.method_type}")


asyncio.run(main())

curl:

curl -X GET "${BASE_URL}/v1/connectors/outlook_calendar/authentication_methods" \
  -H "Authorization: Bearer ${MISTRAL_API_KEY}"

Example output:

Auth type: oauth2

3. Authenticate Accounts via OAuth2

Goal: Obtain OAuth2 authorization URLs and let each account complete the browser flow. The credentials_name parameter controls which named slot the resulting token is stored in. Omitting it stores the token as the default credentials.

Python:

import asyncio
from mistralai import Mistral

client = Mistral(api_key="your-api-key")


async def main() -> None:
    # Account A — stored as the default credentials
    result = await client.beta.connectors.get_auth_url_async(
        connector_id_or_name="outlook_calendar",
        # no credentials_name => stored under name="default"
    )
    print(f"Follow this link to authenticate account A: {result.auth_url}")
    input("Press Enter once done")

    # Account B — stored under the name "personal"
    result = await client.beta.connectors.get_auth_url_async(
        connector_id_or_name="outlook_calendar",
        credentials_name="personal",
    )
    print(f"Follow this link to authenticate account B: {result.auth_url}")
    input("Press Enter once done")


asyncio.run(main())

curl:

# Account A — default credentials
curl -X GET "${BASE_URL}/v1/connectors/outlook_calendar/auth_url" \
  -H "Authorization: Bearer ${MISTRAL_API_KEY}"

# Account B — named "personal"
curl -X GET "${BASE_URL}/v1/connectors/outlook_calendar/auth_url?credentials_name=personal" \
  -H "Authorization: Bearer ${MISTRAL_API_KEY}"

How it works:

  • get_auth_url returns a URL the user must open in a browser to complete the OAuth2 consent flow.
  • Once the flow completes, the token is stored automatically under the given credentials_name (or as default if omitted).
  • The script pauses with input() to give the user time to complete the browser flow before proceeding.

4. List Credentials

Python:

import asyncio
from mistralai import Mistral

client = Mistral(api_key="your-api-key")


async def main() -> None:
    response = await client.beta.connectors.list_user_credentials_async(
        connector_id_or_name="outlook_calendar",
    )
    for cred in response.credentials:
        default_marker = " (default)" if cred.is_default else ""
        print(f"  {cred.name}  [{cred.authentication_type}]{default_marker}")


asyncio.run(main())

Output:

  default  [oauth2] (default)
  personal  [oauth2]

5. Call a Tool with Specific Credentials

Goal: Invoke a calendar tool using a named credentials to query a specific account's calendar.

Python:

import asyncio
from mistralai import Mistral

client = Mistral(api_key="your-api-key")


async def main() -> None:
    # Query the default account
    result = await client.beta.connectors.call_tool_async(
        connector_id_or_name="outlook_calendar",
        tool_name="search_calendar_events",
        arguments={"query": "meeting"},
        credentials_name="default",
    )
    print(f"[default] {result.content[:300]}")

    # Query the personal account
    result = await client.beta.connectors.call_tool_async(
        connector_id_or_name="outlook_calendar",
        tool_name="search_calendar_events",
        arguments={"query": "meeting"},
        credentials_name="personal",
    )
    print(f"[personal] {result.content[:300]}")


asyncio.run(main())

curl:

curl -X POST "${BASE_URL}/v1/connectors/outlook_calendar/call_tool" \
  -H "Authorization: Bearer ${MISTRAL_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"tool_name": "search_calendar_events", "arguments": {"query": "meeting"}, "credentials_name": "default"}'

6. Promote Credentials to Default

To change which account is used when credentials_name is omitted, update is_default without providing a new token — the stored OAuth2 token is preserved.

Python:

import asyncio
from mistralai import Mistral

client = Mistral(api_key="your-api-key")


async def main() -> None:
    await client.beta.connectors.create_or_update_user_credentials_async(
        connector_id_or_name="outlook_calendar",
        name="personal",
        is_default=True,
    )
    print("Promoted 'personal' to default")

    # Now call without specifying credentials — uses personal account
    result = await client.beta.connectors.call_tool_async(
        connector_id_or_name="outlook_calendar",
        tool_name="search_calendar_events",
        arguments={"query": "meeting"},
    )
    print(f"[default] {result.content[:300]}")


asyncio.run(main())

7. Delete Credentials

Note: You cannot delete the default credentials while other credentials exist. Promote another credentials to default first.

Python:

import asyncio
from mistralai import Mistral

client = Mistral(api_key="your-api-key")


async def main() -> None:
    for name in ("default", "personal"):
        result = await client.beta.connectors.delete_user_credentials_async(
            connector_id_or_name="outlook_calendar",
            credentials_name=name,
        )
        print(result.message)


asyncio.run(main())

curl:

curl -X DELETE "${BASE_URL}/v1/connectors/outlook_calendar/user/credentials/personal" \
  -H "Authorization: Bearer ${MISTRAL_API_KEY}"

Naming Conventions

ConceptPython
Get connectorclient.beta.connectors.get_async(connector_id_or_name=)
Get auth methodsget_authentication_methods_async(connector_id_or_name=)
Get OAuth2 URLget_auth_url_async(connector_id_or_name=, credentials_name=)
Store bearer credscreate_or_update_user_credentials_async(connector_id_or_name=, name=, credentials={"bearer_token": ...}, is_default=)
Promote to defaultcreate_or_update_user_credentials_async(connector_id_or_name=, name=, is_default=True)
List user credslist_user_credentials_async(connector_id_or_name=)
Delete user credsdelete_user_credentials_async(connector_id_or_name=, credentials_name=)
Call toolcall_tool_async(connector_id_or_name=, tool_name=, arguments=, credentials_name=)
Scope: workspace*_workspace_credentials*
Scope: organization*_organization_credentials*

Troubleshooting

Credentials don't take effect when calling a tool

  • Verify the credentials was saved: list_user_credentials.
  • Check that credentials_name matches the stored name exactly (case-sensitive).
  • If credentials_name is omitted, the default credentials is used — confirm which is default with list_user_credentials.

OAuth2 token not stored after completing browser flow

  • Make sure you pressed Enter after completing the consent in the browser, not before.
  • If the auth URL expired, call get_auth_url again to get a fresh one.

401 Unauthorized from the MCP server (GitHub)

  • The PAT may have expired. Re-run create_or_update_user_credentials with the same name to rotate it in place.
  • The PAT may lack the required scopes.

Cannot delete the default credentials

  • Promote another credentials to default first: create_or_update_user_credentials(..., is_default=True), then delete the old one.

403 Forbidden on credentials management endpoints

  • Organization-level credentials require ModifyConnector organization permission.
  • Workspace-level credentials require ModifyConnector workspace permission.
  • User-level credentials only require authentication.

Error Codes Reference

HTTP StatusWhen it occursWhat to do
400 Bad RequestEmpty credentials object, or is_default: false on the only existing credentialsProvide bearer_token; always keep one credentials as default
401 UnauthorizedInvalid Mistral API key, or the MCP server rejected the stored tokenCheck your MISTRAL_API_KEY; rotate the credentials
403 ForbiddenInsufficient permissions for the chosen scopeUse a lower scope or request the ModifyConnector permission
404 Not FoundCredentials name or connector does not existVerify names with list_user_credentials
409 ConflictConnector name already taken, or deleting the active defaultRename the connector or promote a different credentials to default first
422 Unprocessable EntityInvalid credentials name formatUse alphanumeric characters and hyphens only