GolfMCP provides two authentication mechanisms to secure your MCP servers:
- OAuth 2.0 - For user-based authentication with external identity providers
- 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
- Middleware: Golf automatically adds middleware that extracts the API key from incoming request headers
- Context Storage: The extracted key is stored in a request-scoped context (only exists for the duration of the request)
- Tool Access: Tools retrieve the API key using
get_api_key()
- Pass-through: Tools forward the key to upstream APIs in whatever format they require
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}"}
)
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.
- You define environment variable names in
ProviderConfig
(e.g., client_id_env_var="MY_APP_CLIENT_ID"
).
- 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
.
- This generated code in
dist/server.py
includes lines like runtime_client_id = os.environ.get("MY_APP_CLIENT_ID")
.
- 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.
- 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.
Responses are generated using AI and may contain mistakes.