golf-mcp 0.1.16__tar.gz → 0.1.17__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 (75) hide show
  1. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/.docs/docs.md +2 -2
  2. {golf_mcp-0.1.16/src/golf_mcp.egg-info → golf_mcp-0.1.17}/PKG-INFO +8 -3
  3. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/README.md +6 -1
  4. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/pyproject.toml +4 -4
  5. golf_mcp-0.1.17/src/golf/__init__.py +1 -0
  6. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/cli/main.py +13 -2
  7. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/commands/init.py +63 -1
  8. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/core/builder.py +139 -60
  9. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/core/config.py +6 -0
  10. golf_mcp-0.1.17/src/golf/core/parser.py +1059 -0
  11. golf_mcp-0.1.17/src/golf/core/platform.py +180 -0
  12. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/core/telemetry.py +28 -8
  13. golf_mcp-0.1.17/src/golf/examples/api_key/.env.example +1 -0
  14. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/api_key/README.md +10 -10
  15. golf_mcp-0.1.17/src/golf/examples/api_key/golf.json +8 -0
  16. golf_mcp-0.1.17/src/golf/examples/basic/.env.example +4 -0
  17. golf_mcp-0.1.17/src/golf/examples/basic/golf.json +8 -0
  18. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/telemetry/instrumentation.py +26 -48
  19. {golf_mcp-0.1.16 → golf_mcp-0.1.17/src/golf_mcp.egg-info}/PKG-INFO +8 -3
  20. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf_mcp.egg-info/SOURCES.txt +1 -0
  21. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf_mcp.egg-info/requires.txt +1 -1
  22. golf_mcp-0.1.16/src/golf/__init__.py +0 -1
  23. golf_mcp-0.1.16/src/golf/core/parser.py +0 -560
  24. golf_mcp-0.1.16/src/golf/examples/api_key/.env.example +0 -5
  25. golf_mcp-0.1.16/src/golf/examples/api_key/golf.json +0 -12
  26. golf_mcp-0.1.16/src/golf/examples/basic/.env.example +0 -5
  27. golf_mcp-0.1.16/src/golf/examples/basic/golf.json +0 -12
  28. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/.docs/fast-mcp.md +0 -0
  29. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/.docs/fastmcp-example-1.py +0 -0
  30. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/.docs/fastmcp-example-2.py +0 -0
  31. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/.docs/mcp.md +0 -0
  32. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/.docs/oauth-implementation.md +0 -0
  33. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/.docs/oauth.md +0 -0
  34. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/LICENSE +0 -0
  35. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/MANIFEST.in +0 -0
  36. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/setup.cfg +0 -0
  37. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/auth/__init__.py +0 -0
  38. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/auth/api_key.py +0 -0
  39. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/auth/helpers.py +0 -0
  40. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/auth/oauth.py +0 -0
  41. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/auth/provider.py +0 -0
  42. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/cli/__init__.py +0 -0
  43. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/commands/__init__.py +0 -0
  44. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/commands/build.py +0 -0
  45. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/commands/run.py +0 -0
  46. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/core/__init__.py +0 -0
  47. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/core/builder_auth.py +0 -0
  48. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/core/builder_telemetry.py +0 -0
  49. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/core/transformer.py +0 -0
  50. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/__init__.py +0 -0
  51. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/api_key/.env +0 -0
  52. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/api_key/pre_build.py +0 -0
  53. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/api_key/tools/issues/create.py +0 -0
  54. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/api_key/tools/issues/list.py +0 -0
  55. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/api_key/tools/repos/list.py +0 -0
  56. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/api_key/tools/search/code.py +0 -0
  57. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/api_key/tools/users/get.py +0 -0
  58. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/.env +0 -0
  59. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/README.md +0 -0
  60. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/pre_build.py +0 -0
  61. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/prompts/welcome.py +0 -0
  62. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/resources/current_time.py +0 -0
  63. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/resources/info.py +0 -0
  64. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/resources/weather/common.py +0 -0
  65. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/resources/weather/current.py +0 -0
  66. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/resources/weather/forecast.py +0 -0
  67. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/tools/github_user.py +0 -0
  68. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/tools/hello.py +0 -0
  69. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/tools/payments/charge.py +0 -0
  70. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/tools/payments/common.py +0 -0
  71. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/examples/basic/tools/payments/refund.py +0 -0
  72. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf/telemetry/__init__.py +0 -0
  73. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf_mcp.egg-info/dependency_links.txt +0 -0
  74. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf_mcp.egg-info/entry_points.txt +0 -0
  75. {golf_mcp-0.1.16 → golf_mcp-0.1.17}/src/golf_mcp.egg-info/top_level.txt +0 -0
@@ -49,7 +49,7 @@ Given a component file `C` with absolute path
49
49
  let `PathRev = [pₙ, …, p₁]` (reverse order of parent dirs under the category). The **ID** is
50
50
 
51
51
  ```
52
- <filename> + ("-" + "-".join(PathRev) if PathRev else "")
52
+ <filename> + ("_" + "_".join(PathRev) if PathRev else "")
53
53
  ```
54
54
 
55
55
  ### Formal Definition (BNF)
@@ -190,7 +190,7 @@ def run(charge_id: str, amount: int) -> dict:
190
190
  from tools.payments.refund import submit as _submit
191
191
 
192
192
  mcp.tool(
193
- name="submit-refund-payments",
193
+ name="submit_refund_payments",
194
194
  description="Submit a refund request to Stripe."
195
195
  )(_submit.run)
196
196
  ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: golf-mcp
3
- Version: 0.1.16
3
+ Version: 0.1.17
4
4
  Summary: Framework for building MCP servers
5
5
  Author-email: Antoni Gmitruk <antoni@golf.dev>
6
6
  License-Expression: Apache-2.0
@@ -21,7 +21,7 @@ Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Requires-Dist: typer>=0.15.4
23
23
  Requires-Dist: rich>=14.0.0
24
- Requires-Dist: fastmcp>=2.0.0
24
+ Requires-Dist: fastmcp<2.6.0,>=2.0.0
25
25
  Requires-Dist: pydantic>=2.11.0
26
26
  Requires-Dist: python-dotenv>=1.1.0
27
27
  Requires-Dist: black>=24.10.0
@@ -128,7 +128,7 @@ A Golf project initialized with `golf init` will have a structure similar to thi
128
128
 
129
129
  - **`golf.json`**: Configures server name, port, transport, telemetry, and other build settings.
130
130
  - **`tools/`**, **`resources/`**, **`prompts/`**: Contain your Python files, each defining a single component. These directories can also contain nested subdirectories to further organize your components (e.g., `tools/payments/charge.py`). The module docstring of each file serves as the component's description.
131
- - Component IDs are automatically derived from their file path. For example, `tools/hello.py` becomes `hello`, and a nested file like `tools/payments/submit.py` would become `submit-payments` (filename, followed by reversed parent directories under the main category, joined by hyphens).
131
+ - Component IDs are automatically derived from their file path. For example, `tools/hello.py` becomes `hello`, and a nested file like `tools/payments/submit.py` would become `submit_payments` (filename, followed by reversed parent directories under the main category, joined by underscores).
132
132
  - **`common.py`** (not shown, but can be placed in subdirectories like `tools/payments/common.py`): Used to share code (clients, models, etc.) among components in the same subdirectory.
133
133
 
134
134
  ## Example: Defining a Tool
@@ -178,6 +178,10 @@ The `golf.json` file is the heart of your Golf project configuration. Here's wha
178
178
  // - "streamable-http": HTTP with streaming support
179
179
  // - "stdio": Standard I/O (for CLI integration)
180
180
 
181
+ // HTTP Transport Configuration (optional)
182
+ "stateless_http": false, // Make streamable-http transport stateless (new session per request)
183
+ // When true, server restarts won't break existing client connections
184
+
181
185
  // Health Check Configuration (optional)
182
186
  "health_check_enabled": false, // Enable health check endpoint for Kubernetes/load balancers
183
187
  "health_check_path": "/health", // HTTP path for health check endpoint
@@ -198,6 +202,7 @@ The `golf.json` file is the heart of your Golf project configuration. Here's wha
198
202
  - `"streamable-http"` provides HTTP streaming for traditional API clients
199
203
  - `"stdio"` enables integration with command-line tools and scripts
200
204
  - **`host` & `port`**: Control where your server listens. Use `"127.0.0.1"` for local development or `"0.0.0.0"` to accept external connections.
205
+ - **`stateless_http`**: When true, makes the streamable-http transport stateless by creating a new session for each request. This ensures that server restarts don't break existing client connections, making the server truly stateless.
201
206
  - **`health_check_enabled`**: When true, enables a health check endpoint for Kubernetes readiness/liveness probes and load balancers
202
207
  - **`health_check_path`**: Customizable path for the health check endpoint (defaults to "/health")
203
208
  - **`health_check_response`**: Customizable response text for successful health checks (defaults to "OK")
@@ -90,7 +90,7 @@ A Golf project initialized with `golf init` will have a structure similar to thi
90
90
 
91
91
  - **`golf.json`**: Configures server name, port, transport, telemetry, and other build settings.
92
92
  - **`tools/`**, **`resources/`**, **`prompts/`**: Contain your Python files, each defining a single component. These directories can also contain nested subdirectories to further organize your components (e.g., `tools/payments/charge.py`). The module docstring of each file serves as the component's description.
93
- - Component IDs are automatically derived from their file path. For example, `tools/hello.py` becomes `hello`, and a nested file like `tools/payments/submit.py` would become `submit-payments` (filename, followed by reversed parent directories under the main category, joined by hyphens).
93
+ - Component IDs are automatically derived from their file path. For example, `tools/hello.py` becomes `hello`, and a nested file like `tools/payments/submit.py` would become `submit_payments` (filename, followed by reversed parent directories under the main category, joined by underscores).
94
94
  - **`common.py`** (not shown, but can be placed in subdirectories like `tools/payments/common.py`): Used to share code (clients, models, etc.) among components in the same subdirectory.
95
95
 
96
96
  ## Example: Defining a Tool
@@ -140,6 +140,10 @@ The `golf.json` file is the heart of your Golf project configuration. Here's wha
140
140
  // - "streamable-http": HTTP with streaming support
141
141
  // - "stdio": Standard I/O (for CLI integration)
142
142
 
143
+ // HTTP Transport Configuration (optional)
144
+ "stateless_http": false, // Make streamable-http transport stateless (new session per request)
145
+ // When true, server restarts won't break existing client connections
146
+
143
147
  // Health Check Configuration (optional)
144
148
  "health_check_enabled": false, // Enable health check endpoint for Kubernetes/load balancers
145
149
  "health_check_path": "/health", // HTTP path for health check endpoint
@@ -160,6 +164,7 @@ The `golf.json` file is the heart of your Golf project configuration. Here's wha
160
164
  - `"streamable-http"` provides HTTP streaming for traditional API clients
161
165
  - `"stdio"` enables integration with command-line tools and scripts
162
166
  - **`host` & `port`**: Control where your server listens. Use `"127.0.0.1"` for local development or `"0.0.0.0"` to accept external connections.
167
+ - **`stateless_http`**: When true, makes the streamable-http transport stateless by creating a new session for each request. This ensures that server restarts don't break existing client connections, making the server truly stateless.
163
168
  - **`health_check_enabled`**: When true, enables a health check endpoint for Kubernetes readiness/liveness probes and load balancers
164
169
  - **`health_check_path`**: Customizable path for the health check endpoint (defaults to "/health")
165
170
  - **`health_check_response`**: Customizable response text for successful health checks (defaults to "OK")
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "golf-mcp"
7
- version = "0.1.16"
7
+ version = "0.1.17"
8
8
  description = "Framework for building MCP servers"
9
9
  authors = [
10
10
  {name = "Antoni Gmitruk", email = "antoni@golf.dev"}
@@ -28,7 +28,7 @@ classifiers = [
28
28
  dependencies = [
29
29
  "typer>=0.15.4",
30
30
  "rich>=14.0.0",
31
- "fastmcp>=2.0.0",
31
+ "fastmcp>=2.0.0,<2.6.0",
32
32
  "pydantic>=2.11.0",
33
33
  "python-dotenv>=1.1.0",
34
34
  "black>=24.10.0",
@@ -64,7 +64,7 @@ golf = ["examples/**/*"]
64
64
 
65
65
  [tool.poetry]
66
66
  name = "golf-mcp"
67
- version = "0.1.16"
67
+ version = "0.1.17"
68
68
  description = "Framework for building MCP servers with zero boilerplate"
69
69
  authors = ["Antoni Gmitruk <antoni@golf.dev>"]
70
70
  license = "Apache-2.0"
@@ -86,7 +86,7 @@ classifiers = [
86
86
 
87
87
  [tool.poetry.dependencies]
88
88
  python = ">=3.8" # Match requires-python
89
- fastmcp = ">=2.0.0"
89
+ fastmcp = ">=2.0.0,<2.6.0"
90
90
  typer = {extras = ["all"], version = ">=0.15.4"}
91
91
  pydantic = ">=2.11.0"
92
92
  rich = ">=14.0.0"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.17"
@@ -121,7 +121,11 @@ def build_dev(
121
121
  None, "--output-dir", "-o", help="Directory to output the built project"
122
122
  ),
123
123
  ) -> None:
124
- """Build a development version with environment variables copied."""
124
+ """Build a development version with app environment variables copied.
125
+
126
+ Golf credentials (GOLF_*) are always loaded from .env for build operations.
127
+ All environment variables are copied to the built project for development.
128
+ """
125
129
  # Find project root directory
126
130
  project_root, config_path = find_project_root()
127
131
 
@@ -173,7 +177,14 @@ def build_prod(
173
177
  None, "--output-dir", "-o", help="Directory to output the built project"
174
178
  ),
175
179
  ) -> None:
176
- """Build a production version without copying environment variables."""
180
+ """Build a production version for deployment.
181
+
182
+ Golf credentials (GOLF_*) are always loaded from .env for build operations
183
+ (platform registration, resource updates). App environment variables are
184
+ NOT copied for security - provide them in your deployment environment.
185
+
186
+ Your production deployment must include GOLF_* vars for runtime telemetry.
187
+ """
177
188
  # Find project root directory
178
189
  project_root, config_path = find_project_root()
179
190
 
@@ -7,7 +7,12 @@ from rich.console import Console
7
7
  from rich.progress import Progress, SpinnerColumn, TextColumn
8
8
  from rich.prompt import Confirm
9
9
 
10
- from golf.core.telemetry import track_command, track_event
10
+ from golf.core.telemetry import (
11
+ track_command,
12
+ track_event,
13
+ set_telemetry_enabled,
14
+ load_telemetry_preference,
15
+ )
11
16
 
12
17
  console = Console()
13
18
 
@@ -95,6 +100,9 @@ def initialize_project(
95
100
  # Copy directory structure
96
101
  _copy_template(template_dir, output_dir, project_name)
97
102
 
103
+ # Ask for telemetry consent
104
+ _prompt_for_telemetry_consent()
105
+
98
106
  # Create virtual environment
99
107
  console.print("[bold green]Project initialized successfully![/bold green]")
100
108
  console.print("\nTo get started, run:")
@@ -207,6 +215,60 @@ def _copy_template(source_dir: Path, target_dir: Path, project_name: str) -> Non
207
215
  f.write("dist/\n")
208
216
 
209
217
 
218
+ def _prompt_for_telemetry_consent() -> None:
219
+ """Prompt user for telemetry consent and save their preference."""
220
+ import os
221
+
222
+ # Skip prompt in test mode, when telemetry is explicitly disabled, or if preference already exists
223
+ if os.environ.get("GOLF_TEST_MODE", "").lower() in ("1", "true", "yes", "on"):
224
+ return
225
+
226
+ # Skip if telemetry is explicitly disabled in environment
227
+ if os.environ.get("GOLF_TELEMETRY", "").lower() in ("0", "false", "no", "off"):
228
+ return
229
+
230
+ # Check if user already has a saved preference
231
+ existing_preference = load_telemetry_preference()
232
+ if existing_preference is not None:
233
+ return # User already made a choice
234
+
235
+ console.print()
236
+ console.rule("[bold blue]Anonymous usage analytics[/bold blue]", style="blue")
237
+ console.print()
238
+ console.print(
239
+ "Golf can collect [bold]anonymous usage analytics[/bold] to help improve the tool."
240
+ )
241
+ console.print()
242
+ console.print("[dim]What we collect:[/dim]")
243
+ console.print(" • Command usage (init, build, run)")
244
+ console.print(" • Error types (to fix bugs)")
245
+ console.print(" • Golf version and Python version")
246
+ console.print(" • Operating system type")
247
+ console.print()
248
+ console.print("[dim]What we DON'T collect:[/dim]")
249
+ console.print(" • Your code or project content")
250
+ console.print(" • File paths or project names")
251
+ console.print(" • Personal information")
252
+ console.print(" • IP addresses")
253
+ console.print()
254
+ console.print(
255
+ "You can change this anytime by setting GOLF_TELEMETRY=0 in your environment."
256
+ )
257
+ console.print()
258
+
259
+ enable_telemetry = Confirm.ask(
260
+ "[bold]Enable anonymous usage analytics?[/bold]", default=False
261
+ )
262
+
263
+ set_telemetry_enabled(enable_telemetry, persist=True)
264
+
265
+ if enable_telemetry:
266
+ console.print("[green]✓[/green] Anonymous analytics enabled")
267
+ else:
268
+ console.print("[yellow]○[/yellow] Anonymous analytics disabled")
269
+ console.print()
270
+
271
+
210
272
  def _is_text_file(path: Path) -> bool:
211
273
  """Check if a file is a text file that needs variable substitution.
212
274
 
@@ -561,14 +561,6 @@ class CodeGenerator:
561
561
  # Add OpenTelemetry imports if enabled
562
562
  if self.settings.opentelemetry_enabled:
563
563
  imports.extend(generate_telemetry_imports())
564
- imports.append("")
565
-
566
- # Add imports section for different transport methods
567
- if self.settings.transport == "sse" or self.settings.transport in [
568
- "streamable-http",
569
- "http",
570
- ]:
571
- imports.append("import uvicorn")
572
564
 
573
565
  # Add health check imports if enabled
574
566
  if self.settings.health_check_enabled:
@@ -664,7 +656,10 @@ class CodeGenerator:
664
656
  # Add code to register this component
665
657
  if self.settings.opentelemetry_enabled:
666
658
  # Use telemetry instrumentation
667
- registration = f"# Register the {component_type.value} '{component.name}' with telemetry"
659
+ registration = (
660
+ f"# Register the {component_type.value} "
661
+ f"'{component.name}' with telemetry"
662
+ )
668
663
  entry_func = (
669
664
  component.entry_function
670
665
  if hasattr(component, "entry_function")
@@ -672,19 +667,31 @@ class CodeGenerator:
672
667
  else "export"
673
668
  )
674
669
 
675
- # Debug: Add logging to verify wrapping
676
- registration += f"\n_wrapped_func = instrument_{component_type.value}({full_module_path}.{entry_func}, '{component.name}')"
670
+ registration += (
671
+ f"\n_wrapped_func = instrument_{component_type.value}("
672
+ f"{full_module_path}.{entry_func}, '{component.name}')"
673
+ )
677
674
 
678
675
  if component_type == ComponentType.TOOL:
679
- registration += f'\nmcp.add_tool(_wrapped_func, name="{component.name}", description="{component.docstring or ""}"'
676
+ registration += (
677
+ f'\nmcp.add_tool(_wrapped_func, name="{component.name}", '
678
+ f'description="{component.docstring or ""}"'
679
+ )
680
680
  # Add annotations if present
681
681
  if hasattr(component, "annotations") and component.annotations:
682
682
  registration += f", annotations={component.annotations}"
683
683
  registration += ")"
684
684
  elif component_type == ComponentType.RESOURCE:
685
- registration += f'\nmcp.add_resource_fn(_wrapped_func, uri="{component.uri_template}", name="{component.name}", description="{component.docstring or ""}")'
685
+ registration += (
686
+ f"\nmcp.add_resource_fn(_wrapped_func, "
687
+ f'uri="{component.uri_template}", name="{component.name}", '
688
+ f'description="{component.docstring or ""}")'
689
+ )
686
690
  else: # PROMPT
687
- registration += f'\nmcp.add_prompt(_wrapped_func, name="{component.name}", description="{component.docstring or ""}")'
691
+ registration += (
692
+ f'\nmcp.add_prompt(_wrapped_func, name="{component.name}", '
693
+ f'description="{component.docstring or ""}")'
694
+ )
688
695
  else:
689
696
  # Standard registration without telemetry
690
697
  if component_type == ComponentType.TOOL:
@@ -795,6 +802,10 @@ class CodeGenerator:
795
802
  for key, value in auth_components["fastmcp_args"].items():
796
803
  mcp_constructor_args.append(f"{key}={value}")
797
804
 
805
+ # Add stateless HTTP parameter if enabled
806
+ if self.settings.stateless_http:
807
+ mcp_constructor_args.append("stateless_http=True")
808
+
798
809
  # Add OpenTelemetry parameters if enabled
799
810
  if self.settings.opentelemetry_enabled:
800
811
  mcp_constructor_args.append("lifespan=telemetry_lifespan")
@@ -803,6 +814,18 @@ class CodeGenerator:
803
814
  server_code_lines.append(mcp_instance_line)
804
815
  server_code_lines.append("")
805
816
 
817
+ # Add early telemetry initialization if enabled (before component registration)
818
+ early_telemetry_init = []
819
+ if self.settings.opentelemetry_enabled:
820
+ early_telemetry_init.extend(
821
+ [
822
+ "# Initialize telemetry early to ensure instrumentation works",
823
+ "from golf.telemetry.instrumentation import init_telemetry",
824
+ f'init_telemetry("{self.settings.name}")',
825
+ "",
826
+ ]
827
+ )
828
+
806
829
  # Main entry point with transport-specific app initialization
807
830
  main_code = [
808
831
  'if __name__ == "__main__":',
@@ -830,71 +853,84 @@ class CodeGenerator:
830
853
 
831
854
  # Transport-specific run methods
832
855
  if self.settings.transport == "sse":
833
- # Check if we need to add API key middleware for SSE
856
+ # Check if we need middleware for SSE
857
+ middleware_setup = []
858
+ middleware_list = []
859
+
834
860
  api_key_config = get_api_key_config()
835
861
  if auth_components.get("has_auth") and api_key_config:
836
- main_code.extend(
837
- [
838
- " # For SSE with API key auth, we need to get the app and add middleware",
839
- ' app = mcp.http_app(transport="sse")',
840
- " app.add_middleware(ApiKeyMiddleware)",
841
- ]
862
+ middleware_setup.append(
863
+ " from starlette.middleware import Middleware"
842
864
  )
843
- else:
865
+ middleware_list.append("Middleware(ApiKeyMiddleware)")
866
+
867
+ # Add OpenTelemetry middleware if enabled
868
+ if self.settings.opentelemetry_enabled:
869
+ middleware_setup.append(
870
+ " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware"
871
+ )
872
+ middleware_setup.append(
873
+ " from starlette.middleware import Middleware"
874
+ )
875
+ middleware_list.append("Middleware(OpenTelemetryMiddleware)")
876
+
877
+ if middleware_setup:
878
+ main_code.extend(middleware_setup)
879
+ main_code.append(f" middleware = [{', '.join(middleware_list)}]")
880
+ main_code.append("")
844
881
  main_code.extend(
845
882
  [
846
- " # For SSE, get the app to add middleware",
847
- ' app = mcp.http_app(transport="sse")',
883
+ " # Run SSE server with middleware using FastMCP's run method",
884
+ ' mcp.run(transport="sse", host=host, port=port, log_level="info", middleware=middleware)',
848
885
  ]
849
886
  )
850
-
851
- # Add OpenTelemetry middleware to the SSE app if enabled
852
- if self.settings.opentelemetry_enabled:
887
+ else:
853
888
  main_code.extend(
854
889
  [
855
- " # Apply OpenTelemetry middleware to the SSE app",
856
- " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware",
857
- " app = OpenTelemetryMiddleware(app)",
890
+ " # Run SSE server using FastMCP's run method",
891
+ ' mcp.run(transport="sse", host=host, port=port, log_level="info")',
858
892
  ]
859
893
  )
860
894
 
861
- main_code.extend(
862
- [
863
- " # Run with the configured app",
864
- ' uvicorn.run(app, host=host, port=port, log_level="info")',
865
- ]
866
- )
867
895
  elif self.settings.transport in ["streamable-http", "http"]:
868
- main_code.extend(
869
- [
870
- " # Create HTTP app and run with uvicorn",
871
- " app = mcp.http_app()",
872
- ]
873
- )
896
+ # Check if we need middleware for streamable-http
897
+ middleware_setup = []
898
+ middleware_list = []
874
899
 
875
- # Check if we need to add API key middleware
876
900
  api_key_config = get_api_key_config()
877
901
  if auth_components.get("has_auth") and api_key_config:
902
+ middleware_setup.append(
903
+ " from starlette.middleware import Middleware"
904
+ )
905
+ middleware_list.append("Middleware(ApiKeyMiddleware)")
906
+
907
+ # Add OpenTelemetry middleware if enabled
908
+ if self.settings.opentelemetry_enabled:
909
+ middleware_setup.append(
910
+ " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware"
911
+ )
912
+ middleware_setup.append(
913
+ " from starlette.middleware import Middleware"
914
+ )
915
+ middleware_list.append("Middleware(OpenTelemetryMiddleware)")
916
+
917
+ if middleware_setup:
918
+ main_code.extend(middleware_setup)
919
+ main_code.append(f" middleware = [{', '.join(middleware_list)}]")
920
+ main_code.append("")
878
921
  main_code.extend(
879
922
  [
880
- " # Add API key middleware",
881
- " app.add_middleware(ApiKeyMiddleware)",
923
+ " # Run HTTP server with middleware using FastMCP's run method",
924
+ ' mcp.run(transport="streamable-http", host=host, port=port, log_level="info", middleware=middleware)',
882
925
  ]
883
926
  )
884
-
885
- # Add OpenTelemetry middleware to the HTTP app if enabled
886
- if self.settings.opentelemetry_enabled:
927
+ else:
887
928
  main_code.extend(
888
929
  [
889
- " # Apply OpenTelemetry middleware to the HTTP app",
890
- " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware",
891
- " app = OpenTelemetryMiddleware(app)",
930
+ " # Run HTTP server using FastMCP's run method",
931
+ ' mcp.run(transport="streamable-http", host=host, port=port, log_level="info")',
892
932
  ]
893
933
  )
894
-
895
- main_code.extend(
896
- [' uvicorn.run(app, host=host, port=port, log_level="info")']
897
- )
898
934
  else:
899
935
  # For stdio transport, use mcp.run()
900
936
  main_code.extend(
@@ -917,12 +953,13 @@ class CodeGenerator:
917
953
 
918
954
  # Combine all sections
919
955
  # Order: imports, env_section, auth_setup, server_code (mcp init),
920
- # post_init (API key middleware), component_registrations, main_code (run block)
956
+ # early_telemetry_init, component_registrations, health_check_code, main_code (run block)
921
957
  code = "\n".join(
922
958
  imports
923
959
  + env_section
924
960
  + auth_setup_code
925
961
  + server_code_lines
962
+ + early_telemetry_init
926
963
  + component_registrations
927
964
  + health_check_code
928
965
  + main_code
@@ -955,6 +992,21 @@ def build_project(
955
992
  build_env: Build environment ('dev' or 'prod')
956
993
  copy_env: Whether to copy environment variables to the built app
957
994
  """
995
+ # Load Golf credentials from .env for build operations (platform registration, etc.)
996
+ # This happens regardless of copy_env setting to ensure build process works
997
+ from dotenv import load_dotenv
998
+
999
+ project_env_file = project_path / ".env"
1000
+ if project_env_file.exists():
1001
+ # Load GOLF_* variables for build process
1002
+ load_dotenv(project_env_file, override=False)
1003
+
1004
+ # Only log if we actually found the specific Golf platform credentials
1005
+ has_api_key = "GOLF_API_KEY" in os.environ
1006
+ has_server_id = "GOLF_SERVER_ID" in os.environ
1007
+ if has_api_key and has_server_id:
1008
+ console.print("[dim]Loaded Golf credentials for build operations[/dim]")
1009
+
958
1010
  # Execute pre_build.py if it exists
959
1011
  pre_build_path = project_path / "pre_build.py"
960
1012
  if pre_build_path.exists():
@@ -1056,9 +1108,6 @@ def build_project(
1056
1108
  env_vars_to_write["OTEL_TRACES_EXPORTER"] = (
1057
1109
  settings.opentelemetry_default_exporter
1058
1110
  )
1059
- console.print(
1060
- f"[info]Setting OTEL_TRACES_EXPORTER to '{settings.opentelemetry_default_exporter}' from golf.json in built app's .env[/info]"
1061
- )
1062
1111
 
1063
1112
  # 3. Apply Golf's project name as OTEL_SERVICE_NAME if not already set
1064
1113
  # (Ensures service name defaults to project name if not specified in user's .env)
@@ -1110,6 +1159,36 @@ def build_project(
1110
1159
  )
1111
1160
  generator.generate()
1112
1161
 
1162
+ # Platform registration (only for prod builds)
1163
+ if build_env == "prod":
1164
+ console.print(
1165
+ "[dim]Registering with Golf platform and updating resources...[/dim]"
1166
+ )
1167
+ import asyncio
1168
+
1169
+ try:
1170
+ from golf.core.platform import register_project_with_platform
1171
+
1172
+ asyncio.run(
1173
+ register_project_with_platform(
1174
+ project_path=project_path,
1175
+ settings=settings,
1176
+ components=generator.components,
1177
+ )
1178
+ )
1179
+ console.print("[green]✓ Platform registration completed[/green]")
1180
+ except ImportError:
1181
+ console.print(
1182
+ "[yellow]Warning: Platform registration module not available[/yellow]"
1183
+ )
1184
+ except Exception as e:
1185
+ console.print(
1186
+ f"[yellow]Warning: Platform registration failed: {e}[/yellow]"
1187
+ )
1188
+ console.print(
1189
+ "[yellow]Tip: Ensure GOLF_API_KEY and GOLF_SERVER_ID are available in your .env file[/yellow]"
1190
+ )
1191
+
1113
1192
  # Create a simple README
1114
1193
  readme_content = f"""# {settings.name}
1115
1194
 
@@ -1130,7 +1209,7 @@ This is a standalone FastMCP server generated by GolfMCP.
1130
1209
 
1131
1210
  # Copy pyproject.toml with required dependencies
1132
1211
  base_dependencies = [
1133
- "fastmcp>=2.0.0",
1212
+ "fastmcp>=2.0.0,<2.6.0",
1134
1213
  "uvicorn>=0.20.0",
1135
1214
  "pydantic>=2.0.0",
1136
1215
  "python-dotenv>=1.0.0",
@@ -109,6 +109,12 @@ class Settings(BaseSettings):
109
109
  health_check_path: str = Field("/health", description="Health check endpoint path")
110
110
  health_check_response: str = Field("OK", description="Health check response text")
111
111
 
112
+ # HTTP session behaviour
113
+ stateless_http: bool = Field(
114
+ False,
115
+ description="Make Streamable-HTTP transport stateless (new session per request)",
116
+ )
117
+
112
118
 
113
119
  def find_config_path(start_path: Path | None = None) -> Path | None:
114
120
  """Find the golf config file by searching upwards from the given path.