golf-mcp 0.2.16__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.
Files changed (52) hide show
  1. golf/__init__.py +1 -0
  2. golf/auth/__init__.py +277 -0
  3. golf/auth/api_key.py +73 -0
  4. golf/auth/factory.py +360 -0
  5. golf/auth/helpers.py +175 -0
  6. golf/auth/providers.py +586 -0
  7. golf/auth/registry.py +256 -0
  8. golf/cli/__init__.py +1 -0
  9. golf/cli/branding.py +191 -0
  10. golf/cli/main.py +377 -0
  11. golf/commands/__init__.py +5 -0
  12. golf/commands/build.py +81 -0
  13. golf/commands/init.py +290 -0
  14. golf/commands/run.py +137 -0
  15. golf/core/__init__.py +1 -0
  16. golf/core/builder.py +1884 -0
  17. golf/core/builder_auth.py +209 -0
  18. golf/core/builder_metrics.py +221 -0
  19. golf/core/builder_telemetry.py +99 -0
  20. golf/core/config.py +199 -0
  21. golf/core/parser.py +1085 -0
  22. golf/core/telemetry.py +492 -0
  23. golf/core/transformer.py +231 -0
  24. golf/examples/__init__.py +0 -0
  25. golf/examples/basic/.env.example +4 -0
  26. golf/examples/basic/README.md +133 -0
  27. golf/examples/basic/auth.py +76 -0
  28. golf/examples/basic/golf.json +5 -0
  29. golf/examples/basic/prompts/welcome.py +27 -0
  30. golf/examples/basic/resources/current_time.py +34 -0
  31. golf/examples/basic/resources/info.py +28 -0
  32. golf/examples/basic/resources/weather/city.py +46 -0
  33. golf/examples/basic/resources/weather/client.py +48 -0
  34. golf/examples/basic/resources/weather/current.py +36 -0
  35. golf/examples/basic/resources/weather/forecast.py +36 -0
  36. golf/examples/basic/tools/calculator.py +94 -0
  37. golf/examples/basic/tools/say/hello.py +65 -0
  38. golf/metrics/__init__.py +10 -0
  39. golf/metrics/collector.py +320 -0
  40. golf/metrics/registry.py +12 -0
  41. golf/telemetry/__init__.py +23 -0
  42. golf/telemetry/instrumentation.py +1402 -0
  43. golf/utilities/__init__.py +12 -0
  44. golf/utilities/context.py +53 -0
  45. golf/utilities/elicitation.py +170 -0
  46. golf/utilities/sampling.py +221 -0
  47. golf_mcp-0.2.16.dist-info/METADATA +262 -0
  48. golf_mcp-0.2.16.dist-info/RECORD +52 -0
  49. golf_mcp-0.2.16.dist-info/WHEEL +5 -0
  50. golf_mcp-0.2.16.dist-info/entry_points.txt +2 -0
  51. golf_mcp-0.2.16.dist-info/licenses/LICENSE +201 -0
  52. golf_mcp-0.2.16.dist-info/top_level.txt +1 -0
@@ -0,0 +1,36 @@
1
+ """Weather forecast resource example demonstrating nested resources."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+
6
+ from .client import weather_client
7
+
8
+ # The URI that clients will use to access this resource
9
+ resource_uri = "weather://forecast"
10
+
11
+
12
+ async def forecast_weather() -> dict[str, Any]:
13
+ """Provide a weather forecast for a default city.
14
+
15
+ This example demonstrates:
16
+ 1. Nested resource organization (resources/weather/forecast.py)
17
+ 2. Resource without URI parameters
18
+ 3. Using shared client from the client.py file
19
+ """
20
+ # Use the shared weather client from client.py
21
+ forecast_data = await weather_client.get_forecast("New York", days=5)
22
+
23
+ # Add some additional data
24
+ forecast_data.update(
25
+ {
26
+ "updated_at": datetime.now().isoformat(),
27
+ "source": "GolfMCP Weather API",
28
+ "unit": "fahrenheit",
29
+ }
30
+ )
31
+
32
+ return forecast_data
33
+
34
+
35
+ # Designate the entry point function
36
+ export = forecast_weather
@@ -0,0 +1,94 @@
1
+ """Enhanced calculator tool with optional LLM-powered explanations."""
2
+
3
+ from typing import Annotated
4
+
5
+ from pydantic import BaseModel, Field
6
+ from golf.utilities import sample
7
+
8
+
9
+ class CalculationResult(BaseModel):
10
+ """Result of a mathematical calculation."""
11
+
12
+ result: float
13
+ operation: str
14
+ expression: str
15
+
16
+
17
+ async def calculate(
18
+ expression: Annotated[
19
+ str,
20
+ Field(
21
+ description="Mathematical expression to evaluate (e.g., '2 + 3', '10 * 5', '100 / 4')",
22
+ examples=["2 + 3", "10 * 5.5", "(8 - 3) * 2"],
23
+ ),
24
+ ],
25
+ explain: Annotated[
26
+ bool,
27
+ Field(
28
+ description="Whether to provide an LLM-powered step-by-step explanation",
29
+ default=False,
30
+ ),
31
+ ] = False,
32
+ ) -> CalculationResult:
33
+ """Evaluate a mathematical expression with optional LLM explanation.
34
+
35
+ This enhanced calculator can:
36
+ - Perform basic arithmetic operations (+, -, *, /, parentheses)
37
+ - Handle decimal numbers
38
+ - Optionally provide LLM-powered step-by-step explanations
39
+
40
+ Examples:
41
+ - calculate("2 + 3") → 5
42
+ - calculate("10 * 5.5") → 55.0
43
+ - calculate("(8 - 3) * 2", explain=True) → 10 with explanation
44
+ """
45
+ try:
46
+ # Simple expression evaluation using eval (safe for basic math)
47
+ # In production, consider using a proper math expression parser
48
+ allowed_chars = set("0123456789+-*/.() ")
49
+ if not all(c in allowed_chars for c in expression):
50
+ raise ValueError("Expression contains invalid characters")
51
+
52
+ # Evaluate the expression
53
+ result = eval(expression, {"__builtins__": {}}, {})
54
+
55
+ # Ensure result is a number
56
+ if not isinstance(result, (int, float)):
57
+ raise ValueError("Expression did not evaluate to a number")
58
+
59
+ # Generate explanation if requested
60
+ result_expression = expression
61
+ if explain:
62
+ try:
63
+ explanation = await sample(
64
+ f"Explain this mathematical expression step by step: {expression} = {result}",
65
+ system_prompt="You are a helpful math tutor. Provide clear, step-by-step explanations.",
66
+ max_tokens=200,
67
+ )
68
+ result_expression = f"{expression}\n\nExplanation: {explanation}"
69
+ except Exception:
70
+ # If sampling fails, continue without explanation
71
+ result_expression = f"{expression}\n\n(Explanation unavailable)"
72
+
73
+ return CalculationResult(
74
+ result=float(result),
75
+ operation="evaluate",
76
+ expression=result_expression,
77
+ )
78
+
79
+ except ZeroDivisionError:
80
+ return CalculationResult(
81
+ result=float("inf"),
82
+ operation="error",
83
+ expression=f"{expression} → Division by zero",
84
+ )
85
+ except Exception as e:
86
+ return CalculationResult(
87
+ result=0.0,
88
+ operation="error",
89
+ expression=f"{expression} → Error: {str(e)}",
90
+ )
91
+
92
+
93
+ # Export the tool
94
+ export = calculate
@@ -0,0 +1,65 @@
1
+ """Enhanced hello tool with elicitation capabilities."""
2
+
3
+ from typing import Annotated
4
+
5
+ from pydantic import BaseModel, Field
6
+ from golf.utilities import elicit
7
+
8
+
9
+ class Output(BaseModel):
10
+ """Response from the hello tool."""
11
+
12
+ message: str
13
+
14
+
15
+ async def hello(
16
+ name: Annotated[str, Field(description="The name of the person to greet")] = "World",
17
+ greeting: Annotated[str, Field(description="The greeting phrase to use")] = "Hello",
18
+ personalized: Annotated[
19
+ bool,
20
+ Field(
21
+ description="Whether to ask for additional personal details to create a personalized greeting",
22
+ default=False,
23
+ ),
24
+ ] = False,
25
+ ) -> Output:
26
+ """Say hello with optional personalized elicitation.
27
+
28
+ This enhanced tool can:
29
+ - Provide basic greetings
30
+ - Elicit additional personal information for personalized messages
31
+ - Demonstrate Golf's elicitation capabilities
32
+
33
+ Examples:
34
+ - hello("Alice") → "Hello, Alice!"
35
+ - hello("Bob", personalized=True) → Asks for details, then personalized greeting
36
+ """
37
+ # Basic greeting
38
+ basic_message = f"{greeting}, {name}!"
39
+
40
+ # If personalized greeting is requested, elicit additional info
41
+ if personalized:
42
+ try:
43
+ # Ask for user's mood
44
+ mood = await elicit(
45
+ "How are you feeling today?",
46
+ ["happy", "excited", "calm", "focused", "creative"],
47
+ )
48
+
49
+ # Create personalized message
50
+ personalized_message = f"{greeting}, {name}! Hope you're having a {mood} day!"
51
+
52
+ return Output(message=personalized_message)
53
+
54
+ except Exception as e:
55
+ # If elicitation fails, fall back to basic greeting
56
+ print(f"Personalization failed: {e}")
57
+ return Output(message=f"{basic_message} (personalization unavailable)")
58
+
59
+ # Return basic greeting
60
+ print(f"{greeting} {name}...")
61
+ return Output(message=basic_message)
62
+
63
+
64
+ # Designate the entry point function
65
+ export = hello
@@ -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
+ ]
@@ -0,0 +1,320 @@
1
+ """Metrics collector for Golf MCP servers."""
2
+
3
+ from typing import Optional
4
+
5
+ # Global metrics collector instance
6
+ _metrics_collector: Optional["MetricsCollector"] = None
7
+
8
+
9
+ class MetricsCollector:
10
+ """Collects metrics for Golf MCP servers using Prometheus client."""
11
+
12
+ def __init__(self, enabled: bool = False) -> None:
13
+ """Initialize the metrics collector.
14
+
15
+ Args:
16
+ enabled: Whether metrics collection is enabled
17
+ """
18
+ self.enabled = enabled
19
+ self._metrics = {}
20
+
21
+ if self.enabled:
22
+ self._init_prometheus_metrics()
23
+
24
+ def _init_prometheus_metrics(self) -> None:
25
+ """Initialize Prometheus metrics if enabled."""
26
+ try:
27
+ from prometheus_client import Counter, Histogram, Gauge
28
+
29
+ # Tool execution metrics
30
+ self._metrics["tool_executions"] = Counter(
31
+ "golf_tool_executions_total",
32
+ "Total number of tool executions",
33
+ ["tool_name", "status"],
34
+ )
35
+
36
+ self._metrics["tool_duration"] = Histogram(
37
+ "golf_tool_duration_seconds",
38
+ "Tool execution duration in seconds",
39
+ ["tool_name"],
40
+ )
41
+
42
+ # HTTP request metrics
43
+ self._metrics["http_requests"] = Counter(
44
+ "golf_http_requests_total",
45
+ "Total number of HTTP requests",
46
+ ["method", "status_code", "path"],
47
+ )
48
+
49
+ self._metrics["http_duration"] = Histogram(
50
+ "golf_http_request_duration_seconds",
51
+ "HTTP request duration in seconds",
52
+ ["method", "path"],
53
+ )
54
+
55
+ # Resource access metrics
56
+ self._metrics["resource_reads"] = Counter(
57
+ "golf_resource_reads_total",
58
+ "Total number of resource reads",
59
+ ["resource_uri"],
60
+ )
61
+
62
+ # Prompt generation metrics
63
+ self._metrics["prompt_generations"] = Counter(
64
+ "golf_prompt_generations_total",
65
+ "Total number of prompt generations",
66
+ ["prompt_name"],
67
+ )
68
+
69
+ # Sampling metrics
70
+ self._metrics["sampling_requests"] = Counter(
71
+ "golf_sampling_requests_total",
72
+ "Total number of sampling requests",
73
+ ["sampling_type", "status"],
74
+ )
75
+
76
+ self._metrics["sampling_duration"] = Histogram(
77
+ "golf_sampling_duration_seconds",
78
+ "Sampling request duration in seconds",
79
+ ["sampling_type"],
80
+ )
81
+
82
+ self._metrics["sampling_tokens"] = Histogram(
83
+ "golf_sampling_tokens",
84
+ "Number of tokens in sampling responses",
85
+ ["sampling_type"],
86
+ )
87
+
88
+ # Elicitation metrics
89
+ self._metrics["elicitation_requests"] = Counter(
90
+ "golf_elicitation_requests_total",
91
+ "Total number of elicitation requests",
92
+ ["elicitation_type", "status"],
93
+ )
94
+
95
+ self._metrics["elicitation_duration"] = Histogram(
96
+ "golf_elicitation_duration_seconds",
97
+ "Elicitation request duration in seconds",
98
+ ["elicitation_type"],
99
+ )
100
+
101
+ # Error metrics
102
+ self._metrics["errors"] = Counter(
103
+ "golf_errors_total",
104
+ "Total number of errors",
105
+ ["component_type", "error_type"],
106
+ )
107
+
108
+ # Session metrics
109
+ self._metrics["sessions_total"] = Counter("golf_sessions_total", "Total number of sessions created")
110
+
111
+ self._metrics["session_duration"] = Histogram(
112
+ "golf_session_duration_seconds", "Session duration in seconds"
113
+ )
114
+
115
+ # System metrics
116
+ self._metrics["uptime"] = Gauge("golf_uptime_seconds", "Server uptime in seconds")
117
+
118
+ except ImportError:
119
+ # Prometheus client not available, disable metrics
120
+ self.enabled = False
121
+
122
+ def increment_tool_execution(self, tool_name: str, status: str) -> None:
123
+ """Record a tool execution.
124
+
125
+ Args:
126
+ tool_name: Name of the tool that was executed
127
+ status: Execution status ('success' or 'error')
128
+ """
129
+ if not self.enabled or "tool_executions" not in self._metrics:
130
+ return
131
+
132
+ self._metrics["tool_executions"].labels(tool_name=tool_name, status=status).inc()
133
+
134
+ def record_tool_duration(self, tool_name: str, duration: float) -> None:
135
+ """Record tool execution duration.
136
+
137
+ Args:
138
+ tool_name: Name of the tool
139
+ duration: Execution duration in seconds
140
+ """
141
+ if not self.enabled or "tool_duration" not in self._metrics:
142
+ return
143
+
144
+ self._metrics["tool_duration"].labels(tool_name=tool_name).observe(duration)
145
+
146
+ def increment_http_request(self, method: str, status_code: int, path: str) -> None:
147
+ """Record an HTTP request.
148
+
149
+ Args:
150
+ method: HTTP method (GET, POST, etc.)
151
+ status_code: HTTP status code
152
+ path: Request path
153
+ """
154
+ if not self.enabled or "http_requests" not in self._metrics:
155
+ return
156
+
157
+ self._metrics["http_requests"].labels(method=method, status_code=str(status_code), path=path).inc()
158
+
159
+ def record_http_duration(self, method: str, path: str, duration: float) -> None:
160
+ """Record HTTP request duration.
161
+
162
+ Args:
163
+ method: HTTP method
164
+ path: Request path
165
+ duration: Request duration in seconds
166
+ """
167
+ if not self.enabled or "http_duration" not in self._metrics:
168
+ return
169
+
170
+ self._metrics["http_duration"].labels(method=method, path=path).observe(duration)
171
+
172
+ def increment_resource_read(self, resource_uri: str) -> None:
173
+ """Record a resource read.
174
+
175
+ Args:
176
+ resource_uri: URI of the resource that was read
177
+ """
178
+ if not self.enabled or "resource_reads" not in self._metrics:
179
+ return
180
+
181
+ self._metrics["resource_reads"].labels(resource_uri=resource_uri).inc()
182
+
183
+ def increment_prompt_generation(self, prompt_name: str) -> None:
184
+ """Record a prompt generation.
185
+
186
+ Args:
187
+ prompt_name: Name of the prompt that was generated
188
+ """
189
+ if not self.enabled or "prompt_generations" not in self._metrics:
190
+ return
191
+
192
+ self._metrics["prompt_generations"].labels(prompt_name=prompt_name).inc()
193
+
194
+ def increment_error(self, component_type: str, error_type: str) -> None:
195
+ """Record an error.
196
+
197
+ Args:
198
+ component_type: Type of component ('tool', 'resource', 'prompt', 'http')
199
+ error_type: Type of error ('timeout', 'auth_error',
200
+ 'validation_error', etc.)
201
+ """
202
+ if not self.enabled or "errors" not in self._metrics:
203
+ return
204
+
205
+ self._metrics["errors"].labels(component_type=component_type, error_type=error_type).inc()
206
+
207
+ def increment_session(self) -> None:
208
+ """Record a new session."""
209
+ if not self.enabled or "sessions_total" not in self._metrics:
210
+ return
211
+
212
+ self._metrics["sessions_total"].inc()
213
+
214
+ def record_session_duration(self, duration: float) -> None:
215
+ """Record session duration.
216
+
217
+ Args:
218
+ duration: Session duration in seconds
219
+ """
220
+ if not self.enabled or "session_duration" not in self._metrics:
221
+ return
222
+
223
+ self._metrics["session_duration"].observe(duration)
224
+
225
+ def set_uptime(self, seconds: float) -> None:
226
+ """Set the server uptime.
227
+
228
+ Args:
229
+ seconds: Server uptime in seconds
230
+ """
231
+ if not self.enabled or "uptime" not in self._metrics:
232
+ return
233
+
234
+ self._metrics["uptime"].set(seconds)
235
+
236
+ def increment_sampling(self, sampling_type: str, status: str) -> None:
237
+ """Record a sampling request.
238
+
239
+ Args:
240
+ sampling_type: Type of sampling ('sample', 'structured', 'context')
241
+ status: Request status ('success' or 'error')
242
+ """
243
+ if not self.enabled or "sampling_requests" not in self._metrics:
244
+ return
245
+
246
+ self._metrics["sampling_requests"].labels(sampling_type=sampling_type, status=status).inc()
247
+
248
+ def record_sampling_duration(self, sampling_type: str, duration: float) -> None:
249
+ """Record sampling request duration.
250
+
251
+ Args:
252
+ sampling_type: Type of sampling
253
+ duration: Request duration in seconds
254
+ """
255
+ if not self.enabled or "sampling_duration" not in self._metrics:
256
+ return
257
+
258
+ self._metrics["sampling_duration"].labels(sampling_type=sampling_type).observe(duration)
259
+
260
+ def record_sampling_tokens(self, sampling_type: str, token_count: int) -> None:
261
+ """Record sampling token count.
262
+
263
+ Args:
264
+ sampling_type: Type of sampling
265
+ token_count: Number of tokens in the response
266
+ """
267
+ if not self.enabled or "sampling_tokens" not in self._metrics:
268
+ return
269
+
270
+ self._metrics["sampling_tokens"].labels(sampling_type=sampling_type).observe(token_count)
271
+
272
+ def increment_elicitation(self, elicitation_type: str, status: str) -> None:
273
+ """Record an elicitation request.
274
+
275
+ Args:
276
+ elicitation_type: Type of elicitation ('elicit', 'confirmation')
277
+ status: Request status ('success' or 'error')
278
+ """
279
+ if not self.enabled or "elicitation_requests" not in self._metrics:
280
+ return
281
+
282
+ self._metrics["elicitation_requests"].labels(elicitation_type=elicitation_type, status=status).inc()
283
+
284
+ def record_elicitation_duration(self, elicitation_type: str, duration: float) -> None:
285
+ """Record elicitation request duration.
286
+
287
+ Args:
288
+ elicitation_type: Type of elicitation
289
+ duration: Request duration in seconds
290
+ """
291
+ if not self.enabled or "elicitation_duration" not in self._metrics:
292
+ return
293
+
294
+ self._metrics["elicitation_duration"].labels(elicitation_type=elicitation_type).observe(duration)
295
+
296
+
297
+ def init_metrics_collector(enabled: bool = False) -> MetricsCollector:
298
+ """Initialize the global metrics collector.
299
+
300
+ Args:
301
+ enabled: Whether to enable metrics collection
302
+
303
+ Returns:
304
+ The initialized metrics collector
305
+ """
306
+ global _metrics_collector
307
+ _metrics_collector = MetricsCollector(enabled=enabled)
308
+ return _metrics_collector
309
+
310
+
311
+ def get_metrics_collector() -> MetricsCollector:
312
+ """Get the global metrics collector instance.
313
+
314
+ Returns:
315
+ The metrics collector, or a disabled one if not initialized
316
+ """
317
+ global _metrics_collector
318
+ if _metrics_collector is None:
319
+ _metrics_collector = MetricsCollector(enabled=False)
320
+ return _metrics_collector
@@ -0,0 +1,12 @@
1
+ """Metrics registry for Golf MCP servers."""
2
+
3
+ from golf.metrics.collector import init_metrics_collector
4
+
5
+
6
+ def init_metrics(enabled: bool = False) -> None:
7
+ """Initialize the metrics system.
8
+
9
+ Args:
10
+ enabled: Whether to enable metrics collection
11
+ """
12
+ init_metrics_collector(enabled=enabled)
@@ -0,0 +1,23 @@
1
+ """Golf telemetry module for OpenTelemetry instrumentation."""
2
+
3
+ from golf.telemetry.instrumentation import (
4
+ get_tracer,
5
+ init_telemetry,
6
+ instrument_elicitation,
7
+ instrument_prompt,
8
+ instrument_resource,
9
+ instrument_sampling,
10
+ instrument_tool,
11
+ telemetry_lifespan,
12
+ )
13
+
14
+ __all__ = [
15
+ "instrument_tool",
16
+ "instrument_resource",
17
+ "instrument_prompt",
18
+ "instrument_elicitation",
19
+ "instrument_sampling",
20
+ "telemetry_lifespan",
21
+ "init_telemetry",
22
+ "get_tracer",
23
+ ]