golf-mcp 0.1.11__py3-none-any.whl → 0.1.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of golf-mcp might be problematic. Click here for more details.

Files changed (48) hide show
  1. golf/__init__.py +1 -1
  2. golf/auth/__init__.py +38 -26
  3. golf/auth/api_key.py +16 -23
  4. golf/auth/helpers.py +68 -54
  5. golf/auth/oauth.py +340 -277
  6. golf/auth/provider.py +58 -53
  7. golf/cli/__init__.py +1 -1
  8. golf/cli/main.py +209 -87
  9. golf/commands/__init__.py +1 -1
  10. golf/commands/build.py +31 -25
  11. golf/commands/init.py +81 -53
  12. golf/commands/run.py +30 -15
  13. golf/core/__init__.py +1 -1
  14. golf/core/builder.py +493 -362
  15. golf/core/builder_auth.py +115 -107
  16. golf/core/builder_telemetry.py +12 -9
  17. golf/core/config.py +62 -46
  18. golf/core/parser.py +174 -136
  19. golf/core/telemetry.py +216 -95
  20. golf/core/transformer.py +53 -55
  21. golf/examples/__init__.py +0 -1
  22. golf/examples/api_key/pre_build.py +2 -2
  23. golf/examples/api_key/tools/issues/create.py +35 -36
  24. golf/examples/api_key/tools/issues/list.py +42 -37
  25. golf/examples/api_key/tools/repos/list.py +50 -29
  26. golf/examples/api_key/tools/search/code.py +50 -37
  27. golf/examples/api_key/tools/users/get.py +21 -20
  28. golf/examples/basic/pre_build.py +4 -4
  29. golf/examples/basic/prompts/welcome.py +6 -7
  30. golf/examples/basic/resources/current_time.py +10 -9
  31. golf/examples/basic/resources/info.py +6 -5
  32. golf/examples/basic/resources/weather/common.py +16 -10
  33. golf/examples/basic/resources/weather/current.py +15 -11
  34. golf/examples/basic/resources/weather/forecast.py +15 -11
  35. golf/examples/basic/tools/github_user.py +19 -21
  36. golf/examples/basic/tools/hello.py +10 -6
  37. golf/examples/basic/tools/payments/charge.py +34 -25
  38. golf/examples/basic/tools/payments/common.py +8 -6
  39. golf/examples/basic/tools/payments/refund.py +29 -25
  40. golf/telemetry/__init__.py +6 -6
  41. golf/telemetry/instrumentation.py +455 -310
  42. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/METADATA +1 -1
  43. golf_mcp-0.1.13.dist-info/RECORD +55 -0
  44. golf_mcp-0.1.11.dist-info/RECORD +0 -55
  45. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/WHEEL +0 -0
  46. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
  47. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
  48. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/top_level.txt +0 -0
golf/core/builder_auth.py CHANGED
@@ -8,9 +8,16 @@ from golf.auth import get_auth_config
8
8
  from golf.auth.api_key import get_api_key_config
9
9
 
10
10
 
11
- def generate_auth_code(server_name: str, host: str = "127.0.0.1", port: int = 3000, https: bool = False, opentelemetry_enabled: bool = False, transport: str = "streamable-http") -> dict:
11
+ def generate_auth_code(
12
+ server_name: str,
13
+ host: str = "127.0.0.1",
14
+ port: int = 3000,
15
+ https: bool = False,
16
+ opentelemetry_enabled: bool = False,
17
+ transport: str = "streamable-http",
18
+ ) -> dict:
12
19
  """Generate authentication components for the FastMCP app.
13
-
20
+
14
21
  Returns a dictionary with:
15
22
  - imports: List of import statements
16
23
  - setup_code: Auth setup code (provider configuration, etc.)
@@ -20,19 +27,16 @@ def generate_auth_code(server_name: str, host: str = "127.0.0.1", port: int = 30
20
27
  # Check for API key configuration first
21
28
  api_key_config = get_api_key_config()
22
29
  if api_key_config:
23
- return generate_api_key_auth_components(server_name, opentelemetry_enabled, transport)
24
-
30
+ return generate_api_key_auth_components(
31
+ server_name, opentelemetry_enabled, transport
32
+ )
33
+
25
34
  # Otherwise check for OAuth configuration
26
35
  original_provider_config, required_scopes_from_config = get_auth_config()
27
-
36
+
28
37
  if not original_provider_config:
29
38
  # If no auth config, return empty components
30
- return {
31
- "imports": [],
32
- "setup_code": [],
33
- "fastmcp_args": {},
34
- "has_auth": False
35
- }
39
+ return {"imports": [], "setup_code": [], "fastmcp_args": {}, "has_auth": False}
36
40
 
37
41
  # Build auth components
38
42
  auth_imports = [
@@ -48,98 +52,107 @@ def generate_auth_code(server_name: str, host: str = "127.0.0.1", port: int = 30
48
52
  setup_code_lines = []
49
53
 
50
54
  # Code to determine runtime server address configuration
51
- setup_code_lines.extend([
52
- "# Determine runtime server address configuration",
53
- f"runtime_host = os.environ.get('HOST', {repr(host)})",
54
- f"runtime_port = int(os.environ.get('PORT', {repr(port)}))",
55
- f"runtime_protocol = 'https' if os.environ.get('HTTPS', {repr(str(https)).lower()}).lower() in ('1', 'true', 'yes') else 'http'",
56
- "",
57
- "# Determine proper issuer URL at runtime for this server instance",
58
- "include_port = (runtime_protocol == 'http' and runtime_port != 80) or (runtime_protocol == 'https' and runtime_port != 443)",
59
- "if include_port:",
60
- " runtime_issuer_url = f'{runtime_protocol}://{runtime_host}:{runtime_port}'",
61
- "else:",
62
- " runtime_issuer_url = f'{runtime_protocol}://{runtime_host}'",
63
- "",
64
- ])
55
+ setup_code_lines.extend(
56
+ [
57
+ "# Determine runtime server address configuration",
58
+ f"runtime_host = os.environ.get('HOST', {repr(host)})",
59
+ f"runtime_port = int(os.environ.get('PORT', {repr(port)}))",
60
+ f"runtime_protocol = 'https' if os.environ.get('HTTPS', {repr(str(https)).lower()}).lower() in ('1', 'true', 'yes') else 'http'",
61
+ "",
62
+ "# Determine proper issuer URL at runtime for this server instance",
63
+ "include_port = (runtime_protocol == 'http' and runtime_port != 80) or (runtime_protocol == 'https' and runtime_port != 443)",
64
+ "if include_port:",
65
+ " runtime_issuer_url = f'{runtime_protocol}://{runtime_host}:{runtime_port}'",
66
+ "else:",
67
+ " runtime_issuer_url = f'{runtime_protocol}://{runtime_host}'",
68
+ "",
69
+ ]
70
+ )
65
71
 
66
72
  # Code to load secrets from environment variables AT RUNTIME in server.py
67
- setup_code_lines.extend([
68
- "# Load secrets from environment variables using names specified in pre_build.py ProviderConfig",
69
- f"runtime_client_id = os.environ.get({repr(original_provider_config.client_id_env_var)})",
70
- f"runtime_client_secret = os.environ.get({repr(original_provider_config.client_secret_env_var)})",
71
- f"runtime_jwt_secret = os.environ.get({repr(original_provider_config.jwt_secret_env_var)})",
72
- "",
73
- "# Check and warn if essential secrets are missing",
74
- "if not runtime_client_id:",
75
- f" print(f\"AUTH WARNING: Environment variable '{original_provider_config.client_id_env_var}' for OAuth Client ID is not set. Authentication will likely fail.\", file=sys.stderr)",
76
- "if not runtime_client_secret:",
77
- f" print(f\"AUTH WARNING: Environment variable '{original_provider_config.client_secret_env_var}' for OAuth Client Secret is not set. Authentication will likely fail.\", file=sys.stderr)",
78
- "if not runtime_jwt_secret:",
79
- f" print(f\"AUTH WARNING: Environment variable '{original_provider_config.jwt_secret_env_var}' for JWT Secret is not set. Using a default insecure fallback. DO NOT USE IN PRODUCTION.\", file=sys.stderr)",
80
- f" runtime_jwt_secret = {repr(f'fallback-dev-jwt-secret-for-{server_name}-!PLEASE-CHANGE-THIS!')}", # Fixed fallback string
81
- "",
82
- ])
73
+ setup_code_lines.extend(
74
+ [
75
+ "# Load secrets from environment variables using names specified in pre_build.py ProviderConfig",
76
+ f"runtime_client_id = os.environ.get({repr(original_provider_config.client_id_env_var)})",
77
+ f"runtime_client_secret = os.environ.get({repr(original_provider_config.client_secret_env_var)})",
78
+ f"runtime_jwt_secret = os.environ.get({repr(original_provider_config.jwt_secret_env_var)})",
79
+ "",
80
+ "# Check and warn if essential secrets are missing",
81
+ "if not runtime_client_id:",
82
+ f" print(f\"AUTH WARNING: Environment variable '{original_provider_config.client_id_env_var}' for OAuth Client ID is not set. Authentication will likely fail.\", file=sys.stderr)",
83
+ "if not runtime_client_secret:",
84
+ f" print(f\"AUTH WARNING: Environment variable '{original_provider_config.client_secret_env_var}' for OAuth Client Secret is not set. Authentication will likely fail.\", file=sys.stderr)",
85
+ "if not runtime_jwt_secret:",
86
+ f" print(f\"AUTH WARNING: Environment variable '{original_provider_config.jwt_secret_env_var}' for JWT Secret is not set. Using a default insecure fallback. DO NOT USE IN PRODUCTION.\", file=sys.stderr)",
87
+ f" runtime_jwt_secret = {repr(f'fallback-dev-jwt-secret-for-{server_name}-!PLEASE-CHANGE-THIS!')}", # Fixed fallback string
88
+ "",
89
+ ]
90
+ )
83
91
 
84
92
  # Code to instantiate ProviderConfig using runtime-loaded secrets and other baked-in non-secrets
85
- setup_code_lines.extend([
86
- "# Instantiate ProviderConfig with runtime-resolved secrets and other pre-configured values",
87
- f"provider_config_instance = GolfProviderConfigInternal(", # Use aliased import
88
- f" provider={repr(original_provider_config.provider)},",
89
- f" client_id_env_var={repr(original_provider_config.client_id_env_var)},",
90
- f" client_secret_env_var={repr(original_provider_config.client_secret_env_var)},",
91
- f" jwt_secret_env_var={repr(original_provider_config.jwt_secret_env_var)},",
92
- f" client_id=runtime_client_id,",
93
- f" client_secret=runtime_client_secret,",
94
- f" jwt_secret=runtime_jwt_secret,",
95
- f" authorize_url={repr(original_provider_config.authorize_url)},",
96
- f" token_url={repr(original_provider_config.token_url)},",
97
- f" userinfo_url={repr(original_provider_config.userinfo_url)},",
98
- f" jwks_uri={repr(original_provider_config.jwks_uri)},",
99
- f" scopes={repr(original_provider_config.scopes)},",
100
- f" issuer_url=runtime_issuer_url,",
101
- f" callback_path={repr(original_provider_config.callback_path)},",
102
- f" token_expiration={original_provider_config.token_expiration}",
103
- ")",
104
- "",
105
- "auth_provider = GolfOAuthProvider(provider_config_instance)",
106
- "from golf.auth.helpers import _set_active_golf_oauth_provider # Ensure helper is imported",
107
- "_set_active_golf_oauth_provider(auth_provider) # Make provider instance available",
108
- "",
109
- ])
93
+ setup_code_lines.extend(
94
+ [
95
+ "# Instantiate ProviderConfig with runtime-resolved secrets and other pre-configured values",
96
+ "provider_config_instance = GolfProviderConfigInternal(", # Use aliased import
97
+ f" provider={repr(original_provider_config.provider)},",
98
+ f" client_id_env_var={repr(original_provider_config.client_id_env_var)},",
99
+ f" client_secret_env_var={repr(original_provider_config.client_secret_env_var)},",
100
+ f" jwt_secret_env_var={repr(original_provider_config.jwt_secret_env_var)},",
101
+ " client_id=runtime_client_id,",
102
+ " client_secret=runtime_client_secret,",
103
+ " jwt_secret=runtime_jwt_secret,",
104
+ f" authorize_url={repr(original_provider_config.authorize_url)},",
105
+ f" token_url={repr(original_provider_config.token_url)},",
106
+ f" userinfo_url={repr(original_provider_config.userinfo_url)},",
107
+ f" jwks_uri={repr(original_provider_config.jwks_uri)},",
108
+ f" scopes={repr(original_provider_config.scopes)},",
109
+ " issuer_url=runtime_issuer_url,",
110
+ f" callback_path={repr(original_provider_config.callback_path)},",
111
+ f" token_expiration={original_provider_config.token_expiration}",
112
+ ")",
113
+ "",
114
+ "auth_provider = GolfOAuthProvider(provider_config_instance)",
115
+ "from golf.auth.helpers import _set_active_golf_oauth_provider # Ensure helper is imported",
116
+ "_set_active_golf_oauth_provider(auth_provider) # Make provider instance available",
117
+ "",
118
+ ]
119
+ )
110
120
 
111
121
  # AuthSettings creation
112
- setup_code_lines.extend([
113
- "# Create auth settings for FastMCP",
114
- "auth_settings = AuthSettings(",
115
- " issuer_url=runtime_issuer_url,",
116
- " client_registration_options=ClientRegistrationOptions(",
117
- " enabled=True,",
118
- f" valid_scopes={repr(original_provider_config.scopes)},",
119
- f" default_scopes={repr(original_provider_config.scopes)}",
120
- " ),",
121
- f" required_scopes={repr(required_scopes_from_config) if required_scopes_from_config else None}",
122
- ")",
123
- "",
124
- ])
122
+ setup_code_lines.extend(
123
+ [
124
+ "# Create auth settings for FastMCP",
125
+ "auth_settings = AuthSettings(",
126
+ " issuer_url=runtime_issuer_url,",
127
+ " client_registration_options=ClientRegistrationOptions(",
128
+ " enabled=True,",
129
+ f" valid_scopes={repr(original_provider_config.scopes)},",
130
+ f" default_scopes={repr(original_provider_config.scopes)}",
131
+ " ),",
132
+ f" required_scopes={repr(required_scopes_from_config) if required_scopes_from_config else None}",
133
+ ")",
134
+ "",
135
+ ]
136
+ )
125
137
 
126
138
  # FastMCP constructor arguments
127
- fastmcp_args = {
128
- "auth_server_provider": "auth_provider",
129
- "auth": "auth_settings"
130
- }
139
+ fastmcp_args = {"auth_server_provider": "auth_provider", "auth": "auth_settings"}
131
140
 
132
141
  return {
133
142
  "imports": auth_imports,
134
143
  "setup_code": setup_code_lines,
135
144
  "fastmcp_args": fastmcp_args,
136
- "has_auth": True
145
+ "has_auth": True,
137
146
  }
138
147
 
139
148
 
140
- def generate_api_key_auth_components(server_name: str, opentelemetry_enabled: bool = False, transport: str = "streamable-http") -> dict:
149
+ def generate_api_key_auth_components(
150
+ server_name: str,
151
+ opentelemetry_enabled: bool = False,
152
+ transport: str = "streamable-http",
153
+ ) -> dict:
141
154
  """Generate authentication components for API key authentication.
142
-
155
+
143
156
  Returns a dictionary with:
144
157
  - imports: List of import statements
145
158
  - setup_code: Auth setup code (middleware setup)
@@ -148,13 +161,8 @@ def generate_api_key_auth_components(server_name: str, opentelemetry_enabled: bo
148
161
  """
149
162
  api_key_config = get_api_key_config()
150
163
  if not api_key_config:
151
- return {
152
- "imports": [],
153
- "setup_code": [],
154
- "fastmcp_args": {},
155
- "has_auth": False
156
- }
157
-
164
+ return {"imports": [], "setup_code": [], "fastmcp_args": {}, "has_auth": False}
165
+
158
166
  auth_imports = [
159
167
  "# API key authentication setup",
160
168
  "from golf.auth.api_key import get_api_key_config, configure_api_key",
@@ -164,14 +172,14 @@ def generate_api_key_auth_components(server_name: str, opentelemetry_enabled: bo
164
172
  "from starlette.responses import JSONResponse",
165
173
  "import os",
166
174
  ]
167
-
175
+
168
176
  setup_code_lines = [
169
177
  "# Recreate API key configuration from pre_build.py",
170
- f"configure_api_key(",
178
+ "configure_api_key(",
171
179
  f" header_name={repr(api_key_config.header_name)},",
172
180
  f" header_prefix={repr(api_key_config.header_prefix)},",
173
181
  f" required={repr(api_key_config.required)}",
174
- f")",
182
+ ")",
175
183
  "",
176
184
  "# Simplified API key middleware that validates presence",
177
185
  "class ApiKeyMiddleware(BaseHTTPMiddleware):",
@@ -217,15 +225,15 @@ def generate_api_key_auth_components(server_name: str, opentelemetry_enabled: bo
217
225
  " return await call_next(request)",
218
226
  "",
219
227
  ]
220
-
228
+
221
229
  # API key auth is handled via middleware, not FastMCP constructor args
222
230
  fastmcp_args = {}
223
-
231
+
224
232
  return {
225
233
  "imports": auth_imports,
226
234
  "setup_code": setup_code_lines,
227
235
  "fastmcp_args": fastmcp_args,
228
- "has_auth": True
236
+ "has_auth": True,
229
237
  }
230
238
 
231
239
 
@@ -237,26 +245,26 @@ def generate_auth_routes() -> str:
237
245
  api_key_config = get_api_key_config()
238
246
  if api_key_config:
239
247
  return ""
240
-
241
- provider_config, _ = get_auth_config() # Used to check if auth is enabled generally
248
+
249
+ provider_config, _ = get_auth_config() # Used to check if auth is enabled generally
242
250
  if not provider_config:
243
251
  return ""
244
-
252
+
245
253
  auth_routes_list = [
246
254
  "",
247
255
  "# Auth-specific routes, using the 'auth_provider' instance defined in the auth setup code",
248
256
  "@mcp.custom_route('/auth/callback', methods=['GET'])",
249
257
  "async def oauth_callback(request):",
250
258
  " # create_callback_handler is imported in the auth setup code block",
251
- " handler = create_callback_handler(auth_provider)",
259
+ " handler = create_callback_handler(auth_provider)",
252
260
  " return await handler(request)",
253
261
  "",
254
262
  "@mcp.custom_route('/login', methods=['GET'])",
255
263
  "async def login(request):",
256
264
  " from starlette.responses import RedirectResponse",
257
265
  " import urllib.parse",
258
- " default_redirect_uri = urllib.parse.quote_plus(\"http://localhost:5173/callback\")",
259
- " authorize_url = f\"/mcp/auth/authorize?client_id=default&response_type=code&redirect_uri={default_redirect_uri}\"",
266
+ ' default_redirect_uri = urllib.parse.quote_plus("http://localhost:5173/callback")',
267
+ ' authorize_url = f"/mcp/auth/authorize?client_id=default&response_type=code&redirect_uri={default_redirect_uri}"',
260
268
  " return RedirectResponse(authorize_url)",
261
269
  "",
262
270
  "@mcp.custom_route('/auth-error', methods=['GET'])",
@@ -264,7 +272,7 @@ def generate_auth_routes() -> str:
264
272
  " from starlette.responses import HTMLResponse",
265
273
  " error = request.query_params.get('error', 'unknown_error')",
266
274
  " error_desc = request.query_params.get('error_description', 'An authentication error occurred')",
267
- " html_content = f\"\"\"",
275
+ ' html_content = f"""',
268
276
  " <!DOCTYPE html>",
269
277
  " <html><head><title>Authentication Error</title>",
270
278
  " <style> body {{ font-family: system-ui, sans-serif; padding: 2rem; max-width: 600px; margin: auto; }} h1 {{ color: #c53030; }} .error-box {{ background-color: #fed7d7; border: 1px solid #f56565; border-radius: 0.25rem; padding: 1rem; margin: 1rem 0; }} .btn {{ background-color: #4299e1; color: white; border: none; padding: 0.5rem 1rem; border-radius: 0.25rem; cursor: pointer; text-decoration: none; display: inline-block; margin-top: 1rem; }} .btn:hover {{ background-color: #2b6cb0; }} </style>",
@@ -274,4 +282,4 @@ def generate_auth_routes() -> str:
274
282
  " return HTMLResponse(content=html_content)",
275
283
  "",
276
284
  ]
277
- return "\n".join(auth_routes_list)
285
+ return "\n".join(auth_routes_list)
@@ -4,9 +4,10 @@ This module provides functions for generating OpenTelemetry initialization
4
4
  and instrumentation code for FastMCP servers built with GolfMCP.
5
5
  """
6
6
 
7
+
7
8
  def generate_telemetry_imports() -> list[str]:
8
9
  """Generate import statements for telemetry instrumentation.
9
-
10
+
10
11
  Returns:
11
12
  List of import statements for telemetry
12
13
  """
@@ -20,16 +21,17 @@ def generate_telemetry_imports() -> list[str]:
20
21
  ")",
21
22
  ]
22
23
 
24
+
23
25
  def generate_component_registration_with_telemetry(
24
26
  component_type: str,
25
27
  component_name: str,
26
28
  module_path: str,
27
29
  entry_function: str,
28
30
  docstring: str = "",
29
- uri_template: str = None
31
+ uri_template: str = None,
30
32
  ) -> str:
31
33
  """Generate component registration code with telemetry instrumentation.
32
-
34
+
33
35
  Args:
34
36
  component_type: Type of component ('tool', 'resource', 'prompt')
35
37
  component_name: Name of the component
@@ -37,31 +39,32 @@ def generate_component_registration_with_telemetry(
37
39
  entry_function: Entry function name
38
40
  docstring: Component description
39
41
  uri_template: URI template for resources (optional)
40
-
42
+
41
43
  Returns:
42
44
  Python code string for registering the component with instrumentation
43
45
  """
44
46
  func_ref = f"{module_path}.{entry_function}"
45
47
  escaped_docstring = docstring.replace('"', '\\"') if docstring else ""
46
-
48
+
47
49
  if component_type == "tool":
48
50
  wrapped_func = f"instrument_tool({func_ref}, '{component_name}')"
49
51
  return f'mcp.add_tool({wrapped_func}, name="{component_name}", description="{escaped_docstring}")'
50
-
52
+
51
53
  elif component_type == "resource":
52
54
  wrapped_func = f"instrument_resource({func_ref}, '{uri_template}')"
53
55
  return f'mcp.add_resource_fn({wrapped_func}, uri="{uri_template}", name="{component_name}", description="{escaped_docstring}")'
54
-
56
+
55
57
  elif component_type == "prompt":
56
58
  wrapped_func = f"instrument_prompt({func_ref}, '{component_name}')"
57
59
  return f'mcp.add_prompt({wrapped_func}, name="{component_name}", description="{escaped_docstring}")'
58
-
60
+
59
61
  else:
60
62
  raise ValueError(f"Unknown component type: {component_type}")
61
63
 
64
+
62
65
  def get_otel_dependencies() -> list[str]:
63
66
  """Get list of OpenTelemetry dependencies to add to pyproject.toml.
64
-
67
+
65
68
  Returns:
66
69
  List of package requirements strings
67
70
  """
golf/core/config.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Configuration management for GolfMCP."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Any, Dict, List, Optional, Union, Tuple
4
+ from typing import Any
5
5
 
6
6
  from pydantic import BaseModel, Field, field_validator
7
7
  from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -16,14 +16,14 @@ class AuthConfig(BaseModel):
16
16
  provider: str = Field(
17
17
  ..., description="Authentication provider (e.g., 'jwks', 'google', 'github')"
18
18
  )
19
- scopes: List[str] = Field(default_factory=list, description="Required OAuth scopes")
20
- client_id_env: Optional[str] = Field(
19
+ scopes: list[str] = Field(default_factory=list, description="Required OAuth scopes")
20
+ client_id_env: str | None = Field(
21
21
  None, description="Environment variable name for client ID"
22
22
  )
23
- client_secret_env: Optional[str] = Field(
23
+ client_secret_env: str | None = Field(
24
24
  None, description="Environment variable name for client secret"
25
25
  )
26
- redirect_uri: Optional[str] = Field(
26
+ redirect_uri: str | None = Field(
27
27
  None, description="OAuth redirect URI (defaults to localhost callback)"
28
28
  )
29
29
 
@@ -44,7 +44,7 @@ class DeployConfig(BaseModel):
44
44
  """Deployment configuration."""
45
45
 
46
46
  default: str = Field("vercel", description="Default deployment target")
47
- options: Dict[str, Any] = Field(
47
+ options: dict[str, Any] = Field(
48
48
  default_factory=dict, description="Target-specific options"
49
49
  )
50
50
 
@@ -61,80 +61,91 @@ class Settings(BaseSettings):
61
61
 
62
62
  # Project metadata
63
63
  name: str = Field("GolfMCP Project", description="FastMCP instance name")
64
- description: Optional[str] = Field(None, description="Project description")
65
-
64
+ description: str | None = Field(None, description="Project description")
65
+
66
66
  # Build settings
67
67
  output_dir: str = Field("build", description="Build artifact folder")
68
-
68
+
69
69
  # Server settings
70
70
  host: str = Field("127.0.0.1", description="Server host")
71
71
  port: int = Field(3000, description="Server port")
72
- transport: str = Field("streamable-http", description="Transport protocol (streamable-http, sse, stdio)")
73
-
72
+ transport: str = Field(
73
+ "streamable-http",
74
+ description="Transport protocol (streamable-http, sse, stdio)",
75
+ )
76
+
74
77
  # Auth settings
75
- auth: Optional[Union[str, AuthConfig]] = Field(
78
+ auth: str | AuthConfig | None = Field(
76
79
  None, description="Authentication configuration or URI"
77
80
  )
78
-
81
+
79
82
  # Deploy settings
80
83
  deploy: DeployConfig = Field(
81
84
  default_factory=DeployConfig, description="Deployment configuration"
82
85
  )
83
-
86
+
84
87
  # Feature flags
85
88
  telemetry: bool = Field(True, description="Enable anonymous telemetry")
86
-
89
+
87
90
  # Project paths
88
91
  tools_dir: str = Field("tools", description="Directory containing tools")
89
- resources_dir: str = Field("resources", description="Directory containing resources")
92
+ resources_dir: str = Field(
93
+ "resources", description="Directory containing resources"
94
+ )
90
95
  prompts_dir: str = Field("prompts", description="Directory containing prompts")
91
96
 
92
97
  # OpenTelemetry config
93
- opentelemetry_enabled: bool = Field(False, description="Enable OpenTelemetry tracing")
94
- opentelemetry_default_exporter: str = Field("console", description="Default OpenTelemetry exporter type")
98
+ opentelemetry_enabled: bool = Field(
99
+ False, description="Enable OpenTelemetry tracing"
100
+ )
101
+ opentelemetry_default_exporter: str = Field(
102
+ "console", description="Default OpenTelemetry exporter type"
103
+ )
95
104
 
96
105
 
97
- def find_config_path(start_path: Optional[Path] = None) -> Optional[Path]:
106
+ def find_config_path(start_path: Path | None = None) -> Path | None:
98
107
  """Find the golf config file by searching upwards from the given path.
99
-
108
+
100
109
  Args:
101
110
  start_path: Path to start searching from (defaults to current directory)
102
-
111
+
103
112
  Returns:
104
113
  Path to the config file if found, None otherwise
105
114
  """
106
115
  if start_path is None:
107
116
  start_path = Path.cwd()
108
-
117
+
109
118
  current = start_path.absolute()
110
-
119
+
111
120
  # Don't search above the home directory
112
121
  home = Path.home().absolute()
113
-
122
+
114
123
  while current != current.parent and current != home:
115
124
  # Check for JSON config first (preferred)
116
125
  json_config = current / "golf.json"
117
126
  if json_config.exists():
118
127
  return json_config
119
-
128
+
120
129
  # Fall back to TOML config
121
130
  toml_config = current / "golf.toml"
122
131
  if toml_config.exists():
123
132
  return toml_config
124
-
133
+
125
134
  current = current.parent
126
-
135
+
127
136
  return None
128
137
 
129
138
 
130
- def find_project_root(start_path: Optional[Path] = None) -> Tuple[Optional[Path], Optional[Path]]:
139
+ def find_project_root(
140
+ start_path: Path | None = None,
141
+ ) -> tuple[Path | None, Path | None]:
131
142
  """Find a GolfMCP project root by searching for a config file.
132
-
143
+
133
144
  This is the central project discovery function that should be used by all commands.
134
-
145
+
135
146
  Args:
136
147
  start_path: Path to start searching from (defaults to current directory)
137
-
148
+
138
149
  Returns:
139
150
  Tuple of (project_root, config_path) if a project is found, or (None, None) if not
140
151
  """
@@ -144,38 +155,40 @@ def find_project_root(start_path: Optional[Path] = None) -> Tuple[Optional[Path]
144
155
  return None, None
145
156
 
146
157
 
147
- def load_settings(project_path: Union[str, Path]) -> Settings:
158
+ def load_settings(project_path: str | Path) -> Settings:
148
159
  """Load settings from a project directory.
149
-
160
+
150
161
  Args:
151
162
  project_path: Path to the project directory
152
-
163
+
153
164
  Returns:
154
165
  Settings object with values loaded from config files
155
166
  """
156
167
  # Convert to Path if needed
157
168
  if isinstance(project_path, str):
158
169
  project_path = Path(project_path)
159
-
170
+
160
171
  # Create default settings
161
172
  settings = Settings()
162
-
173
+
163
174
  # Check for .env file
164
175
  env_file = project_path / ".env"
165
176
  if env_file.exists():
166
177
  settings = Settings(_env_file=env_file)
167
-
178
+
168
179
  # Try to load JSON config file first
169
180
  json_config_path = project_path / "golf.json"
170
181
  if json_config_path.exists():
171
182
  return _load_json_settings(json_config_path, settings)
172
-
183
+
173
184
  # Fall back to TOML config file if JSON not found
174
185
  toml_config_path = project_path / "golf.toml"
175
186
  if toml_config_path.exists():
176
- console.print("[yellow]Warning: Using .toml configuration is deprecated. Please migrate to .json format.[/yellow]")
187
+ console.print(
188
+ "[yellow]Warning: Using .toml configuration is deprecated. Please migrate to .json format.[/yellow]"
189
+ )
177
190
  return _load_toml_settings(toml_config_path, settings)
178
-
191
+
179
192
  # No config file found, use defaults
180
193
  return settings
181
194
 
@@ -184,21 +197,24 @@ def _load_json_settings(path: Path, settings: Settings) -> Settings:
184
197
  """Load settings from a JSON file."""
185
198
  try:
186
199
  import json
187
- with open(path, "r") as f:
200
+
201
+ with open(path) as f:
188
202
  config_data = json.load(f)
189
-
203
+
190
204
  # Update settings from config data
191
205
  for key, value in config_data.items():
192
206
  if hasattr(settings, key):
193
207
  setattr(settings, key, value)
194
-
208
+
195
209
  return settings
196
210
  except Exception as e:
197
- console.print(f"[bold red]Error loading JSON config from {path}: {e}[/bold red]")
211
+ console.print(
212
+ f"[bold red]Error loading JSON config from {path}: {e}[/bold red]"
213
+ )
198
214
  return settings
199
215
 
200
216
 
201
217
  def _load_toml_settings(path: Path, settings: Settings) -> Settings:
202
218
  """Load settings from a TOML file."""
203
-
204
- return settings
219
+
220
+ return settings