golf-mcp 0.1.17__tar.gz → 0.1.18__tar.gz

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 (74) hide show
  1. {golf_mcp-0.1.17/src/golf_mcp.egg-info → golf_mcp-0.1.18}/PKG-INFO +3 -1
  2. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/pyproject.toml +7 -2
  3. golf_mcp-0.1.18/src/golf/__init__.py +1 -0
  4. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/core/builder.py +83 -1
  5. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/core/builder_auth.py +5 -0
  6. golf_mcp-0.1.18/src/golf/core/builder_metrics.py +232 -0
  7. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/core/config.py +6 -0
  8. golf_mcp-0.1.18/src/golf/metrics/__init__.py +10 -0
  9. golf_mcp-0.1.18/src/golf/metrics/collector.py +239 -0
  10. golf_mcp-0.1.18/src/golf/metrics/registry.py +12 -0
  11. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/telemetry/instrumentation.py +153 -98
  12. {golf_mcp-0.1.17 → golf_mcp-0.1.18/src/golf_mcp.egg-info}/PKG-INFO +3 -1
  13. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf_mcp.egg-info/SOURCES.txt +4 -0
  14. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf_mcp.egg-info/requires.txt +3 -0
  15. golf_mcp-0.1.17/src/golf/__init__.py +0 -1
  16. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/.docs/docs.md +0 -0
  17. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/.docs/fast-mcp.md +0 -0
  18. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/.docs/fastmcp-example-1.py +0 -0
  19. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/.docs/fastmcp-example-2.py +0 -0
  20. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/.docs/mcp.md +0 -0
  21. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/.docs/oauth-implementation.md +0 -0
  22. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/.docs/oauth.md +0 -0
  23. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/LICENSE +0 -0
  24. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/MANIFEST.in +0 -0
  25. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/README.md +0 -0
  26. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/setup.cfg +0 -0
  27. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/auth/__init__.py +0 -0
  28. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/auth/api_key.py +0 -0
  29. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/auth/helpers.py +0 -0
  30. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/auth/oauth.py +0 -0
  31. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/auth/provider.py +0 -0
  32. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/cli/__init__.py +0 -0
  33. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/cli/main.py +0 -0
  34. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/commands/__init__.py +0 -0
  35. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/commands/build.py +0 -0
  36. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/commands/init.py +0 -0
  37. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/commands/run.py +0 -0
  38. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/core/__init__.py +0 -0
  39. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/core/builder_telemetry.py +0 -0
  40. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/core/parser.py +0 -0
  41. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/core/platform.py +0 -0
  42. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/core/telemetry.py +0 -0
  43. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/core/transformer.py +0 -0
  44. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/__init__.py +0 -0
  45. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/api_key/.env +0 -0
  46. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/api_key/.env.example +0 -0
  47. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/api_key/README.md +0 -0
  48. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/api_key/golf.json +0 -0
  49. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/api_key/pre_build.py +0 -0
  50. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/api_key/tools/issues/create.py +0 -0
  51. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/api_key/tools/issues/list.py +0 -0
  52. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/api_key/tools/repos/list.py +0 -0
  53. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/api_key/tools/search/code.py +0 -0
  54. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/api_key/tools/users/get.py +0 -0
  55. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/.env +0 -0
  56. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/.env.example +0 -0
  57. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/README.md +0 -0
  58. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/golf.json +0 -0
  59. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/pre_build.py +0 -0
  60. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/prompts/welcome.py +0 -0
  61. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/resources/current_time.py +0 -0
  62. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/resources/info.py +0 -0
  63. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/resources/weather/common.py +0 -0
  64. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/resources/weather/current.py +0 -0
  65. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/resources/weather/forecast.py +0 -0
  66. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/tools/github_user.py +0 -0
  67. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/tools/hello.py +0 -0
  68. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/tools/payments/charge.py +0 -0
  69. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/tools/payments/common.py +0 -0
  70. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/examples/basic/tools/payments/refund.py +0 -0
  71. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf/telemetry/__init__.py +0 -0
  72. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf_mcp.egg-info/dependency_links.txt +0 -0
  73. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf_mcp.egg-info/entry_points.txt +0 -0
  74. {golf_mcp-0.1.17 → golf_mcp-0.1.18}/src/golf_mcp.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: golf-mcp
3
- Version: 0.1.17
3
+ Version: 0.1.18
4
4
  Summary: Framework for building MCP servers
5
5
  Author-email: Antoni Gmitruk <antoni@golf.dev>
6
6
  License-Expression: Apache-2.0
@@ -34,6 +34,8 @@ Requires-Dist: opentelemetry-sdk>=1.33.1; extra == "telemetry"
34
34
  Requires-Dist: opentelemetry-instrumentation-asgi>=0.40b0; extra == "telemetry"
35
35
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=0.40b0; extra == "telemetry"
36
36
  Requires-Dist: wrapt>=1.17.0; extra == "telemetry"
37
+ Provides-Extra: metrics
38
+ Requires-Dist: prometheus-client>=0.22.1; extra == "metrics"
37
39
  Dynamic: license-file
38
40
 
39
41
  <div align="center">
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "golf-mcp"
7
- version = "0.1.17"
7
+ version = "0.1.18"
8
8
  description = "Framework for building MCP servers"
9
9
  authors = [
10
10
  {name = "Antoni Gmitruk", email = "antoni@golf.dev"}
@@ -45,6 +45,9 @@ telemetry = [
45
45
  "opentelemetry-exporter-otlp-proto-http>=0.40b0",
46
46
  "wrapt>=1.17.0"
47
47
  ]
48
+ metrics = [
49
+ "prometheus-client>=0.22.1"
50
+ ]
48
51
 
49
52
  [project.scripts]
50
53
  golf = "golf.cli.main:app"
@@ -64,7 +67,7 @@ golf = ["examples/**/*"]
64
67
 
65
68
  [tool.poetry]
66
69
  name = "golf-mcp"
67
- version = "0.1.17"
70
+ version = "0.1.18"
68
71
  description = "Framework for building MCP servers with zero boilerplate"
69
72
  authors = ["Antoni Gmitruk <antoni@golf.dev>"]
70
73
  license = "Apache-2.0"
@@ -100,9 +103,11 @@ opentelemetry-sdk = {version = ">=1.33.1", optional = true}
100
103
  opentelemetry-instrumentation-asgi = {version = ">=0.40b0", optional = true}
101
104
  opentelemetry-exporter-otlp-proto-http = {version = ">=0.40b0", optional = true}
102
105
  wrapt = {version = ">=1.17.0", optional = true}
106
+ prometheus-client = {version = ">=0.22.1", optional = true}
103
107
 
104
108
  [tool.poetry.extras]
105
109
  telemetry = ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-instrumentation-asgi", "opentelemetry-exporter-otlp-proto-http", "wrapt"]
110
+ metrics = ["prometheus-client"]
106
111
 
107
112
  [tool.poetry.group.dev.dependencies]
108
113
  pytest = "^7.4.0"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.18"
@@ -562,6 +562,18 @@ class CodeGenerator:
562
562
  if self.settings.opentelemetry_enabled:
563
563
  imports.extend(generate_telemetry_imports())
564
564
 
565
+ # Add metrics imports if enabled
566
+ if self.settings.metrics_enabled:
567
+ from golf.core.builder_metrics import (
568
+ generate_metrics_imports,
569
+ generate_metrics_instrumentation,
570
+ generate_session_tracking,
571
+ )
572
+
573
+ imports.extend(generate_metrics_imports())
574
+ imports.extend(generate_metrics_instrumentation())
575
+ imports.extend(generate_session_tracking())
576
+
565
577
  # Add health check imports if enabled
566
578
  if self.settings.health_check_enabled:
567
579
  imports.extend(
@@ -672,6 +684,44 @@ class CodeGenerator:
672
684
  f"{full_module_path}.{entry_func}, '{component.name}')"
673
685
  )
674
686
 
687
+ if component_type == ComponentType.TOOL:
688
+ registration += (
689
+ f'\nmcp.add_tool(_wrapped_func, name="{component.name}", '
690
+ f'description="{component.docstring or ""}"'
691
+ )
692
+ # Add annotations if present
693
+ if hasattr(component, "annotations") and component.annotations:
694
+ registration += f", annotations={component.annotations}"
695
+ registration += ")"
696
+ elif component_type == ComponentType.RESOURCE:
697
+ registration += (
698
+ f"\nmcp.add_resource_fn(_wrapped_func, "
699
+ f'uri="{component.uri_template}", name="{component.name}", '
700
+ f'description="{component.docstring or ""}")'
701
+ )
702
+ else: # PROMPT
703
+ registration += (
704
+ f'\nmcp.add_prompt(_wrapped_func, name="{component.name}", '
705
+ f'description="{component.docstring or ""}")'
706
+ )
707
+ elif self.settings.metrics_enabled:
708
+ # Use metrics instrumentation
709
+ registration = (
710
+ f"# Register the {component_type.value} "
711
+ f"'{component.name}' with metrics"
712
+ )
713
+ entry_func = (
714
+ component.entry_function
715
+ if hasattr(component, "entry_function")
716
+ and component.entry_function
717
+ else "export"
718
+ )
719
+
720
+ registration += (
721
+ f"\n_wrapped_func = instrument_{component_type.value}("
722
+ f"{full_module_path}.{entry_func}, '{component.name}')"
723
+ )
724
+
675
725
  if component_type == ComponentType.TOOL:
676
726
  registration += (
677
727
  f'\nmcp.add_tool(_wrapped_func, name="{component.name}", '
@@ -826,6 +876,15 @@ class CodeGenerator:
826
876
  ]
827
877
  )
828
878
 
879
+ # Add metrics initialization if enabled
880
+ early_metrics_init = []
881
+ if self.settings.metrics_enabled:
882
+ from golf.core.builder_metrics import generate_metrics_initialization
883
+
884
+ early_metrics_init.extend(
885
+ generate_metrics_initialization(self.settings.name)
886
+ )
887
+
829
888
  # Main entry point with transport-specific app initialization
830
889
  main_code = [
831
890
  'if __name__ == "__main__":',
@@ -864,6 +923,13 @@ class CodeGenerator:
864
923
  )
865
924
  middleware_list.append("Middleware(ApiKeyMiddleware)")
866
925
 
926
+ # Add metrics middleware if enabled
927
+ if self.settings.metrics_enabled:
928
+ middleware_setup.append(
929
+ " from starlette.middleware import Middleware"
930
+ )
931
+ middleware_list.append("Middleware(MetricsMiddleware)")
932
+
867
933
  # Add OpenTelemetry middleware if enabled
868
934
  if self.settings.opentelemetry_enabled:
869
935
  middleware_setup.append(
@@ -904,6 +970,13 @@ class CodeGenerator:
904
970
  )
905
971
  middleware_list.append("Middleware(ApiKeyMiddleware)")
906
972
 
973
+ # Add metrics middleware if enabled
974
+ if self.settings.metrics_enabled:
975
+ middleware_setup.append(
976
+ " from starlette.middleware import Middleware"
977
+ )
978
+ middleware_list.append("Middleware(MetricsMiddleware)")
979
+
907
980
  # Add OpenTelemetry middleware if enabled
908
981
  if self.settings.opentelemetry_enabled:
909
982
  middleware_setup.append(
@@ -937,6 +1010,13 @@ class CodeGenerator:
937
1010
  [" # Run with stdio transport", ' mcp.run(transport="stdio")']
938
1011
  )
939
1012
 
1013
+ # Add metrics route if enabled
1014
+ metrics_route_code = []
1015
+ if self.settings.metrics_enabled:
1016
+ from golf.core.builder_metrics import generate_metrics_route
1017
+
1018
+ metrics_route_code = generate_metrics_route(self.settings.metrics_path)
1019
+
940
1020
  # Add health check route if enabled
941
1021
  health_check_code = []
942
1022
  if self.settings.health_check_enabled:
@@ -953,14 +1033,16 @@ class CodeGenerator:
953
1033
 
954
1034
  # Combine all sections
955
1035
  # Order: imports, env_section, auth_setup, server_code (mcp init),
956
- # early_telemetry_init, component_registrations, health_check_code, main_code (run block)
1036
+ # early_telemetry_init, early_metrics_init, component_registrations, metrics_route_code, health_check_code, main_code (run block)
957
1037
  code = "\n".join(
958
1038
  imports
959
1039
  + env_section
960
1040
  + auth_setup_code
961
1041
  + server_code_lines
962
1042
  + early_telemetry_init
1043
+ + early_metrics_init
963
1044
  + component_registrations
1045
+ + metrics_route_code
964
1046
  + health_check_code
965
1047
  + main_code
966
1048
  )
@@ -187,6 +187,11 @@ def generate_api_key_auth_components(
187
187
  " # Debug mode from environment",
188
188
  " debug = os.environ.get('GOLF_API_KEY_DEBUG', '').lower() == 'true'",
189
189
  " ",
190
+ " # Skip auth for monitoring endpoints",
191
+ " path = request.url.path",
192
+ " if path in ['/metrics', '/health']:",
193
+ " return await call_next(request)",
194
+ " ",
190
195
  " api_key_config = get_api_key_config()",
191
196
  " ",
192
197
  " if api_key_config:",
@@ -0,0 +1,232 @@
1
+ """Metrics integration for the GolfMCP build process.
2
+
3
+ This module provides functions for generating Prometheus metrics initialization
4
+ and collection code for FastMCP servers built with GolfMCP.
5
+ """
6
+
7
+
8
+ def generate_metrics_imports() -> list[str]:
9
+ """Generate import statements for metrics collection.
10
+
11
+ Returns:
12
+ List of import statements for metrics
13
+ """
14
+ return [
15
+ "# Prometheus metrics imports",
16
+ "from golf.metrics import init_metrics, get_metrics_collector",
17
+ "from prometheus_client import generate_latest, CONTENT_TYPE_LATEST",
18
+ "from starlette.responses import Response",
19
+ "from starlette.middleware.base import BaseHTTPMiddleware",
20
+ "from starlette.requests import Request",
21
+ "import time",
22
+ ]
23
+
24
+
25
+ def generate_metrics_initialization(server_name: str) -> list[str]:
26
+ """Generate metrics initialization code.
27
+
28
+ Args:
29
+ server_name: Name of the MCP server
30
+
31
+ Returns:
32
+ List of code lines for metrics initialization
33
+ """
34
+ return [
35
+ "# Initialize metrics collection",
36
+ "init_metrics(enabled=True)",
37
+ "",
38
+ ]
39
+
40
+
41
+ def generate_metrics_route(metrics_path: str) -> list[str]:
42
+ """Generate the metrics endpoint route code.
43
+
44
+ Args:
45
+ metrics_path: Path for the metrics endpoint (e.g., "/metrics")
46
+
47
+ Returns:
48
+ List of code lines for the metrics route
49
+ """
50
+ return [
51
+ "# Add metrics endpoint",
52
+ f'@mcp.custom_route("{metrics_path}", methods=["GET"])',
53
+ "async def metrics_endpoint(request):",
54
+ ' """Prometheus metrics endpoint for monitoring."""',
55
+ " # Update uptime before returning metrics",
56
+ " update_uptime()",
57
+ " return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)",
58
+ "",
59
+ ]
60
+
61
+
62
+ def get_metrics_dependencies() -> list[str]:
63
+ """Get list of metrics dependencies to add to pyproject.toml.
64
+
65
+ Returns:
66
+ List of package requirements strings
67
+ """
68
+ return [
69
+ "prometheus-client>=0.19.0",
70
+ ]
71
+
72
+
73
+ def generate_metrics_instrumentation() -> list[str]:
74
+ """Generate metrics instrumentation wrapper functions.
75
+
76
+ Returns:
77
+ List of code lines for metrics instrumentation
78
+ """
79
+ return [
80
+ "# Metrics instrumentation wrapper functions",
81
+ "import time",
82
+ "import functools",
83
+ "from typing import Any, Callable",
84
+ "",
85
+ "def instrument_tool(func: Callable, tool_name: str) -> Callable:",
86
+ ' """Wrap a tool function with metrics collection."""',
87
+ " @functools.wraps(func)",
88
+ " async def wrapper(*args, **kwargs) -> Any:",
89
+ " collector = get_metrics_collector()",
90
+ " start_time = time.time()",
91
+ " status = 'success'",
92
+ " try:",
93
+ " result = await func(*args, **kwargs)",
94
+ " return result",
95
+ " except Exception as e:",
96
+ " status = 'error'",
97
+ " collector.increment_error('tool', type(e).__name__)",
98
+ " raise",
99
+ " finally:",
100
+ " duration = time.time() - start_time",
101
+ " collector.increment_tool_execution(tool_name, status)",
102
+ " collector.record_tool_duration(tool_name, duration)",
103
+ " return wrapper",
104
+ "",
105
+ "def instrument_resource(func: Callable, resource_name: str) -> Callable:",
106
+ ' """Wrap a resource function with metrics collection."""',
107
+ " @functools.wraps(func)",
108
+ " async def wrapper(*args, **kwargs) -> Any:",
109
+ " collector = get_metrics_collector()",
110
+ " try:",
111
+ " result = await func(*args, **kwargs)",
112
+ " # Extract URI from args if available for resource_reads metric",
113
+ " if args and len(args) > 0:",
114
+ " uri = str(args[0]) if args[0] else resource_name",
115
+ " else:",
116
+ " uri = resource_name",
117
+ " collector.increment_resource_read(uri)",
118
+ " return result",
119
+ " except Exception as e:",
120
+ " collector.increment_error('resource', type(e).__name__)",
121
+ " raise",
122
+ " return wrapper",
123
+ "",
124
+ "def instrument_prompt(func: Callable, prompt_name: str) -> Callable:",
125
+ ' """Wrap a prompt function with metrics collection."""',
126
+ " @functools.wraps(func)",
127
+ " async def wrapper(*args, **kwargs) -> Any:",
128
+ " collector = get_metrics_collector()",
129
+ " try:",
130
+ " result = await func(*args, **kwargs)",
131
+ " collector.increment_prompt_generation(prompt_name)",
132
+ " return result",
133
+ " except Exception as e:",
134
+ " collector.increment_error('prompt', type(e).__name__)",
135
+ " raise",
136
+ " return wrapper",
137
+ "",
138
+ "# HTTP Request Metrics Middleware",
139
+ "class MetricsMiddleware(BaseHTTPMiddleware):",
140
+ ' """Middleware to collect HTTP request metrics."""',
141
+ "",
142
+ " async def dispatch(self, request: Request, call_next):",
143
+ " collector = get_metrics_collector()",
144
+ " start_time = time.time()",
145
+ " ",
146
+ " # Extract path and method",
147
+ " method = request.method",
148
+ " path = request.url.path",
149
+ " ",
150
+ " try:",
151
+ " response = await call_next(request)",
152
+ " status_code = response.status_code",
153
+ " except Exception as e:",
154
+ " status_code = 500",
155
+ " collector.increment_error('http', type(e).__name__)",
156
+ " raise",
157
+ " finally:",
158
+ " duration = time.time() - start_time",
159
+ " collector.increment_http_request(method, status_code, path)",
160
+ " collector.record_http_duration(method, path, duration)",
161
+ " ",
162
+ " return response",
163
+ "",
164
+ "# Session tracking helpers",
165
+ "import atexit",
166
+ "from contextlib import asynccontextmanager",
167
+ "",
168
+ "# Global server start time for uptime tracking",
169
+ "_server_start_time = time.time()",
170
+ "",
171
+ "def track_session_start():",
172
+ ' """Track when a new session starts."""',
173
+ " collector = get_metrics_collector()",
174
+ " collector.increment_session()",
175
+ "",
176
+ "def track_session_end(start_time: float):",
177
+ ' """Track when a session ends."""',
178
+ " collector = get_metrics_collector()",
179
+ " duration = time.time() - start_time",
180
+ " collector.record_session_duration(duration)",
181
+ "",
182
+ "def update_uptime():",
183
+ ' """Update the uptime metric."""',
184
+ " collector = get_metrics_collector()",
185
+ " uptime = time.time() - _server_start_time",
186
+ " collector.set_uptime(uptime)",
187
+ "",
188
+ "# Initialize uptime tracking",
189
+ "update_uptime()",
190
+ "",
191
+ ]
192
+
193
+
194
+ def generate_session_tracking() -> list[str]:
195
+ """Generate session tracking integration code.
196
+
197
+ Returns:
198
+ List of code lines for session tracking
199
+ """
200
+ return [
201
+ "# Session tracking integration",
202
+ "import asyncio",
203
+ "from typing import Dict",
204
+ "",
205
+ "# Track active sessions",
206
+ "_active_sessions: Dict[str, float] = {}",
207
+ "",
208
+ "# Hook into FastMCP's session lifecycle if available",
209
+ "try:",
210
+ " from fastmcp.server import SessionManager",
211
+ " ",
212
+ " # Monkey patch session creation if possible",
213
+ " _original_create_session = getattr(mcp, '_create_session', None)",
214
+ " if _original_create_session:",
215
+ " async def _patched_create_session(*args, **kwargs):",
216
+ " session_id = str(id(args)) if args else 'unknown'",
217
+ " _active_sessions[session_id] = time.time()",
218
+ " track_session_start()",
219
+ " try:",
220
+ " return await _original_create_session(*args, **kwargs)",
221
+ " except Exception:",
222
+ " # If session creation fails, clean up",
223
+ " if session_id in _active_sessions:",
224
+ " del _active_sessions[session_id]",
225
+ " raise",
226
+ " ",
227
+ " mcp._create_session = _patched_create_session",
228
+ "except (ImportError, AttributeError):",
229
+ " # Fallback: track sessions via request patterns",
230
+ " pass",
231
+ "",
232
+ ]
@@ -115,6 +115,12 @@ class Settings(BaseSettings):
115
115
  description="Make Streamable-HTTP transport stateless (new session per request)",
116
116
  )
117
117
 
118
+ # Metrics configuration
119
+ metrics_enabled: bool = Field(
120
+ False, description="Enable Prometheus metrics endpoint"
121
+ )
122
+ metrics_path: str = Field("/metrics", description="Metrics endpoint path")
123
+
118
124
 
119
125
  def find_config_path(start_path: Path | None = None) -> Path | None:
120
126
  """Find the golf config file by searching upwards from the given path.
@@ -0,0 +1,10 @@
1
+ """Golf metrics module for Prometheus-compatible metrics collection."""
2
+
3
+ from golf.metrics.collector import MetricsCollector, get_metrics_collector
4
+ from golf.metrics.registry import init_metrics
5
+
6
+ __all__ = [
7
+ "MetricsCollector",
8
+ "get_metrics_collector",
9
+ "init_metrics",
10
+ ]