golf-mcp 0.1.8__tar.gz → 0.1.10__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 (70) hide show
  1. {golf_mcp-0.1.8/src/golf_mcp.egg-info → golf_mcp-0.1.10}/PKG-INFO +1 -1
  2. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/pyproject.toml +2 -2
  3. golf_mcp-0.1.10/src/golf/__init__.py +1 -0
  4. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/auth/__init__.py +1 -1
  5. golf_mcp-0.1.10/src/golf/auth/helpers.py +207 -0
  6. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/core/builder.py +33 -17
  7. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/core/builder_auth.py +31 -91
  8. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/core/telemetry.py +26 -30
  9. {golf_mcp-0.1.8/src/golf/examples/basic → golf_mcp-0.1.10/src/golf/examples/api_key}/.env.example +1 -1
  10. {golf_mcp-0.1.8 → golf_mcp-0.1.10/src/golf_mcp.egg-info}/PKG-INFO +1 -1
  11. golf_mcp-0.1.8/src/golf/__init__.py +0 -1
  12. golf_mcp-0.1.8/src/golf/auth/helpers.py +0 -97
  13. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/.docs/docs.md +0 -0
  14. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/.docs/fast-mcp.md +0 -0
  15. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/.docs/fastmcp-example-1.py +0 -0
  16. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/.docs/fastmcp-example-2.py +0 -0
  17. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/.docs/mcp.md +0 -0
  18. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/.docs/oauth-implementation.md +0 -0
  19. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/.docs/oauth.md +0 -0
  20. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/LICENSE +0 -0
  21. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/MANIFEST.in +0 -0
  22. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/README.md +0 -0
  23. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/setup.cfg +0 -0
  24. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/auth/api_key.py +0 -0
  25. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/auth/oauth.py +0 -0
  26. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/auth/provider.py +0 -0
  27. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/cli/__init__.py +0 -0
  28. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/cli/main.py +0 -0
  29. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/commands/__init__.py +0 -0
  30. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/commands/build.py +0 -0
  31. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/commands/init.py +0 -0
  32. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/commands/run.py +0 -0
  33. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/core/__init__.py +0 -0
  34. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/core/builder_telemetry.py +0 -0
  35. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/core/config.py +0 -0
  36. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/core/parser.py +0 -0
  37. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/core/transformer.py +0 -0
  38. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/__init__.py +0 -0
  39. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/api_key/.env +0 -0
  40. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/api_key/README.md +0 -0
  41. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/api_key/golf.json +0 -0
  42. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/api_key/pre_build.py +0 -0
  43. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/api_key/tools/issues/create.py +0 -0
  44. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/api_key/tools/issues/list.py +0 -0
  45. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/api_key/tools/repos/list.py +0 -0
  46. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/api_key/tools/search/code.py +0 -0
  47. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/api_key/tools/users/get.py +0 -0
  48. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/.env +0 -0
  49. {golf_mcp-0.1.8/src/golf/examples/api_key → golf_mcp-0.1.10/src/golf/examples/basic}/.env.example +0 -0
  50. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/README.md +0 -0
  51. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/golf.json +0 -0
  52. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/pre_build.py +0 -0
  53. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/prompts/welcome.py +0 -0
  54. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/resources/current_time.py +0 -0
  55. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/resources/info.py +0 -0
  56. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/resources/weather/common.py +0 -0
  57. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/resources/weather/current.py +0 -0
  58. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/resources/weather/forecast.py +0 -0
  59. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/tools/github_user.py +0 -0
  60. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/tools/hello.py +0 -0
  61. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/tools/payments/charge.py +0 -0
  62. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/tools/payments/common.py +0 -0
  63. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/examples/basic/tools/payments/refund.py +0 -0
  64. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/telemetry/__init__.py +0 -0
  65. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf/telemetry/instrumentation.py +0 -0
  66. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf_mcp.egg-info/SOURCES.txt +0 -0
  67. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf_mcp.egg-info/dependency_links.txt +0 -0
  68. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf_mcp.egg-info/entry_points.txt +0 -0
  69. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/src/golf_mcp.egg-info/requires.txt +0 -0
  70. {golf_mcp-0.1.8 → golf_mcp-0.1.10}/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.8
3
+ Version: 0.1.10
4
4
  Summary: Framework for building MCP servers
5
5
  Author-email: Antoni Gmitruk <antoni@golf.dev>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "golf-mcp"
7
- version = "0.1.8"
7
+ version = "0.1.10"
8
8
  description = "Framework for building MCP servers"
9
9
  authors = [
10
10
  {name = "Antoni Gmitruk", email = "antoni@golf.dev"}
@@ -64,7 +64,7 @@ golf = ["examples/**/*"]
64
64
 
65
65
  [tool.poetry]
66
66
  name = "golf-mcp"
67
- version = "0.1.8"
67
+ version = "0.1.10"
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"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.10"
@@ -11,7 +11,7 @@ from mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions
11
11
 
12
12
  from .provider import ProviderConfig
13
13
  from .oauth import GolfOAuthProvider, create_callback_handler
14
- from .helpers import get_access_token, get_provider_token, extract_token_from_header, get_api_key, set_api_key
14
+ from .helpers import get_access_token, get_provider_token, extract_token_from_header, get_api_key, set_api_key, debug_api_key_context
15
15
  from .api_key import configure_api_key, get_api_key_config, is_api_key_configured
16
16
 
17
17
  class AuthConfig:
@@ -0,0 +1,207 @@
1
+ """Helper functions for working with authentication in MCP context."""
2
+
3
+ from typing import Optional, Dict, Any
4
+ from contextvars import ContextVar
5
+
6
+ # Re-export get_access_token from the MCP SDK
7
+ from mcp.server.auth.middleware.auth_context import get_access_token
8
+
9
+ from .oauth import GolfOAuthProvider
10
+
11
+ # Context variable to store the active OAuth provider
12
+ _active_golf_oauth_provider: Optional[GolfOAuthProvider] = None
13
+
14
+ # Context variable to store the current request's API key
15
+ _current_api_key: ContextVar[Optional[str]] = ContextVar('current_api_key', default=None)
16
+
17
+ def _set_active_golf_oauth_provider(provider: GolfOAuthProvider) -> None:
18
+ """
19
+ Sets the active GolfOAuthProvider instance.
20
+ Should only be called once during server startup.
21
+ """
22
+ global _active_golf_oauth_provider
23
+ _active_golf_oauth_provider = provider
24
+
25
+ def get_provider_token() -> Optional[str]:
26
+ """
27
+ Get a provider token (e.g., GitHub token) associated with the current
28
+ MCP session's access token.
29
+
30
+ This relies on _set_active_golf_oauth_provider being called at server startup.
31
+ """
32
+ mcp_access_token = get_access_token() # From MCP SDK, uses its own ContextVar
33
+ if not mcp_access_token:
34
+ # No active MCP session token.
35
+ return None
36
+
37
+ provider = _active_golf_oauth_provider
38
+ if not provider:
39
+ return None
40
+
41
+ if not hasattr(provider, "get_provider_token"):
42
+ return None
43
+
44
+ # Call the get_provider_token method on the actual GolfOAuthProvider instance
45
+ return provider.get_provider_token(mcp_access_token.token)
46
+
47
+ def extract_token_from_header(auth_header: str) -> Optional[str]:
48
+ """Extract bearer token from Authorization header.
49
+
50
+ Args:
51
+ auth_header: Authorization header value
52
+
53
+ Returns:
54
+ Bearer token or None if not present/valid
55
+ """
56
+ if not auth_header:
57
+ return None
58
+
59
+ parts = auth_header.split()
60
+ if len(parts) != 2 or parts[0].lower() != 'bearer':
61
+ return None
62
+
63
+ return parts[1]
64
+
65
+ def set_api_key(api_key: Optional[str]) -> None:
66
+ """Set the API key for the current request context.
67
+
68
+ This is an internal function used by the middleware.
69
+
70
+ Args:
71
+ api_key: The API key to store in the context
72
+ """
73
+ _current_api_key.set(api_key)
74
+
75
+ def get_api_key() -> Optional[str]:
76
+ """Get the API key from the current request context.
77
+
78
+ This function should be used in tools to retrieve the API key
79
+ that was sent in the request headers.
80
+
81
+ Returns:
82
+ The API key if available, None otherwise
83
+
84
+ Example:
85
+ # In a tool file
86
+ from golf.auth import get_api_key
87
+
88
+ async def call_api():
89
+ api_key = get_api_key()
90
+ if not api_key:
91
+ return {"error": "No API key provided"}
92
+
93
+ # Use the API key in your request
94
+ headers = {"Authorization": f"Bearer {api_key}"}
95
+ ...
96
+ """
97
+ # Try to get directly from HTTP request if available (FastMCP pattern)
98
+ try:
99
+ # This follows the FastMCP pattern for accessing HTTP requests
100
+ from fastmcp.server.dependencies import get_http_request
101
+ request = get_http_request()
102
+
103
+ if request and hasattr(request, 'state') and hasattr(request.state, 'api_key'):
104
+ api_key = request.state.api_key
105
+ return api_key
106
+
107
+ # Get the API key configuration
108
+ from golf.auth.api_key import get_api_key_config
109
+ api_key_config = get_api_key_config()
110
+
111
+ if api_key_config and request:
112
+ # Extract API key from headers
113
+ header_name = api_key_config.header_name
114
+ header_prefix = api_key_config.header_prefix
115
+
116
+ # Case-insensitive header lookup
117
+ api_key = None
118
+ for k, v in request.headers.items():
119
+ if k.lower() == header_name.lower():
120
+ api_key = v
121
+ break
122
+
123
+ # Strip prefix if configured
124
+ if api_key and header_prefix and api_key.startswith(header_prefix):
125
+ api_key = api_key[len(header_prefix):]
126
+
127
+ if api_key:
128
+ return api_key
129
+ except (ImportError, RuntimeError) as e:
130
+ # FastMCP not available or not in HTTP context
131
+ pass
132
+ except Exception as e:
133
+ pass
134
+
135
+ # Final fallback: environment variable (for development/testing)
136
+ import os
137
+ env_api_key = os.environ.get('API_KEY')
138
+ if env_api_key:
139
+ return env_api_key
140
+
141
+ return None
142
+
143
+ def get_api_key_from_request(request) -> Optional[str]:
144
+ """Get the API key from a specific request object.
145
+
146
+ This is useful when you have direct access to the request object.
147
+
148
+ Args:
149
+ request: The Starlette Request object
150
+
151
+ Returns:
152
+ The API key if available, None otherwise
153
+ """
154
+ # Check request state first (set by our middleware)
155
+ if hasattr(request, 'state') and hasattr(request.state, 'api_key'):
156
+ return request.state.api_key
157
+
158
+ # Fall back to context variable
159
+ return _current_api_key.get()
160
+
161
+ def debug_api_key_context() -> Dict[str, Any]:
162
+ """Debug function to inspect API key context.
163
+
164
+ Returns a dictionary with debugging information about the current
165
+ API key context. Useful for troubleshooting authentication issues.
166
+
167
+ Returns:
168
+ Dictionary with debug information
169
+ """
170
+ import asyncio
171
+ import sys
172
+ import os
173
+
174
+ debug_info = {
175
+ "context_var_value": _current_api_key.get(),
176
+ "has_async_task": False,
177
+ "task_id": None,
178
+ "main_module_has_storage": False,
179
+ "main_module_has_context": False,
180
+ "request_id_from_context": None,
181
+ "env_vars": {
182
+ "API_KEY": bool(os.environ.get('API_KEY')),
183
+ "GOLF_API_KEY_DEBUG": os.environ.get('GOLF_API_KEY_DEBUG', 'false')
184
+ }
185
+ }
186
+
187
+ try:
188
+ task = asyncio.current_task()
189
+ if task:
190
+ debug_info["has_async_task"] = True
191
+ debug_info["task_id"] = id(task)
192
+ except:
193
+ pass
194
+
195
+ try:
196
+ main_module = sys.modules.get('__main__')
197
+ if main_module:
198
+ debug_info["main_module_has_storage"] = hasattr(main_module, 'api_key_storage')
199
+ debug_info["main_module_has_context"] = hasattr(main_module, 'request_id_context')
200
+
201
+ if hasattr(main_module, 'request_id_context'):
202
+ request_id_context = getattr(main_module, 'request_id_context')
203
+ debug_info["request_id_from_context"] = request_id_context.get()
204
+ except:
205
+ pass
206
+
207
+ return debug_info
@@ -20,6 +20,7 @@ from golf.core.parser import (
20
20
  from golf.core.transformer import transform_component
21
21
  from golf.core.builder_auth import generate_auth_code, generate_auth_routes
22
22
  from golf.auth import get_auth_config
23
+ from golf.auth.api_key import get_api_key_config
23
24
  from golf.core.builder_telemetry import (
24
25
  generate_telemetry_imports,
25
26
  get_otel_dependencies
@@ -548,8 +549,7 @@ class CodeGenerator:
548
549
  # Add imports section for different transport methods
549
550
  if self.settings.transport == "sse":
550
551
  imports.append("import uvicorn")
551
- imports.append("from fastmcp.server.http import create_sse_app")
552
- elif self.settings.transport != "stdio":
552
+ elif self.settings.transport in ["streamable-http", "http"]:
553
553
  imports.append("import uvicorn")
554
554
 
555
555
  # Get transport-specific configuration
@@ -735,12 +735,6 @@ class CodeGenerator:
735
735
  server_code_lines.append(mcp_instance_line)
736
736
  server_code_lines.append("")
737
737
 
738
- # Add any post-init code from auth
739
- post_init_code = []
740
- if auth_components.get("has_auth") and auth_components.get("post_init_code"):
741
- post_init_code.extend(auth_components["post_init_code"])
742
- post_init_code.append("")
743
-
744
738
  # Main entry point with transport-specific app initialization
745
739
  main_code = [
746
740
  "if __name__ == \"__main__\":",
@@ -764,16 +758,35 @@ class CodeGenerator:
764
758
 
765
759
  # Transport-specific run methods
766
760
  if self.settings.transport == "sse":
767
- main_code.extend([
768
- " # For SSE, FastMCP's run method handles auth integration better",
769
- " mcp.run(transport=\"sse\", host=host, port=port, log_level=\"info\")"
770
- ])
771
- elif self.settings.transport == "streamable-http":
761
+ # Check if we need to add API key middleware for SSE
762
+ api_key_config = get_api_key_config()
763
+ if auth_components.get("has_auth") and api_key_config:
764
+ main_code.extend([
765
+ " # For SSE with API key auth, we need to get the app and add middleware",
766
+ " app = mcp.http_app(transport=\"sse\")",
767
+ " app.add_middleware(ApiKeyMiddleware)",
768
+ " # Run with the configured app",
769
+ " uvicorn.run(app, host=host, port=port, log_level=\"info\")"
770
+ ])
771
+ else:
772
+ main_code.extend([
773
+ " # For SSE, FastMCP's run method handles auth integration better",
774
+ " mcp.run(transport=\"sse\", host=host, port=port, log_level=\"info\")"
775
+ ])
776
+ elif self.settings.transport in ["streamable-http", "http"]:
772
777
  main_code.extend([
773
778
  " # Create HTTP app and run with uvicorn",
774
779
  " app = mcp.http_app()",
775
780
  ])
776
781
 
782
+ # Check if we need to add API key middleware
783
+ api_key_config = get_api_key_config()
784
+ if auth_components.get("has_auth") and api_key_config:
785
+ main_code.extend([
786
+ " # Add API key middleware",
787
+ " app.add_middleware(ApiKeyMiddleware)",
788
+ ])
789
+
777
790
  # Add OpenTelemetry middleware to the HTTP app if enabled
778
791
  if self.settings.opentelemetry_enabled:
779
792
  main_code.extend([
@@ -800,7 +813,6 @@ class CodeGenerator:
800
813
  env_section +
801
814
  auth_setup_code +
802
815
  server_code_lines +
803
- post_init_code +
804
816
  component_registrations +
805
817
  main_code
806
818
  )
@@ -1116,9 +1128,13 @@ def build_import_map(project_path: Path, common_files: Dict[str, Path]) -> Dict[
1116
1128
  try:
1117
1129
  rel_to_component = dir_path.relative_to(component_type)
1118
1130
  # Create the new import path
1119
- new_path = f"components.{component_type}.{rel_to_component}".replace("/", ".")
1120
- # Fix any double dots
1121
- new_path = new_path.replace("..", ".")
1131
+ if str(rel_to_component) == ".":
1132
+ # This is at the root of the component type
1133
+ new_path = f"components.{component_type}"
1134
+ else:
1135
+ # Replace path separators with dots
1136
+ path_parts = str(rel_to_component).replace("\\", "/").split("/")
1137
+ new_path = f"components.{component_type}.{'.'.join(path_parts)}"
1122
1138
 
1123
1139
  # Map both the directory and the common file
1124
1140
  orig_module = dir_path_str
@@ -145,7 +145,6 @@ def generate_api_key_auth_components(server_name: str, opentelemetry_enabled: bo
145
145
  - setup_code: Auth setup code (middleware setup)
146
146
  - fastmcp_args: Dict of arguments to add to FastMCP constructor
147
147
  - has_auth: Whether auth is configured
148
- - post_init_code: Code to run after FastMCP instance is created
149
148
  """
150
149
  api_key_config = get_api_key_config()
151
150
  if not api_key_config:
@@ -153,24 +152,35 @@ def generate_api_key_auth_components(server_name: str, opentelemetry_enabled: bo
153
152
  "imports": [],
154
153
  "setup_code": [],
155
154
  "fastmcp_args": {},
156
- "has_auth": False,
157
- "post_init_code": []
155
+ "has_auth": False
158
156
  }
159
157
 
160
158
  auth_imports = [
161
159
  "# API key authentication setup",
162
- "from golf.auth.helpers import set_api_key",
163
- "from golf.auth.api_key import get_api_key_config",
160
+ "from golf.auth.api_key import get_api_key_config, configure_api_key",
161
+ "from golf.auth import set_api_key",
164
162
  "from starlette.middleware.base import BaseHTTPMiddleware",
165
163
  "from starlette.requests import Request",
166
164
  "from starlette.responses import JSONResponse",
165
+ "import os",
167
166
  ]
168
167
 
169
168
  setup_code_lines = [
170
- "# Middleware to extract API key from headers",
169
+ "# Recreate API key configuration from pre_build.py",
170
+ f"configure_api_key(",
171
+ f" header_name={repr(api_key_config.header_name)},",
172
+ f" header_prefix={repr(api_key_config.header_prefix)},",
173
+ f" required={repr(api_key_config.required)}",
174
+ f")",
175
+ "",
176
+ "# Simplified API key middleware that validates presence",
171
177
  "class ApiKeyMiddleware(BaseHTTPMiddleware):",
172
178
  " async def dispatch(self, request: Request, call_next):",
179
+ " # Debug mode from environment",
180
+ " debug = os.environ.get('GOLF_API_KEY_DEBUG', '').lower() == 'true'",
181
+ " ",
173
182
  " api_key_config = get_api_key_config()",
183
+ " ",
174
184
  " if api_key_config:",
175
185
  " # Extract API key from the configured header",
176
186
  " header_name = api_key_config.header_name",
@@ -183,109 +193,39 @@ def generate_api_key_auth_components(server_name: str, opentelemetry_enabled: bo
183
193
  " api_key = v",
184
194
  " break",
185
195
  " ",
186
- " # Check if API key is required and missing",
196
+ " # Process the API key if found",
197
+ " if api_key:",
198
+ " # Strip prefix if configured",
199
+ " if header_prefix and api_key.startswith(header_prefix):",
200
+ " api_key = api_key[len(header_prefix):]",
201
+ " ",
202
+ " # Store the API key in request state for tools to access",
203
+ " request.state.api_key = api_key",
204
+ " ",
205
+ " # Also store in context variable for tools",
206
+ " set_api_key(api_key)",
207
+ " ",
208
+ " # Check if API key is required but missing",
187
209
  " if api_key_config.required and not api_key:",
188
210
  " return JSONResponse(",
189
211
  " {'error': 'unauthorized', 'detail': f'Missing required {header_name} header'},",
190
212
  " status_code=401,",
191
213
  " headers={'WWW-Authenticate': f'{header_name} realm=\"MCP Server\"'}",
192
214
  " )",
193
- " ",
194
- " # Strip prefix if configured and present",
195
- " if api_key and header_prefix and api_key.startswith(header_prefix):",
196
- " api_key = api_key[len(header_prefix):]",
197
- " elif api_key and header_prefix and api_key_config.required:",
198
- " # Has API key but wrong format when required",
199
- " return JSONResponse(",
200
- " {'error': 'unauthorized', 'detail': f'Invalid {header_name} format, expected prefix: {header_prefix}'},",
201
- " status_code=401,",
202
- " headers={'WWW-Authenticate': f'{header_name} realm=\"MCP Server\"'}",
203
- " )",
204
- " ",
205
- " # Store the API key in context for tools to access",
206
- " set_api_key(api_key)",
207
215
  " ",
208
216
  " # Continue with the request",
209
- " response = await call_next(request)",
210
- " return response",
217
+ " return await call_next(request)",
211
218
  "",
212
219
  ]
213
220
 
214
221
  # API key auth is handled via middleware, not FastMCP constructor args
215
222
  fastmcp_args = {}
216
223
 
217
- # Code to run after FastMCP instance is created
218
- # FastMCP doesn't expose .app directly, so we need to use custom_route
219
- # to add a middleware-like functionality
220
- post_init_code = [
221
- "# API key authentication via custom middleware function",
222
- "# Since FastMCP doesn't expose .app, we'll use a different approach",
223
- "import functools",
224
- "from starlette.responses import JSONResponse",
225
- "",
226
- "# Store original method references",
227
- "_original_call_tool = mcp._mcp_call_tool if hasattr(mcp, '_mcp_call_tool') else None",
228
- "_original_read_resource = mcp._mcp_read_resource if hasattr(mcp, '_mcp_read_resource') else None",
229
- "_original_get_prompt = mcp._mcp_get_prompt if hasattr(mcp, '_mcp_get_prompt') else None",
230
- "",
231
- "# Wrapper to extract API key before processing",
232
- "def with_api_key_extraction(original_method):",
233
- " @functools.wraps(original_method)",
234
- " async def wrapper(request, *args, **kwargs):",
235
- " # Extract API key from request headers",
236
- " api_key_config = get_api_key_config()",
237
- " if api_key_config and hasattr(request, 'headers'):",
238
- " header_name = api_key_config.header_name",
239
- " header_prefix = api_key_config.header_prefix",
240
- " ",
241
- " # Case-insensitive header lookup",
242
- " api_key = None",
243
- " for k, v in request.headers.items():",
244
- " if k.lower() == header_name.lower():",
245
- " api_key = v",
246
- " break",
247
- " ",
248
- " # Check if API key is required and missing",
249
- " if api_key_config.required and not api_key:",
250
- " return JSONResponse(",
251
- " {'error': 'unauthorized', 'detail': f'Missing required {header_name} header'},",
252
- " status_code=401,",
253
- " headers={'WWW-Authenticate': f'{header_name} realm=\"MCP Server\"'}",
254
- " )",
255
- " ",
256
- " # Strip prefix if configured and present",
257
- " if api_key and header_prefix and api_key.startswith(header_prefix):",
258
- " api_key = api_key[len(header_prefix):]",
259
- " elif api_key and header_prefix and api_key_config.required:",
260
- " # Has API key but wrong format when required",
261
- " return JSONResponse(",
262
- " {'error': 'unauthorized', 'detail': f'Invalid {header_name} format, expected prefix: {header_prefix}'},",
263
- " status_code=401,",
264
- " headers={'WWW-Authenticate': f'{header_name} realm=\"MCP Server\"'}",
265
- " )",
266
- " ",
267
- " # Store the API key in context for tools to access",
268
- " set_api_key(api_key)",
269
- " ",
270
- " # Call the original method",
271
- " return await original_method(request, *args, **kwargs)",
272
- " return wrapper",
273
- "",
274
- "# Wrap the MCP methods if they exist",
275
- "if _original_call_tool:",
276
- " mcp._mcp_call_tool = with_api_key_extraction(_original_call_tool)",
277
- "if _original_read_resource:",
278
- " mcp._mcp_read_resource = with_api_key_extraction(_original_read_resource)",
279
- "if _original_get_prompt:",
280
- " mcp._mcp_get_prompt = with_api_key_extraction(_original_get_prompt)",
281
- ]
282
-
283
224
  return {
284
225
  "imports": auth_imports,
285
226
  "setup_code": setup_code_lines,
286
227
  "fastmcp_args": fastmcp_args,
287
- "has_auth": True,
288
- "post_init_code": post_init_code
228
+ "has_auth": True
289
229
  }
290
230
 
291
231
 
@@ -7,7 +7,6 @@ from pathlib import Path
7
7
  from typing import Optional, Dict, Any
8
8
  import json
9
9
  import uuid
10
- import getpass
11
10
 
12
11
  import posthog
13
12
  from rich.console import Console
@@ -26,6 +25,7 @@ POSTHOG_HOST = "https://us.i.posthog.com"
26
25
  # Telemetry state
27
26
  _telemetry_enabled: Optional[bool] = None
28
27
  _anonymous_id: Optional[str] = None
28
+ _user_identified: bool = False # Track if we've already identified the user
29
29
 
30
30
 
31
31
  def get_telemetry_config_path() -> Path:
@@ -142,16 +142,10 @@ def get_anonymous_id() -> str:
142
142
  pass
143
143
 
144
144
  # Generate new ID with more unique data
145
- # Include home directory path to differentiate between users on same machine
146
- # Include a random component to ensure uniqueness even with identical setups
145
+ # Use only non-identifying system information
147
146
 
148
- try:
149
- username = getpass.getuser()
150
- except Exception:
151
- username = "unknown"
152
-
153
- # Combine multiple factors for uniqueness
154
- machine_data = f"{platform.node()}-{platform.machine()}-{platform.system()}-{username}-{str(Path.home())}"
147
+ # Combine non-identifying factors for uniqueness
148
+ machine_data = f"{platform.machine()}-{platform.system()}-{platform.python_version()}"
155
149
  machine_hash = hashlib.sha256(machine_data.encode()).hexdigest()[:8]
156
150
 
157
151
  # Add a random component to ensure uniqueness
@@ -200,6 +194,8 @@ def track_event(event_name: str, properties: Optional[Dict[str, Any]] = None) ->
200
194
  event_name: Name of the event (e.g., "cli_init", "cli_build")
201
195
  properties: Optional properties to include with the event
202
196
  """
197
+ global _user_identified
198
+
203
199
  if not is_telemetry_enabled():
204
200
  return
205
201
 
@@ -215,33 +211,33 @@ def track_event(event_name: str, properties: Optional[Dict[str, Any]] = None) ->
215
211
  # Get anonymous ID
216
212
  anonymous_id = get_anonymous_id()
217
213
 
218
- # Set person properties to differentiate installations
219
- # This helps PostHog understand these are different users
220
- try:
221
- hostname = platform.node()
222
- except Exception:
223
- hostname = "unknown"
224
-
225
- person_properties = {
226
- "$set": {
227
- "golf_version": __version__,
228
- "os": platform.system(),
229
- "hostname": hostname,
230
- "python_version": f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}",
214
+ # Only identify the user once per session
215
+ if not _user_identified:
216
+ # Set person properties to differentiate installations
217
+ # Only include non-identifying information
218
+ person_properties = {
219
+ "$set": {
220
+ "golf_version": __version__,
221
+ "os": platform.system(),
222
+ "python_version": f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}",
223
+ }
231
224
  }
232
- }
233
-
234
- # Identify the user with properties
235
- posthog.identify(
236
- distinct_id=anonymous_id,
237
- properties=person_properties
238
- )
225
+
226
+ # Identify the user with properties
227
+ posthog.identify(
228
+ distinct_id=anonymous_id,
229
+ properties=person_properties
230
+ )
231
+
232
+ _user_identified = True
239
233
 
240
234
  # Only include minimal, non-identifying properties
241
235
  safe_properties = {
242
236
  "golf_version": __version__,
243
237
  "python_version": f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}",
244
238
  "os": platform.system(),
239
+ # Explicitly disable IP tracking
240
+ "$ip": None,
245
241
  }
246
242
 
247
243
  # Filter properties to only include safe ones
@@ -2,4 +2,4 @@ GOLF_CLIENT_ID="default-client-id"
2
2
  GOLF_CLIENT_SECRET="default-secret"
3
3
  JWT_SECRET="example-jwt-secret-for-development-only"
4
4
  OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318/v1/traces"
5
- OTEL_SERVICE_NAME="golf-mcp"
5
+ OTEL_SERVICE_NAME="golf-mcp"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: golf-mcp
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: Framework for building MCP servers
5
5
  Author-email: Antoni Gmitruk <antoni@golf.dev>
6
6
  License-Expression: Apache-2.0
@@ -1 +0,0 @@
1
- __version__ = "0.1.8"
@@ -1,97 +0,0 @@
1
- """Helper functions for working with authentication in MCP context."""
2
-
3
- from typing import Optional
4
- from contextvars import ContextVar
5
-
6
- # Re-export get_access_token from the MCP SDK
7
- from mcp.server.auth.middleware.auth_context import get_access_token
8
-
9
- from .oauth import GolfOAuthProvider
10
-
11
- # Context variable to store the active OAuth provider
12
- _active_golf_oauth_provider: Optional[GolfOAuthProvider] = None
13
-
14
- # Context variable to store the current request's API key
15
- _current_api_key: ContextVar[Optional[str]] = ContextVar('current_api_key', default=None)
16
-
17
- def _set_active_golf_oauth_provider(provider: GolfOAuthProvider) -> None:
18
- """
19
- Sets the active GolfOAuthProvider instance.
20
- Should only be called once during server startup.
21
- """
22
- global _active_golf_oauth_provider
23
- _active_golf_oauth_provider = provider
24
-
25
- def get_provider_token() -> Optional[str]:
26
- """
27
- Get a provider token (e.g., GitHub token) associated with the current
28
- MCP session's access token.
29
-
30
- This relies on _set_active_golf_oauth_provider being called at server startup.
31
- """
32
- mcp_access_token = get_access_token() # From MCP SDK, uses its own ContextVar
33
- if not mcp_access_token:
34
- # No active MCP session token.
35
- return None
36
-
37
- provider = _active_golf_oauth_provider
38
- if not provider:
39
- return None
40
-
41
- if not hasattr(provider, "get_provider_token"):
42
- return None
43
-
44
- # Call the get_provider_token method on the actual GolfOAuthProvider instance
45
- return provider.get_provider_token(mcp_access_token.token)
46
-
47
- def extract_token_from_header(auth_header: str) -> Optional[str]:
48
- """Extract bearer token from Authorization header.
49
-
50
- Args:
51
- auth_header: Authorization header value
52
-
53
- Returns:
54
- Bearer token or None if not present/valid
55
- """
56
- if not auth_header:
57
- return None
58
-
59
- parts = auth_header.split()
60
- if len(parts) != 2 or parts[0].lower() != 'bearer':
61
- return None
62
-
63
- return parts[1]
64
-
65
- def set_api_key(api_key: Optional[str]) -> None:
66
- """Set the API key for the current request context.
67
-
68
- This is an internal function used by the middleware.
69
-
70
- Args:
71
- api_key: The API key to store in the context
72
- """
73
- _current_api_key.set(api_key)
74
-
75
- def get_api_key() -> Optional[str]:
76
- """Get the API key from the current request context.
77
-
78
- This function should be used in tools to retrieve the API key
79
- that was sent in the request headers.
80
-
81
- Returns:
82
- The API key if available, None otherwise
83
-
84
- Example:
85
- # In a tool file
86
- from golf.auth import get_api_key
87
-
88
- async def call_api():
89
- api_key = get_api_key()
90
- if not api_key:
91
- return {"error": "No API key provided"}
92
-
93
- # Use the API key in your request
94
- headers = {"Authorization": f"Bearer {api_key}"}
95
- ...
96
- """
97
- return _current_api_key.get()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes