refactor: convert f-string logging to lazy %-style format (G004)

Sweep all 1676 G004 violations across 112 files, converting
`logger.<level>(f"…{x}…")` to `logger.<level>("…%s…", x)`.

Why: ruff rule G004 was added to pyproject.toml to enforce lazy
%-style logging — defers formatting until the log level is enabled
and lets structured log tooling match the unformatted template.

Conversion preserves rendered output byte-for-byte:
- `{x}` → `%s` + `x`
- `{x!r}` / `{x!s}` / `{x!a}` → `%r` / `%s` / `%a`
- Format specs (`{x:.2f}`, `{x:>10}`) → `%s` + `format(x, 'spec')`
  (printf-style specs aren't 1:1 with Python format specs, so we
  delegate to `format()` to keep identical output)
- Literal `%` → `%%`
- Concatenated f-strings (`f"a {x} " "b"`) flattened
- Trailing kwargs (`exc_info=True`) preserved

Verified:
- `uv run ruff check --select G004` → 0 violations
- `uv run ty check -- nextcloud_mcp_server` → passes
- `uv run pytest tests/unit/` → 1010 passed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-13 01:12:17 +02:00
co-authored by Claude Opus 4.7
parent a4e6125d28
commit 665cb9b1eb
112 changed files with 2534 additions and 1859 deletions
+112 -85
View File
@@ -169,10 +169,10 @@ def initialize_document_processors():
progress_interval=unst_config.get("progress_interval", 10),
)
registry.register(processor, priority=10)
logger.info(f"Registered Unstructured processor: {unst_config['api_url']}")
logger.info("Registered Unstructured processor: %s", unst_config["api_url"])
registered_count += 1
except Exception as e:
logger.warning(f"Failed to register Unstructured processor: {e}")
logger.warning("Failed to register Unstructured processor: %s", e)
# Register Tesseract processor
if "tesseract" in config["processors"]:
@@ -187,10 +187,10 @@ def initialize_document_processors():
default_lang=tess_config["lang"],
)
registry.register(processor, priority=5)
logger.info(f"Registered Tesseract processor: lang={tess_config['lang']}")
logger.info("Registered Tesseract processor: lang=%s", tess_config["lang"])
registered_count += 1
except Exception as e:
logger.warning(f"Failed to register Tesseract processor: {e}")
logger.warning("Failed to register Tesseract processor: %s", e)
# Register PyMuPDF processor (high priority, local, no API required)
if "pymupdf" in config["processors"]:
@@ -206,11 +206,12 @@ def initialize_document_processors():
)
registry.register(processor, priority=15) # Higher than unstructured
logger.info(
f"Registered PyMuPDF processor: extract_images={pymupdf_config.get('extract_images', True)}"
"Registered PyMuPDF processor: extract_images=%s",
pymupdf_config.get("extract_images", True),
)
registered_count += 1
except Exception as e:
logger.warning(f"Failed to register PyMuPDF processor: {e}")
logger.warning("Failed to register PyMuPDF processor: %s", e)
# Register custom processor
if "custom" in config["processors"]:
@@ -229,16 +230,19 @@ def initialize_document_processors():
)
registry.register(processor, priority=1)
logger.info(
f"Registered Custom processor '{custom_config['name']}': {custom_config['api_url']}"
"Registered Custom processor '%s': %s",
custom_config["name"],
custom_config["api_url"],
)
registered_count += 1
except Exception as e:
logger.warning(f"Failed to register Custom processor: {e}")
logger.warning("Failed to register Custom processor: %s", e)
if registered_count > 0:
logger.info(
f"Document processing initialized with {registered_count} processor(s): "
f"{', '.join(registry.list_processors())}"
"Document processing initialized with %s processor(s): %s",
registered_count,
", ".join(registry.list_processors()),
)
else:
logger.warning("Document processing enabled but no processors registered")
@@ -409,10 +413,10 @@ class BasicAuthMiddleware:
"password": password,
}
logger.debug(
f"BasicAuth credentials extracted for user: {username}"
"BasicAuth credentials extracted for user: %s", username
)
except Exception as e:
logger.warning(f"Failed to extract BasicAuth credentials: {e}")
logger.warning("Failed to extract BasicAuth credentials: %s", e)
await self.app(scope, receive, send)
@@ -451,7 +455,7 @@ async def load_oauth_client_credentials(
client_data = await storage.get_oauth_client()
if client_data:
logger.info(
f"Loaded OAuth client from SQLite: {client_data['client_id'][:16]}..."
"Loaded OAuth client from SQLite: %s...", client_data["client_id"][:16]
)
return (client_data["client_id"], client_data["client_secret"])
except ValueError:
@@ -496,7 +500,7 @@ async def load_oauth_client_credentials(
dcr_scopes = f"{dcr_scopes} offline_access"
logger.info("✓ offline_access scope enabled for refresh tokens")
logger.info(f"MCP server DCR scopes (resource server): {dcr_scopes}")
logger.info("MCP server DCR scopes (resource server): %s", dcr_scopes)
# Get token type from environment (Bearer or jwt)
# Note: Must be lowercase "jwt" to match OIDC app's check
@@ -504,7 +508,7 @@ async def load_oauth_client_credentials(
# Special case: "bearer" should remain capitalized for compatibility
if token_type != "jwt":
token_type = "Bearer"
logger.info(f"Requesting token type: {token_type}")
logger.info("Requesting token type: %s", token_type)
# Ensure OAuth client in SQLite storage
storage = RefreshTokenStorage.from_env()
@@ -525,7 +529,7 @@ async def load_oauth_client_credentials(
resource_url=resource_url, # RFC 9728 Protected Resource URL
)
logger.info(f"OAuth client ready: {client_info.client_id[:16]}...")
logger.info("OAuth client ready: %s...", client_info.client_id[:16])
return (client_info.client_id, client_info.client_secret)
# No credentials available
@@ -555,7 +559,8 @@ async def app_lifespan_basic(server: FastMCP) -> AsyncIterator[AppContext]:
is_multi_user = settings.enable_multi_user_basic_auth
logger.info(
f"Starting MCP session in {'multi-user' if is_multi_user else 'single-user'} BasicAuth mode"
"Starting MCP session in %s BasicAuth mode",
"multi-user" if is_multi_user else "single-user",
)
# Only create shared client for single-user mode
@@ -633,7 +638,7 @@ async def setup_oauth_config():
discovery_url = os.getenv(
"OIDC_DISCOVERY_URL", f"{nextcloud_host}/.well-known/openid-configuration"
)
logger.info(f"Performing OIDC discovery: {discovery_url}")
logger.info("Performing OIDC discovery: %s", discovery_url)
# Perform OIDC discovery
async with nextcloud_httpx_client(follow_redirects=True) as client:
@@ -654,12 +659,12 @@ async def setup_oauth_config():
registration_endpoint = discovery.get("registration_endpoint")
logger.info("OIDC endpoints discovered:")
logger.info(f" Issuer: {issuer}")
logger.info(f" Userinfo: {userinfo_uri}")
logger.info(" Issuer: %s", issuer)
logger.info(" Userinfo: %s", userinfo_uri)
if jwks_uri:
logger.info(f" JWKS: {jwks_uri}")
logger.info(" JWKS: %s", jwks_uri)
if introspection_uri:
logger.info(f" Introspection: {introspection_uri}")
logger.info(" Introspection: %s", introspection_uri)
# Auto-detect provider mode based on issuer
# External IdP mode: issuer doesn't match Nextcloud host
@@ -685,7 +690,9 @@ async def setup_oauth_config():
if is_external_idp:
oauth_provider = "external" # Could be Keycloak, Auth0, Okta, etc.
logger.info(
f"✓ Detected external IdP mode (issuer: {issuer} != Nextcloud: {nextcloud_host})"
"✓ Detected external IdP mode (issuer: %s != Nextcloud: %s)",
issuer,
nextcloud_host,
)
logger.info(" Tokens will be validated via Nextcloud user_oidc app")
else:
@@ -716,7 +723,7 @@ async def setup_oauth_config():
"✓ Refresh token storage initialized (offline_access enabled)"
)
except Exception as e:
logger.error(f"Failed to initialize refresh token storage: {e}")
logger.error("Failed to initialize refresh token storage: %s", e)
logger.warning(
"Continuing without refresh token storage - users will need to re-authenticate after token expiration"
)
@@ -726,7 +733,7 @@ async def setup_oauth_config():
client_secret = os.getenv("NEXTCLOUD_OIDC_CLIENT_SECRET")
if client_id and client_secret:
logger.info(f"Using static OIDC client credentials: {client_id}")
logger.info("Using static OIDC client credentials: %s", client_id)
elif registration_endpoint:
logger.info(
"NEXTCLOUD_OIDC_CLIENT_ID not set, attempting Dynamic Client Registration"
@@ -753,13 +760,13 @@ async def setup_oauth_config():
# Warn if resource URIs are not configured (required for ADR-005 compliance)
if not os.getenv("NEXTCLOUD_MCP_SERVER_URL"):
logger.warning(
f"NEXTCLOUD_MCP_SERVER_URL not set, defaulting to: {mcp_server_url}. "
"This should be set explicitly for proper audience validation."
"NEXTCLOUD_MCP_SERVER_URL not set, defaulting to: %s. This should be set explicitly for proper audience validation.",
mcp_server_url,
)
if not os.getenv("NEXTCLOUD_RESOURCE_URI"):
logger.warning(
f"NEXTCLOUD_RESOURCE_URI not set, defaulting to: {nextcloud_resource_uri}. "
"This should be set explicitly for proper audience validation."
"NEXTCLOUD_RESOURCE_URI not set, defaulting to: %s. This should be set explicitly for proper audience validation.",
nextcloud_resource_uri,
)
# Create settings for UnifiedTokenVerifier (use same settings instance from start of function)
@@ -790,8 +797,8 @@ async def setup_oauth_config():
logger.info(
"✓ Multi-audience mode enabled (ADR-005) - tokens must contain both MCP and Nextcloud audiences"
)
logger.info(f" Required MCP audience: {client_id} or {mcp_server_url}")
logger.info(f" Required Nextcloud audience: {nextcloud_resource_uri}")
logger.info(" Required MCP audience: %s or %s", client_id, mcp_server_url)
logger.info(" Required Nextcloud audience: %s", nextcloud_resource_uri)
if introspection_uri:
logger.info("✓ Opaque token introspection enabled (RFC 7662)")
@@ -881,7 +888,8 @@ async def setup_oauth_config_for_multi_user_basic(
f"{nextcloud_host}/.well-known/openid-configuration",
)
logger.info(
f"Performing OIDC discovery for multi-user BasicAuth hybrid mode: {discovery_url}"
"Performing OIDC discovery for multi-user BasicAuth hybrid mode: %s",
discovery_url,
)
# Perform OIDC discovery
@@ -894,20 +902,22 @@ async def setup_oauth_config_for_multi_user_basic(
discovery = response.json()
except httpx.HTTPStatusError as e:
logger.error(
f"OIDC discovery failed: HTTP {e.response.status_code} from {discovery_url}"
"OIDC discovery failed: HTTP %s from %s",
e.response.status_code,
discovery_url,
)
raise ValueError(
f"OIDC discovery failed: HTTP {e.response.status_code} from {discovery_url}. "
"Ensure Nextcloud OIDC (user_oidc app) is installed and configured."
) from e
except httpx.RequestError as e:
logger.error(f"OIDC discovery failed: {e}")
logger.error("OIDC discovery failed: %s", e)
raise ValueError(
f"OIDC discovery failed: Cannot connect to {discovery_url}. Error: {e}"
) from e
except (KeyError, ValueError) as e:
logger.error(
f"OIDC discovery failed: Invalid response from {discovery_url}: {e}"
"OIDC discovery failed: Invalid response from %s: %s", discovery_url, e
)
raise ValueError(
f"OIDC discovery failed: Invalid response from {discovery_url}. "
@@ -923,10 +933,10 @@ async def setup_oauth_config_for_multi_user_basic(
introspection_uri = discovery.get("introspection_endpoint")
logger.info("OIDC endpoints configured for management API:")
logger.info(f" Issuer: {issuer}")
logger.info(f" Userinfo: {userinfo_uri}")
logger.info(f" JWKS: {jwks_uri}")
logger.info(f" Introspection: {introspection_uri}")
logger.info(" Issuer: %s", issuer)
logger.info(" Userinfo: %s", userinfo_uri)
logger.info(" JWKS: %s", jwks_uri)
logger.info(" Introspection: %s", introspection_uri)
# Get MCP server URL for audience validation
mcp_server_url = os.getenv("NEXTCLOUD_MCP_SERVER_URL", "http://localhost:8000")
@@ -982,8 +992,8 @@ async def setup_oauth_config_for_multi_user_basic(
"✓ Refresh token storage initialized for background operations (hybrid mode)"
)
except Exception as e:
logger.error(f"Failed to initialize refresh token storage: {e}")
logger.debug(f"Full traceback:\n{traceback.format_exc()}")
logger.error("Failed to initialize refresh token storage: %s", e)
logger.debug("Full traceback:\\n%s", traceback.format_exc())
logger.warning(
"Continuing without refresh token storage - webhook management may be limited"
)
@@ -1012,8 +1022,8 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
logger.error(error_msg)
raise ValueError(error_msg)
logger.info(f"✅ Configuration validated successfully for {mode.value} mode")
logger.debug(f"Mode details:\n{get_mode_summary(mode)}")
logger.info("✅ Configuration validated successfully for %s mode", mode.value)
logger.debug("Mode details:\\n%s", get_mode_summary(mode))
# Derive helper variables for backward compatibility with existing code.
# `oauth_enabled` is True for the LOGIN_FLOW (formerly OAUTH_SINGLE_AUDIENCE)
@@ -1033,7 +1043,7 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
if settings.metrics_enabled:
setup_metrics(port=settings.metrics_port)
logger.info(
f"Prometheus metrics enabled on dedicated port {settings.metrics_port}"
"Prometheus metrics enabled on dedicated port %s", settings.metrics_port
)
# Setup OpenTelemetry tracing (optional)
@@ -1045,7 +1055,8 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
sampling_rate=settings.otel_traces_sampler_arg,
)
logger.info(
f"OpenTelemetry tracing enabled (endpoint: {settings.otel_exporter_otlp_endpoint})"
"OpenTelemetry tracing enabled (endpoint: %s)",
settings.otel_exporter_otlp_endpoint,
)
else:
logger.info(
@@ -1093,7 +1104,8 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
# This avoids relying on discovery doc which may use public URLs unreachable from containers
registration_endpoint = f"{settings.nextcloud_host}/apps/oidc/register"
logger.info(
f"Attempting Dynamic Client Registration at: {registration_endpoint}"
"Attempting Dynamic Client Registration at: %s",
registration_endpoint,
)
# Perform DCR
@@ -1108,13 +1120,13 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
registration_endpoint=registration_endpoint,
)
logger.info(
f"✓ Dynamic Client Registration successful for background operations "
f"(client_id: {client_id[:16]}...)"
"✓ Dynamic Client Registration successful for background operations (client_id: %s...)",
client_id[:16],
)
return (client_id, client_secret)
except Exception as e:
logger.error(f"Dynamic Client Registration failed: {e}")
logger.debug(f"Full traceback:\n{traceback.format_exc()}")
logger.error("Dynamic Client Registration failed: %s", e)
logger.debug("Full traceback:\\n%s", traceback.format_exc())
logger.warning("Background vector sync will be disabled.")
return None
@@ -1151,8 +1163,8 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
# - httpx.HTTPError: Network issues, OIDC discovery failures
# - ValueError: Missing required configuration (NEXTCLOUD_HOST)
# - KeyError: Missing required fields in OIDC discovery response
logger.error(f"Failed to setup OAuth infrastructure: {e}")
logger.debug(f"Full traceback:\n{traceback.format_exc()}")
logger.error("Failed to setup OAuth infrastructure: %s", e)
logger.debug("Full traceback:\\n%s", traceback.format_exc())
logger.warning(
"Management API will be unavailable. "
"Webhook management from Astrolabe admin UI will not work."
@@ -1163,8 +1175,8 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
except Exception as e:
# Unexpected error - this is a programming error, re-raise it
logger.error(
f"Unexpected error during OAuth infrastructure setup: {e}. "
"This is likely a programming error that should be fixed."
"Unexpected error during OAuth infrastructure setup: %s. This is likely a programming error that should be fixed.",
e,
)
raise
@@ -1191,7 +1203,7 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
Lifespan context for OAuth mode - captures OAuth configuration from outer scope.
"""
logger.info("Starting MCP server in OAuth mode")
logger.info(f"Using OAuth provider: {oauth_provider}")
logger.info("Using OAuth provider: %s", oauth_provider)
if refresh_token_storage:
logger.info("Refresh token storage is available")
if oauth_client:
@@ -1223,7 +1235,7 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
try:
await oauth_client.close()
except Exception as e:
logger.warning(f"Error closing OAuth client: {e}")
logger.warning("Error closing OAuth client: %s", e)
logger.info("MCP server shutdown complete")
mcp = FastMCP(
@@ -1239,7 +1251,7 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
)
else:
# BasicAuth modes (single-user or multi-user)
logger.info(f"Configuring MCP server for {mode.value} mode")
logger.info("Configuring MCP server for %s mode", mode.value)
mcp = FastMCP(
"Nextcloud MCP",
lifespan=app_lifespan_basic,
@@ -1264,11 +1276,13 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
# Configure only the enabled apps
for app_name in enabled_apps:
if app_name in AVAILABLE_APPS:
logger.info(f"Configuring {app_name} tools")
logger.info("Configuring %s tools", app_name)
AVAILABLE_APPS[app_name](mcp)
else:
logger.warning(
f"Unknown app: {app_name}. Available apps: {list(AVAILABLE_APPS.keys())}"
"Unknown app: %s. Available apps: %s",
app_name,
list(AVAILABLE_APPS.keys()),
)
# Register semantic search tools (cross-app feature)
@@ -1304,8 +1318,9 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
user_scopes = get_access_token_scopes()
is_jwt = is_jwt_token()
logger.info(
f"🔍 list_tools called - Token type: {'JWT' if is_jwt else 'opaque/none'}, "
f"User scopes: {user_scopes}"
"🔍 list_tools called - Token type: %s, User scopes: %s",
"JWT" if is_jwt else "opaque/none",
user_scopes,
)
# Get all tools
@@ -1323,14 +1338,17 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
]
token_type = "JWT" if is_jwt else "Bearer"
logger.info(
f"✂️ {token_type} scope filtering: {len(allowed_tools)}/{len(all_tools)} tools "
f"available for scopes: {user_scopes}"
"✂️ %s scope filtering: %s/%s tools available for scopes: %s",
token_type,
len(allowed_tools),
len(all_tools),
user_scopes,
)
else:
# BasicAuth mode or no token - show all tools
allowed_tools = all_tools
logger.info(
f"📋 Showing all {len(all_tools)} tools (no token/BasicAuth)"
"📋 Showing all %s tools (no token/BasicAuth)", len(all_tools)
)
# Return the Tool objects directly (they're already in the correct format)
@@ -1457,7 +1475,8 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
break
logger.info(
f"OAuth context initialized for login routes (client_id={client_id[:16]}...)"
"OAuth context initialized for login routes (client_id=%s...)",
client_id[:16],
)
else:
# BasicAuth mode - initialize storage for webhook management
@@ -1505,7 +1524,8 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
}
app.state.oauth_context = oauth_context_dict
logger.info(
f"✓ OAuth context initialized for management APIs (hybrid mode, client_id={sync_client_id[:16]}...)"
"✓ OAuth context initialized for management APIs (hybrid mode, client_id=%s...)",
sync_client_id[:16],
)
elif multi_user_basic_oauth_creds and multi_user_token_verifier is None:
logger.warning(
@@ -1573,7 +1593,7 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
await get_qdrant_client() # Triggers collection creation if needed
logger.info("Qdrant collection ready")
except Exception as e:
logger.error(f"Failed to initialize Qdrant collection: {e}")
logger.error("Failed to initialize Qdrant collection: %s", e)
raise RuntimeError(
f"Cannot start vector sync - Qdrant initialization failed: {e}"
) from e
@@ -1639,8 +1659,8 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
_vector_sync_state.eviction_task_group = tg
logger.info(
f"Background sync tasks started: 1 scanner + "
f"{settings.vector_sync_processor_workers} processors"
"Background sync tasks started: 1 scanner + %s processors",
settings.vector_sync_processor_workers,
)
# Run MCP session manager and yield
@@ -1664,7 +1684,7 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
# OAuth mode with background operations - multi-user sync
# Also used for multi-user BasicAuth mode (client auth is BasicAuth, background sync uses app passwords or OAuth)
mode_desc = "OAuth mode" if oauth_enabled else "Multi-user BasicAuth mode"
logger.info(f"Starting background vector sync tasks for {mode_desc}")
logger.info("Starting background vector sync tasks for %s", mode_desc)
# Get nextcloud_host (from settings - already validated)
nextcloud_host_for_sync = settings.nextcloud_host
@@ -1735,7 +1755,7 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
await get_qdrant_client() # Triggers collection creation if needed
logger.info("Qdrant collection ready")
except Exception as e:
logger.error(f"Failed to initialize Qdrant collection: {e}")
logger.error("Failed to initialize Qdrant collection: %s", e)
raise RuntimeError(
f"Cannot start vector sync - Qdrant initialization failed: {e}"
) from e
@@ -1748,10 +1768,12 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
)
if removed:
logger.info(
f"Cleaned up {len(removed)} stale app password(s): {removed}"
"Cleaned up %s stale app password(s): %s",
len(removed),
removed,
)
except Exception as e:
logger.warning(f"App password cleanup failed (non-fatal): {e}")
logger.warning("App password cleanup failed (non-fatal): %s", e)
# Initialize shared state
send_stream, receive_stream = anyio.create_memory_object_stream(
@@ -1829,8 +1851,8 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
_vector_sync_state.eviction_task_group = tg
logger.info(
f"Background sync tasks started: 1 user manager + "
f"{settings.vector_sync_processor_workers} processors"
"Background sync tasks started: 1 user manager + %s processors",
settings.vector_sync_processor_workers,
)
# Run MCP session manager and yield
@@ -2137,8 +2159,10 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
if oauth_provisioning_available:
logger.info(
f"OAuth provisioning routes enabled for mode: {mode.value} "
f"(oauth_enabled={oauth_enabled}, hybrid_mode={not oauth_enabled})"
"OAuth provisioning routes enabled for mode: %s (oauth_enabled=%s, hybrid_mode=%s)",
mode.value,
oauth_enabled,
not oauth_enabled,
)
def oauth_protected_resource_metadata(request):
@@ -2330,7 +2354,7 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
browser_routes.append(
Mount("/static", StaticFiles(directory=static_dir), name="static")
)
logger.info(f"Mounted static files from {static_dir}")
logger.info("Mounted static files from %s", static_dir)
browser_app = Starlette(routes=browser_routes)
browser_app.add_middleware(
@@ -2381,13 +2405,14 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
token_preview = (
auth_header[:50] + "..." if len(auth_header) > 50 else auth_header
)
logger.info(f"🔑 /mcp request with Authorization: {token_preview}")
logger.info("🔑 /mcp request with Authorization: %s", token_preview)
else:
# Only warn about missing Authorization in OAuth mode
# In BasicAuth mode, /mcp requests without Authorization are expected
if oauth_enabled:
logger.warning(
f"⚠️ /mcp request WITHOUT Authorization header from {request.client}"
"⚠️ /mcp request WITHOUT Authorization header from %s",
request.client,
)
# Log client capabilities on initialize request
@@ -2404,8 +2429,9 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
client_info = params.get("clientInfo", {})
logger.info(
f"🔌 MCP client connected: {client_info.get('name', 'unknown')} "
f"v{client_info.get('version', 'unknown')}"
"🔌 MCP client connected: %s v%s",
client_info.get("name", "unknown"),
client_info.get("version", "unknown"),
)
# Log capabilities in a structured way
@@ -2421,16 +2447,17 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
)
logger.info(
f"📋 Client capabilities: {', '.join(cap_summary) if cap_summary else 'none'}"
"📋 Client capabilities: %s",
", ".join(cap_summary) if cap_summary else "none",
)
# Log full capabilities at INFO level to diagnose capability issues
logger.info(
f"Full capabilities JSON: {json.dumps(capabilities)}"
"Full capabilities JSON: %s", json.dumps(capabilities)
)
except Exception as e:
# Don't fail the request if logging fails
logger.debug(
f"Failed to parse MCP request for capability logging: {e}"
"Failed to parse MCP request for capability logging: %s", e
)
response = await call_next(request)