GolfMCP provides two authentication mechanisms to secure your MCP servers:

  1. OAuth 2.0 - For user-based authentication with external identity providers
  2. API Key - For simple pass-through authentication to upstream APIs

API key authentication

Overview

Golf provides a simple API key pass-through authentication mechanism that allows MCP servers to extract API keys from request headers and forward them to upstream services. The actual authentication happens at the destination API level, not within the MCP server.

Configuration

Configure API key extraction in your project’s pre_build.py:

from golf.auth import configure_api_key

# Configure which header contains the API key
configure_api_key(
    header_name="Authorization",  # Header to extract from
    header_prefix="Bearer "       # Optional prefix to strip
)

How it works

  1. Middleware: Golf automatically adds middleware that extracts the API key from incoming request headers
  2. Context Storage: The extracted key is stored in a request-scoped context (only exists for the duration of the request)
  3. Tool Access: Tools retrieve the API key using get_api_key()
  4. Pass-through: Tools forward the key to upstream APIs in whatever format they require

Using in tools

Tools access the API key with a simple import:

from golf.auth import get_api_key

async def my_tool():
    api_key = get_api_key()
    if not api_key:
        return {"error": "No API key provided"}
    
    # Use the key with your API
    response = await client.get(
        "https://api.example.com/endpoint",
        headers={"Authorization": f"Bearer {api_key}"}
    )

Complete example: GitHub API tool

Here’s a complete example of a GitHub tool using API key authentication:

pre_build.py:

from golf.auth import configure_api_key

# GitHub expects: Authorization: Bearer ghp_xxxxxxxxxxxx
configure_api_key(
    header_name="Authorization",
    header_prefix="Bearer "
)

tools/repos/list.py:

"""List GitHub repositories for a user."""

from typing import Annotated, Optional, Dict, Any, List
from pydantic import BaseModel, Field
import httpx

from golf.auth import get_api_key


class Output(BaseModel):
    """List of repositories."""
    repositories: List[Dict[str, Any]]
    error: Optional[str] = None


async def list(
    username: Annotated[str, Field(description="GitHub username")]
) -> Output:
    """List public repositories for a GitHub user."""
    # Get the API key from the request context
    github_token = get_api_key()
    
    # Prepare headers - include token if available
    headers = {
        "Accept": "application/vnd.github.v3+json",
        "User-Agent": "My-MCP-Server"
    }
    if github_token:
        headers["Authorization"] = f"Bearer {github_token}"
    
    try:
        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"https://api.github.com/users/{username}/repos",
                headers=headers,
                timeout=10.0
            )
            
            if response.status_code == 401:
                return Output(
                    repositories=[], 
                    error="Invalid GitHub token"
                )
            
            response.raise_for_status()
            repos = response.json()
            
            return Output(
                repositories=[{
                    "name": r["name"],
                    "stars": r["stargazers_count"]
                } for r in repos]
            )
    except Exception as e:
        return Output(repositories=[], error=str(e))


export = list

Common configuration patterns

X-API-Key Header (many APIs):

configure_api_key(header_name="X-API-Key")

Authorization with Different Prefixes:

# OpenAI style
configure_api_key(
    header_name="Authorization",
    header_prefix="Bearer "  # Strips "Bearer sk-..." → "sk-..."
)

# No prefix
configure_api_key(
    header_name="X-Custom-Token",
    header_prefix=""  # Token used as-is
)

OAuth 2.0 authentication

GolfMCP provides a high-level abstraction for OAuth 2.0 integration, allowing you to configure authentication without manually implementing the MCP SDK’s OAuthAuthorizationServerProvider interface.

Core concepts

  • ProviderConfig (golf.auth.provider.ProviderConfig): A Pydantic model used to define the settings for an external OAuth 2.0 identity provider (IdP). Key fields include:

    • provider: A string identifying the provider (e.g., "github", "google", or "custom").
    • client_id_env_var: The name of the environment variable holding the OAuth Client ID.
    • client_secret_env_var: The name of the environment variable holding the OAuth Client Secret.
    • jwt_secret_env_var: The name of the environment variable holding the secret key for signing JWTs issued by your GolfMCP server.
    • authorize_url: The IdP’s authorization endpoint URL.
    • token_url: The IdP’s token endpoint URL.
    • userinfo_url (Optional): The IdP’s userinfo endpoint URL.
    • scopes: A list of scopes to request from the IdP (e.g., ["read:user", "openid"]).
    • issuer_url: The publicly accessible base URL of your GolfMCP server. This is crucial as it’s used as the iss (issuer) claim in JWTs your server generates and for constructing callback URLs.
    • callback_path: The path on your GolfMCP server where the IdP should redirect after authentication (e.g., "/auth/callback").
    • token_expiration: The lifetime (in seconds) for JWTs issued by your GolfMCP server.
  • configure_auth (golf.auth.configure_auth): A function, typically called in pre_build.py, to register the authentication provider configuration with GolfMCP.

    from golf.auth import ProviderConfig, configure_auth
    
    my_provider_config = ProviderConfig(...)
    configure_auth(
        provider_config=my_provider_config,
        required_scopes=["scope1", "scope2"] # Optional: Global scopes required for any token
    )
    

    This function stores the provider_config and required_scopes globally, which the build process then uses.

  • GolfOAuthProvider (golf.auth.oauth.GolfOAuthProvider): The heart of Golf’s auth abstraction. This class implements the MCP SDK’s OAuthAuthorizationServerProvider interface. It’s initialized with the ProviderConfig you define.

    • OAuth Flow Management: It handles the Authorization Code Grant flow:
      • Redirecting users to the IdP’s authorize_url.
      • Exchanging the authorization code (received at callback_path) for tokens at the IdP’s token_url.
      • Fetching user information (if userinfo_url is provided).
    • JWT Issuance: After successful authentication with the IdP, GolfOAuthProvider mints its own JWT. This JWT represents the user’s session with your GolfMCP server.
      • The JWT is signed using the jwt_secret (loaded from jwt_secret_env_var).
      • It includes standard claims like iss (your server’s issuer_url), sub (subject, often client ID), exp (expiration), iat (issued at), and scp (scopes).
    • Token Storage (TokenStorage in golf.auth.oauth):
      • A simple in-memory dictionary-based storage for:
        • Authorization codes (temporary).
        • Refresh tokens.
        • Access tokens (primarily for JWT verification state, though JWTs are self-contained).
        • Mappings between your server’s access tokens and the IdP’s access tokens (e.g., to store the GitHub token).
      • Note: This in-memory storage is suitable for development and simple use cases. For production, a persistent and more robust token storage solution (e.g., Redis, database) would be necessary. (This is part of the roadmap).
  • Callback Handling (create_callback_handler in golf.auth.oauth): This function takes a GolfOAuthProvider instance and returns an async HTTP request handler. The build process uses this to generate the route for your callback_path (e.g., /auth/callback) in the dist/server.py. This handler processes the redirect from the IdP.

How secrets are handled

A key design principle is that sensitive secrets (Client ID, Client Secret, JWT Secret) are not hardcoded or stored directly in your project files or the built distributable.

  1. You define environment variable names in ProviderConfig (e.g., client_id_env_var="MY_APP_CLIENT_ID").
  2. During the golf build process, the CodeGenerator (specifically _generate_server and the auth-related code in builder_auth.py) writes Python code into dist/server.py.
  3. This generated code in dist/server.py includes lines like runtime_client_id = os.environ.get("MY_APP_CLIENT_ID").
  4. When you run golf run (or execute python dist/server.py), the server, at startup, reads the actual secret values from the environment variables of the system it’s running on.
  5. These runtime-loaded secrets are then used to initialize the GolfOAuthProvider instance.

This ensures that your dist/ directory can be safely distributed or committed (if needed, though typically dist/ is in .gitignore) without exposing secrets. Secrets must be present in the environment where the server runs.

Accessing provider tokens (e.g., GitHub token)

If your tools need to make API calls to the external provider on behalf of the user (e.g., call the GitHub API), you can retrieve the IdP’s access token that GolfMCP stored during the OAuth flow.

# tools/my_github_tool.py
"""A tool that interacts with the GitHub API."""
from golf.auth.helpers import get_provider_token # Helper to get the IdP token
import httpx
from pydantic import BaseModel # Assuming Output model needs pydantic

class Output(BaseModel): # Define BaseModel for schema generation
    # ... define output fields ...
    data: dict = {} # Example field
    error: str = None # Example error field


async def get_my_repos() -> Output:
    github_token = get_provider_token() # Retrieves the GitHub token for the current user

    if not github_token:
        return Output(error="User not authenticated with GitHub or token unavailable.")

    headers = {
        "Authorization": f"Bearer {github_token}",
        "Accept": "application/vnd.github.v3+json"
    }
    async with httpx.AsyncClient() as client:
        response = await client.get("https://api.github.com/user/repos", headers=headers)
        if response.status_code == 200:
            return Output(data=response.json())
        else:
            return Output(error=f"GitHub API error: {response.status_code}")

export = get_my_repos

The get_provider_token() function (from golf.auth.helpers) looks up the mapping between the current GolfMCP session’s access token and the stored external provider’s access token.