Coverage for /Users/antonigmitruk/golf/src/golf/core/platform.py: 0%

1"""Platform registration for Golf MCP projects."""

2

3 import os

4 from datetime import datetime

5 from pathlib import Path

6 from typing import Any

7

8 import httpx

9 from rich.console import Console

10

11 from golf import __version__

12 from golf.core.config import Settings

13 from golf.core.parser import ComponentType, ParsedComponent

14

15# Import endpoints with fallback for dev mode

16 try:

17 # In built wheels, this exists (generated from _endpoints.py.in)

18 from golf import _endpoints # type: ignore

19 except ImportError:

20 # In editable/dev installs, fall back to env-based values

21 from golf import _endpoints_fallback as _endpoints # type: ignore

22

23 console = Console()

24

25

26 async def register_project_with_platform(

27 project_path: Path,

28 settings: Settings,

29 components: dict[ComponentType, list[ParsedComponent]],

30) -> bool:

31 """Register project with Golf platform during prod build.

32

33 Args:

34 project_path: Path to the project root

35 settings: Project settings

36 components: Parsed components dictionary

37

38 Returns:

39 True if registration succeeded or was skipped, False if failed

40 """

41 # Check if platform integration is enabled

42 api_key = os.getenv("GOLF_API_KEY")

43 if not api_key:

44 return True # Skip silently if no API key

45

46 # Require explicit server ID

47 server_id = os.getenv("GOLF_SERVER_ID")

48 if not server_id:

49 console.print(

50 "[yellow]Warning: Platform registration skipped - GOLF_SERVER_ID environment variable required[/yellow]"

51 )

52 return True # Skip registration but don't fail build

53

54 # Build metadata payload

55 metadata = {

56 "project_name": settings.name,

57 "description": settings.description,

58 "server_id": server_id,

59 "components": _build_component_list(components, project_path),

60 "build_timestamp": datetime.utcnow().isoformat(),

61 "golf_version": __version__,

62 "component_counts": _get_component_counts(components),

63 "server_config": {

64 "host": settings.host,

65 "port": settings.port,

66 "transport": settings.transport,

67 "auth_enabled": bool(settings.auth),

68 "telemetry_enabled": settings.opentelemetry_enabled,

69 "health_check_enabled": settings.health_check_enabled,

70 },

71 }

72

73 try:

74 async with httpx.AsyncClient(timeout=10.0) as client:

75 response = await client.post(

76 _endpoints.PLATFORM_API_URL,

77 json=metadata,

78 headers={

79 "X-Golf-Key": api_key,

80 "Content-Type": "application/json",

81 "User-Agent": f"Golf-MCP/{__version__}",

82 },

83 )

84 response.raise_for_status()

85

86 console.print("[green]✓[/green] Registered with Golf platform")

87 return True

88

89 except httpx.TimeoutException:

90 console.print("[yellow]Warning: Platform registration timed out[/yellow]")

91 return False

92 except httpx.HTTPStatusError as e:

93 if e.response.status_code == 401:

94 console.print("[yellow]Warning: Platform registration failed - invalid API key[/yellow]")

95 elif e.response.status_code == 403:

96 console.print("[yellow]Warning: Platform registration failed - access denied[/yellow]")

97 else:

98 console.print(f"[yellow]Warning: Platform registration failed - HTTP {e.response.status_code}[/yellow]")

99 return False

100 except Exception as e:

101 console.print(f"[yellow]Warning: Platform registration failed: {e}[/yellow]")

102 return False # Don't fail the build

103

104

105 def _build_component_list(

106 components: dict[ComponentType, list[ParsedComponent]],

107 project_path: Path,

108) -> list[dict[str, Any]]:

109 """Convert parsed components to platform format.

110

111 Args:

112 components: Dictionary of parsed components by type

113 project_path: Path to the project root

114

115 Returns:

116 List of component metadata dictionaries

117 """

118 result = []

119

120 for comp_type, comp_list in components.items():

121 for comp in comp_list:

122 # Start with basic component data

123 component_data = {

124 "name": comp.name,

125 "type": comp_type.value,

126 "description": comp.docstring,

127 "entry_function": comp.entry_function,

128 "parent_module": comp.parent_module,

129 }

130

131 # Add file path relative to project root if available

132 if comp.file_path:

133 try:

134 file_path = Path(comp.file_path)

135 # Use the provided project_path for relative calculation

136 rel_path = file_path.relative_to(project_path)

137 component_data["file_path"] = str(rel_path)

138 except ValueError:

139 # If relative_to fails, try to find a common path or use filename

140 component_data["file_path"] = Path(comp.file_path).name

141

142 # Add schema information only if available and not None

143 if hasattr(comp, "input_schema") and comp.input_schema:

144 component_data["input_schema"] = comp.input_schema

145 if hasattr(comp, "output_schema") and comp.output_schema:

146 component_data["output_schema"] = comp.output_schema

147

148 # Add component-specific fields only if they have values

149 if comp_type == ComponentType.RESOURCE:

150 if hasattr(comp, "uri_template") and comp.uri_template:

151 component_data["uri_template"] = comp.uri_template

152

153 elif comp_type == ComponentType.TOOL:

154 if hasattr(comp, "annotations") and comp.annotations:

155 component_data["annotations"] = comp.annotations

156

157 # Add parameters only if they exist and are not empty

158 if hasattr(comp, "parameters") and comp.parameters:

159 component_data["parameters"] = comp.parameters

160

161 result.append(component_data)

162

163 return result

164

165

166 def _get_component_counts(

167 components: dict[ComponentType, list[ParsedComponent]],

168) -> dict[str, int]:

169 """Get component counts by type.

170

171 Args:

172 components: Dictionary of parsed components by type

173

174 Returns:

175 Dictionary with counts for each component type

176 """

177 return {

178 "tools": len(components.get(ComponentType.TOOL, [])),

179 "resources": len(components.get(ComponentType.RESOURCE, [])),

180 "prompts": len(components.get(ComponentType.PROMPT, [])),

181 "total": sum(len(comp_list) for comp_list in components.values()),

182 }