+
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
701628b
chore: remove specific Python version classifiers from pyproject.toml
aschlean Sep 25, 2025
ffccda7
chore: bump version to 0.2.11 in __init__.py and pyproject.toml, upda…
aschlean Sep 25, 2025
32b50e4
feat: implement readiness and health check generation in CodeGenerator
aschlean Sep 25, 2025
0e61956
fix: update health check configuration to indicate deprecation in con…
aschlean Sep 25, 2025
bb115f8
test: add integration tests for readiness and health check endpoints
aschlean Sep 25, 2025
2f78b1b
test: add unit tests for readiness and health check generation functi…
aschlean Sep 25, 2025
5a05494
test: remove integration and unit tests for readiness and health chec…
aschlean Sep 25, 2025
ab48456
test: remove integration tests for health check functionality
aschlean Sep 25, 2025
3cf871f
test: add unit tests for default and custom readiness and health endp…
aschlean Sep 25, 2025
3269b36
test: update assertions for readiness and health endpoint checks in t…
aschlean Sep 25, 2025
7c806d2
test: refactor assertions for startup script checks
aschlean Sep 25, 2025
4fc095d
style: clean up whitespace and formatting in builder.py
aschlean Sep 25, 2025
d557ff1
style: improve formatting and consistency in JSONResponse handling in…
aschlean Sep 25, 2025
d53c3fe
refactor: update health and readiness check generation logic in build…
aschlean Sep 25, 2025
8aea61a
test: enhance readiness endpoint test to include health checks config…
aschlean Sep 25, 2025
a1fe125
style: clean up whitespace and formatting in builder.py
aschlean Sep 25, 2025
62fe1a7
refactor: streamline health and readiness check generation by using d…
aschlean Sep 25, 2025
198d117
style: remove unnecessary whitespace in builder.py
aschlean Sep 25, 2025
911d2ea
refactor: update readiness and health endpoint tests to use direct fu…
aschlean Sep 25, 2025
fdf9fd5
style: remove unnecessary whitespace in test_builder.py
aschlean Sep 25, 2025
147835a
test: update assertions in test_builder.py for Request import and JSO…
aschlean Sep 25, 2025
534a5ad
refactor: enhance readiness and health check endpoints to return JSON…
aschlean Sep 26, 2025
2ffc186
test: update assertions in test_builder.py to verify JSONResponse han…
aschlean Sep 26, 2025
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
1 change: 1 addition & 0 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 0.2.11 | :white_check_mark: |
| 0.2.10 | :white_check_mark: |
| 0.2.9 | :white_check_mark: |
| 0.2.8 | :white_check_mark: |
Expand Down
7 changes: 2 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "golf-mcp"
version = "0.2.10"
version = "0.2.11"
description = "Framework for building MCP servers"
authors = [
{name = "Antoni Gmitruk", email = "antoni@golf.dev"}
Expand Down Expand Up @@ -66,7 +66,7 @@ golf = ["examples/**/*"]

[tool.poetry]
name = "golf-mcp"
version = "0.2.10"
version = "0.2.11"
description = "Framework for building MCP servers with zero boilerplate"
authors = ["Antoni Gmitruk <antoni@golf.dev>"]
license = "Apache-2.0"
Expand All @@ -79,9 +79,6 @@ classifiers = [
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12"
]
Expand Down
2 changes: 1 addition & 1 deletion src/golf/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.2.10"
__version__ = "0.2.11"

# Import endpoints with fallback for dev mode
try:
Expand Down
198 changes: 177 additions & 21 deletions src/golf/core/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,137 @@ def _generate_startup_section(self, project_path: Path) -> list[str]:
"",
]

def _generate_readiness_section(self, project_path: Path) -> list[str]:
"""Generate code section for readiness.py execution during server runtime."""
readiness_path = project_path / "readiness.py"

if not readiness_path.exists():
# Only generate default readiness if health checks are explicitly enabled
if not self.settings.health_check_enabled:
return []
return [
"# Default readiness check - no custom readiness.py found",
"@mcp.custom_route('/ready', methods=[\"GET\"])",
"async def readiness_check(request: Request) -> JSONResponse:",
' """Readiness check endpoint for Kubernetes and load balancers."""',
' return JSONResponse({"status": "pass"}, status_code=200)',
"",
]

return [
"# Custom readiness check from readiness.py",
"from readiness import check as readiness_check_func",
"@mcp.custom_route('/ready', methods=[\"GET\"])",
"async def readiness_check(request: Request):",
' """Readiness check endpoint for Kubernetes and load balancers."""',
" result = readiness_check_func()",
" if isinstance(result, dict):",
" return JSONResponse(result)",
" return result",
"",
]

def _generate_health_section(self, project_path: Path) -> list[str]:
"""Generate code section for health.py execution during server runtime."""
health_path = project_path / "health.py"

if not health_path.exists():
# Check if legacy health configuration is used
if self.settings.health_check_enabled:
return [
"# Legacy health check configuration (deprecated)",
"@mcp.custom_route('" + self.settings.health_check_path + '\', methods=["GET"])',
"async def health_check(request: Request) -> PlainTextResponse:",
' """Health check endpoint for Kubernetes and load balancers."""',
f' return PlainTextResponse("{self.settings.health_check_response}")',
"",
]
else:
# If health checks are disabled, return empty (no default health check)
return []

return [
"# Custom health check from health.py",
"from health import check as health_check_func",
"@mcp.custom_route('/health', methods=[\"GET\"])",
"async def health_check(request: Request):",
' """Health check endpoint for Kubernetes and load balancers."""',
" result = health_check_func()",
" if isinstance(result, dict):",
" return JSONResponse(result)",
" return result",
"",
]

def _generate_check_function_helper(self) -> list[str]:
"""Generate helper function to call custom check functions."""
return [
"# Helper function to call custom check functions",
"async def _call_check_function(check_type: str) -> JSONResponse:",
' """Call custom check function and handle errors gracefully."""',
" import importlib.util",
" import traceback",
" from pathlib import Path",
" from datetime import datetime",
" ",
" try:",
" # Load the custom check module",
" module_path = Path(__file__).parent / f'{check_type}.py'",
" if not module_path.exists():",
' return JSONResponse({"status": "pass"}, status_code=200)',
" ",
" spec = importlib.util.spec_from_file_location(f'{check_type}_check', module_path)",
" if spec and spec.loader:",
" module = importlib.util.module_from_spec(spec)",
" spec.loader.exec_module(module)",
" ",
" # Call the check function if it exists",
" if hasattr(module, 'check'):",
" result = module.check()",
" ",
" # Handle different return types",
" if isinstance(result, dict):",
" # User returned structured response",
" status_code = result.get('status_code', 200)",
" response_data = {k: v for k, v in result.items() if k != 'status_code'}",
" elif isinstance(result, bool):",
" # User returned simple boolean",
" status_code = 200 if result else 503",
" response_data = {",
' "status": "pass" if result else "fail",',
' "timestamp": datetime.utcnow().isoformat()',
" }",
" elif result is None:",
" # User returned nothing - assume success",
" status_code = 200",
' response_data = {"status": "pass"}',
" else:",
" # User returned something else - treat as success message",
" status_code = 200",
" response_data = {",
' "status": "pass",',
' "message": str(result)',
" }",
" ",
" return JSONResponse(response_data, status_code=status_code)",
" else:",
" return JSONResponse(",
' {"status": "fail", "error": f"No check() function found in {check_type}.py"},',
" status_code=503",
" )",
" ",
" except Exception as e:",
" # Log error and return failure response",
" import sys",
' print(f"Error calling {check_type} check function: {e}", file=sys.stderr)',
" print(traceback.format_exc(), file=sys.stderr)",
" return JSONResponse({",
' "status": "fail",',
' "error": f"Error calling {check_type} check function: {str(e)}"',
" }, status_code=503)",
"",
]

def _generate_server(self) -> None:
"""Generate the main server entry point."""
server_file = self.output_dir / "server.py"
Expand Down Expand Up @@ -700,14 +831,31 @@ def _generate_server(self) -> None:
imports.extend(generate_metrics_instrumentation())
imports.extend(generate_session_tracking())

# Add health check imports if enabled
if self.settings.health_check_enabled:
imports.extend(
[
"from starlette.requests import Request",
"from starlette.responses import PlainTextResponse",
]
)
# Add health check imports only when we generate default endpoints
readiness_exists = (self.project_path / "readiness.py").exists()
health_exists = (self.project_path / "health.py").exists()

# Only import starlette when we generate default endpoints (not when custom files exist)
will_generate_default_readiness = not readiness_exists and self.settings.health_check_enabled
will_generate_default_health = not health_exists and self.settings.health_check_enabled

if will_generate_default_readiness or will_generate_default_health:
imports.append("from starlette.requests import Request")

# Determine response types needed for default endpoints
response_types = []
if will_generate_default_readiness:
response_types.append("JSONResponse")
if will_generate_default_health:
response_types.append("PlainTextResponse")

if response_types:
imports.append(f"from starlette.responses import {', '.join(response_types)}")

# Import Request and JSONResponse for custom check routes (they need both)
elif readiness_exists or health_exists:
imports.append("from starlette.requests import Request")
imports.append("from starlette.responses import JSONResponse")

# Get transport-specific configuration
transport_config = self._get_transport_config(self.settings.transport)
Expand Down Expand Up @@ -1199,22 +1347,17 @@ def _generate_server(self) -> None:

metrics_route_code = generate_metrics_route(self.settings.metrics_path)

# Add health check route if enabled
health_check_code = []
if self.settings.health_check_enabled:
health_check_code = [
"# Add health check route",
"@mcp.custom_route('" + self.settings.health_check_path + '\', methods=["GET"])',
"async def health_check(request: Request) -> PlainTextResponse:",
' """Health check endpoint for Kubernetes and load balancers."""',
(f' return PlainTextResponse("{self.settings.health_check_response}")'),
"",
]
# Generate readiness and health check sections
readiness_section = self._generate_readiness_section(self.project_path)
health_section = self._generate_health_section(self.project_path)

# No longer need the check helper function since we use direct imports
check_helper_section = []

# Combine all sections
# Order: imports, env_section, startup_section, auth_setup, server_code (mcp init),
# early_telemetry_init, early_metrics_init, component_registrations,
# metrics_route_code, health_check_code, main_code (run block)
# metrics_route_code, check_helper_section, readiness_section, health_section, main_code (run block)
code = "\n".join(
imports
+ env_section
Expand All @@ -1225,7 +1368,9 @@ def _generate_server(self) -> None:
+ early_metrics_init
+ component_registrations
+ metrics_route_code
+ health_check_code
+ check_helper_section
+ readiness_section
+ health_section
+ main_code
)

Expand Down Expand Up @@ -1427,6 +1572,17 @@ def build_project(
shutil.copy2(startup_path, dest_path)
console.print(get_status_text("success", "Startup script copied to build directory"))

# Copy optional check files to build directory
readiness_path = project_path / "readiness.py"
if readiness_path.exists():
shutil.copy2(readiness_path, output_dir)
console.print(get_status_text("success", "Readiness script copied to build directory"))

health_path = project_path / "health.py"
if health_path.exists():
shutil.copy2(health_path, output_dir)
console.print(get_status_text("success", "Health script copied to build directory"))

# Platform registration (only for prod builds)
if build_env == "prod":
console.print()
Expand Down
4 changes: 2 additions & 2 deletions src/golf/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ class Settings(BaseSettings):
)

# Health check configuration
health_check_enabled: bool = Field(False, description="Enable health check endpoint")
health_check_enabled: bool = Field(False, description="Enable health check endpoint (deprecated - use health.py)")
health_check_path: str = Field("/health", description="Health check endpoint path")
health_check_response: str = Field("OK", description="Health check response text")
health_check_response: str = Field("OK", description="Health check response text (deprecated - use health.py)")

# HTTP session behaviour
stateless_http: bool = Field(
Expand Down
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载