Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions plane_mcp/auth/plane_header_auth_provider.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import os
import time

import httpx
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
from fastmcp.utilities.logging import get_logger

logger = get_logger(__name__)

DEFAULT_PLANE_BASE_URL = "https://api.plane.so"


class PlaneHeaderAuthProvider(TokenVerifier):
def __init__(self, required_scopes: list[str] | None = None):
def __init__(self, required_scopes: list[str] | None = None, timeout_seconds: int = 10):
super().__init__(required_scopes=required_scopes)
self.timeout_seconds = timeout_seconds
self.plane_base_url = (
os.getenv("PLANE_INTERNAL_BASE_URL") or os.getenv("PLANE_BASE_URL", DEFAULT_PLANE_BASE_URL)
).rstrip("/")

async def verify_token(self, token: str) -> AccessToken | None:
try:
Expand All @@ -20,7 +28,21 @@ async def verify_token(self, token: str) -> AccessToken | None:
if token:
workspace_slug = headers.get("x-workspace-slug")
if workspace_slug:
logger.info("Using API key from HTTP headers")
logger.info("Verifying API key against Plane API")
user_url = f"{self.plane_base_url}/api/v1/users/me/"
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
response = await client.get(
user_url,
headers={
"x-api-key": token,
"Content-Type": "application/json",
},
)
if response.status_code != 200:
logger.warning("API key verification failed: %s", response.status_code)
return None

logger.info("API key verified successfully")
expires_at = int(time.time() + 3600)
return AccessToken(
token=token,
Expand All @@ -34,6 +56,9 @@ async def verify_token(self, token: str) -> AccessToken | None:
)
else:
logger.warning("x-api-key header found but x-workspace-slug is missing")
except httpx.RequestError as e:
logger.warning("API key verification request failed: %s", e)
return None
except RuntimeError:
# No active HTTP request available (e.g., stdio transport)
logger.debug("No active HTTP request available for header check")