golf-mcp 0.2.5__tar.gz → 0.2.7__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.
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/PKG-INFO +1 -2
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/README.md +0 -1
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/pyproject.toml +2 -2
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/__init__.py +1 -1
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/auth/__init__.py +17 -15
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/auth/helpers.py +66 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/auth/providers.py +19 -23
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/core/builder.py +137 -32
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/basic/resources/weather/client.py +1 -1
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf_mcp.egg-info/PKG-INFO +1 -2
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/.docs/docs.md +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/.docs/fastmcp-diff.md +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/.docs/mcp.md +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/LICENSE +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/MANIFEST.in +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/setup.cfg +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/setup.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/_endpoints.py.in +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/_endpoints_fallback.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/auth/api_key.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/auth/factory.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/auth/registry.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/cli/__init__.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/cli/branding.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/cli/main.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/commands/__init__.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/commands/build.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/commands/init.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/commands/run.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/core/__init__.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/core/builder_auth.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/core/builder_metrics.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/core/builder_telemetry.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/core/config.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/core/parser.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/core/platform.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/core/telemetry.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/core/transformer.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/__init__.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/basic/.env.example +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/basic/README.md +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/basic/auth.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/basic/golf.json +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/basic/prompts/welcome.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/basic/resources/current_time.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/basic/resources/info.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/basic/resources/weather/city.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/basic/resources/weather/current.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/basic/resources/weather/forecast.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/basic/tools/calculator.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/examples/basic/tools/say/hello.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/metrics/__init__.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/metrics/collector.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/metrics/registry.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/telemetry/__init__.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/telemetry/instrumentation.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/utilities/__init__.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/utilities/context.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/utilities/elicitation.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf/utilities/sampling.py +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf_mcp.egg-info/SOURCES.txt +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf_mcp.egg-info/dependency_links.txt +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf_mcp.egg-info/entry_points.txt +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/src/golf_mcp.egg-info/requires.txt +0 -0
- {golf_mcp-0.2.5 → golf_mcp-0.2.7}/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.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Framework for building MCP servers
|
|
5
5
|
Author-email: Antoni Gmitruk <antoni@golf.dev>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -132,7 +132,6 @@ A Golf project initialized with `golf init` will have a structure similar to thi
|
|
|
132
132
|
- **`auth.py`**: Dedicated authentication configuration file (new in v0.2.0, breaking change from v0.1.x authentication API) for JWT, OAuth Server, API key, or development authentication.
|
|
133
133
|
- **`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.
|
|
134
134
|
- 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).
|
|
135
|
-
- **`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.
|
|
136
135
|
|
|
137
136
|
## Example: Defining a Tool
|
|
138
137
|
|
|
@@ -92,7 +92,6 @@ A Golf project initialized with `golf init` will have a structure similar to thi
|
|
|
92
92
|
- **`auth.py`**: Dedicated authentication configuration file (new in v0.2.0, breaking change from v0.1.x authentication API) for JWT, OAuth Server, API key, or development authentication.
|
|
93
93
|
- **`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.
|
|
94
94
|
- 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).
|
|
95
|
-
- **`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.
|
|
96
95
|
|
|
97
96
|
## Example: Defining a Tool
|
|
98
97
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "golf-mcp"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.7"
|
|
8
8
|
description = "Framework for building MCP servers"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Antoni Gmitruk", email = "antoni@golf.dev"}
|
|
@@ -66,7 +66,7 @@ golf = ["examples/**/*"]
|
|
|
66
66
|
|
|
67
67
|
[tool.poetry]
|
|
68
68
|
name = "golf-mcp"
|
|
69
|
-
version = "0.2.
|
|
69
|
+
version = "0.2.7"
|
|
70
70
|
description = "Framework for building MCP servers with zero boilerplate"
|
|
71
71
|
authors = ["Antoni Gmitruk <antoni@golf.dev>"]
|
|
72
72
|
license = "Apache-2.0"
|
|
@@ -35,6 +35,7 @@ from .helpers import (
|
|
|
35
35
|
debug_api_key_context,
|
|
36
36
|
extract_token_from_header,
|
|
37
37
|
get_api_key,
|
|
38
|
+
get_auth_token,
|
|
38
39
|
get_provider_token,
|
|
39
40
|
set_api_key,
|
|
40
41
|
)
|
|
@@ -72,6 +73,7 @@ __all__ = [
|
|
|
72
73
|
"debug_api_key_context",
|
|
73
74
|
"extract_token_from_header",
|
|
74
75
|
"get_api_key",
|
|
76
|
+
"get_auth_token",
|
|
75
77
|
"get_provider_token",
|
|
76
78
|
"set_api_key",
|
|
77
79
|
]
|
|
@@ -195,14 +197,14 @@ def configure_dev_auth(
|
|
|
195
197
|
|
|
196
198
|
|
|
197
199
|
def configure_oauth_proxy(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
authorization_endpoint: str,
|
|
201
|
+
token_endpoint: str,
|
|
202
|
+
client_id: str,
|
|
203
|
+
client_secret: str,
|
|
202
204
|
base_url: str,
|
|
203
205
|
token_verifier_config: JWTAuthConfig | StaticTokenConfig,
|
|
204
206
|
scopes_supported: list[str] | None = None,
|
|
205
|
-
|
|
207
|
+
revocation_endpoint: str | None = None,
|
|
206
208
|
redirect_path: str = "/oauth/callback",
|
|
207
209
|
) -> None:
|
|
208
210
|
"""Configure OAuth proxy authentication for non-DCR providers.
|
|
@@ -212,25 +214,25 @@ def configure_oauth_proxy(
|
|
|
212
214
|
fixed client credentials.
|
|
213
215
|
|
|
214
216
|
Args:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
authorization_endpoint: Provider's authorization URL
|
|
218
|
+
token_endpoint: Provider's token endpoint URL
|
|
219
|
+
client_id: Your client ID registered with the provider
|
|
220
|
+
client_secret: Your client secret from the provider
|
|
219
221
|
base_url: This proxy server's public URL
|
|
220
222
|
token_verifier_config: JWT or static token config for token verification
|
|
221
223
|
scopes_supported: Scopes to advertise to MCP clients
|
|
222
|
-
|
|
224
|
+
revocation_endpoint: Optional token revocation endpoint
|
|
223
225
|
redirect_path: OAuth callback path (default: /oauth/callback)
|
|
224
226
|
|
|
225
227
|
Note:
|
|
226
228
|
Requires golf-mcp-enterprise package for implementation.
|
|
227
229
|
"""
|
|
228
230
|
config = OAuthProxyConfig(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
authorization_endpoint=authorization_endpoint,
|
|
232
|
+
token_endpoint=token_endpoint,
|
|
233
|
+
client_id=client_id,
|
|
234
|
+
client_secret=client_secret,
|
|
235
|
+
revocation_endpoint=revocation_endpoint,
|
|
234
236
|
base_url=base_url,
|
|
235
237
|
redirect_path=redirect_path,
|
|
236
238
|
scopes_supported=scopes_supported or [],
|
|
@@ -142,6 +142,72 @@ def get_api_key_from_request(request: Request) -> str | None:
|
|
|
142
142
|
return _current_api_key.get()
|
|
143
143
|
|
|
144
144
|
|
|
145
|
+
def get_auth_token() -> str | None:
|
|
146
|
+
"""Get the authorization token from the current request context.
|
|
147
|
+
|
|
148
|
+
This function should be used in tools to retrieve the authorization token
|
|
149
|
+
(typically a JWT or OAuth token) that was sent in the request headers.
|
|
150
|
+
|
|
151
|
+
Unlike get_api_key(), this function extracts the raw token from the Authorization
|
|
152
|
+
header without stripping any prefix, making it suitable for passing through
|
|
153
|
+
to upstream APIs that expect the full Authorization header value.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The authorization token if available, None otherwise
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
# In a tool file
|
|
160
|
+
from golf.auth import get_auth_token
|
|
161
|
+
|
|
162
|
+
async def call_upstream_api():
|
|
163
|
+
auth_token = get_auth_token()
|
|
164
|
+
if not auth_token:
|
|
165
|
+
return {"error": "No authorization token provided"}
|
|
166
|
+
|
|
167
|
+
# Use the full token in upstream request
|
|
168
|
+
headers = {"Authorization": f"Bearer {auth_token}"}
|
|
169
|
+
async with httpx.AsyncClient() as client:
|
|
170
|
+
response = await client.get("https://api.example.com/data", headers=headers)
|
|
171
|
+
...
|
|
172
|
+
"""
|
|
173
|
+
# Try to get directly from HTTP request if available (FastMCP pattern)
|
|
174
|
+
try:
|
|
175
|
+
# This follows the FastMCP pattern for accessing HTTP requests
|
|
176
|
+
from fastmcp.server.dependencies import get_http_request
|
|
177
|
+
|
|
178
|
+
request = get_http_request()
|
|
179
|
+
|
|
180
|
+
if request and hasattr(request, "state") and hasattr(request.state, "auth_token"):
|
|
181
|
+
return request.state.auth_token
|
|
182
|
+
|
|
183
|
+
if request:
|
|
184
|
+
# Extract authorization token from Authorization header
|
|
185
|
+
auth_header = None
|
|
186
|
+
for k, v in request.headers.items():
|
|
187
|
+
if k.lower() == "authorization":
|
|
188
|
+
auth_header = v
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
if auth_header:
|
|
192
|
+
# Extract the token part (everything after "Bearer ")
|
|
193
|
+
token = extract_token_from_header(auth_header)
|
|
194
|
+
if token:
|
|
195
|
+
return token
|
|
196
|
+
|
|
197
|
+
# If not Bearer format, return the whole header value minus "Bearer " prefix if present
|
|
198
|
+
if auth_header.lower().startswith("bearer "):
|
|
199
|
+
return auth_header[7:] # Remove "Bearer " prefix
|
|
200
|
+
return auth_header
|
|
201
|
+
|
|
202
|
+
except (ImportError, RuntimeError):
|
|
203
|
+
# FastMCP not available or not in HTTP context
|
|
204
|
+
pass
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
|
|
145
211
|
def debug_api_key_context() -> dict[str, Any]:
|
|
146
212
|
"""Debug function to inspect API key context.
|
|
147
213
|
|
|
@@ -458,12 +458,12 @@ class OAuthProxyConfig(BaseModel):
|
|
|
458
458
|
|
|
459
459
|
provider_type: Literal["oauth_proxy"] = "oauth_proxy"
|
|
460
460
|
|
|
461
|
-
#
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
461
|
+
# OAuth provider configuration
|
|
462
|
+
authorization_endpoint: str = Field(..., description="OAuth provider's authorization endpoint URL")
|
|
463
|
+
token_endpoint: str = Field(..., description="OAuth provider's token endpoint URL")
|
|
464
|
+
client_id: str = Field(..., description="Your registered client ID with the OAuth provider")
|
|
465
|
+
client_secret: str = Field(..., description="Your registered client secret with the OAuth provider")
|
|
466
|
+
revocation_endpoint: str | None = Field(None, description="Optional token revocation endpoint")
|
|
467
467
|
|
|
468
468
|
# This proxy server configuration
|
|
469
469
|
base_url: str = Field(..., description="Public URL of this OAuth proxy server")
|
|
@@ -476,22 +476,18 @@ class OAuthProxyConfig(BaseModel):
|
|
|
476
476
|
)
|
|
477
477
|
|
|
478
478
|
# Environment variable names for runtime configuration
|
|
479
|
-
|
|
480
|
-
None, description="Environment variable name for
|
|
479
|
+
authorization_endpoint_env_var: str | None = Field(
|
|
480
|
+
None, description="Environment variable name for authorization endpoint"
|
|
481
481
|
)
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
None, description="Environment variable name for upstream client secret"
|
|
488
|
-
)
|
|
489
|
-
upstream_revocation_endpoint_env_var: str | None = Field(
|
|
490
|
-
None, description="Environment variable name for upstream revocation endpoint"
|
|
482
|
+
token_endpoint_env_var: str | None = Field(None, description="Environment variable name for token endpoint")
|
|
483
|
+
client_id_env_var: str | None = Field(None, description="Environment variable name for client ID")
|
|
484
|
+
client_secret_env_var: str | None = Field(None, description="Environment variable name for client secret")
|
|
485
|
+
revocation_endpoint_env_var: str | None = Field(
|
|
486
|
+
None, description="Environment variable name for revocation endpoint"
|
|
491
487
|
)
|
|
492
488
|
base_url_env_var: str | None = Field(None, description="Environment variable name for base URL")
|
|
493
489
|
|
|
494
|
-
@field_validator("
|
|
490
|
+
@field_validator("authorization_endpoint", "token_endpoint", "base_url")
|
|
495
491
|
@classmethod
|
|
496
492
|
def validate_required_urls(cls, v: str) -> str:
|
|
497
493
|
"""Validate required URLs are properly formatted."""
|
|
@@ -514,7 +510,7 @@ class OAuthProxyConfig(BaseModel):
|
|
|
514
510
|
|
|
515
511
|
return url
|
|
516
512
|
|
|
517
|
-
@field_validator("
|
|
513
|
+
@field_validator("revocation_endpoint")
|
|
518
514
|
@classmethod
|
|
519
515
|
def validate_optional_url(cls, v: str | None) -> str | None:
|
|
520
516
|
"""Validate optional URLs are properly formatted."""
|
|
@@ -588,12 +584,12 @@ class OAuthProxyConfig(BaseModel):
|
|
|
588
584
|
|
|
589
585
|
urls_to_check = [
|
|
590
586
|
("base_url", self.base_url),
|
|
591
|
-
("
|
|
592
|
-
("
|
|
587
|
+
("authorization_endpoint", self.authorization_endpoint),
|
|
588
|
+
("token_endpoint", self.token_endpoint),
|
|
593
589
|
]
|
|
594
590
|
|
|
595
|
-
if self.
|
|
596
|
-
urls_to_check.append(("
|
|
591
|
+
if self.revocation_endpoint:
|
|
592
|
+
urls_to_check.append(("revocation_endpoint", self.revocation_endpoint))
|
|
597
593
|
|
|
598
594
|
for field_name, url in urls_to_check:
|
|
599
595
|
parsed = urlparse(url)
|
|
@@ -170,6 +170,41 @@ class ManifestBuilder:
|
|
|
170
170
|
console.print(f"[green]Manifest saved to {output_path}[/green]")
|
|
171
171
|
return output_path
|
|
172
172
|
|
|
173
|
+
def _get_fastmcp_version(self) -> str | None:
|
|
174
|
+
"""Get the installed FastMCP version.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
FastMCP version string (e.g., "2.12.0") or None if not available
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
import fastmcp
|
|
181
|
+
|
|
182
|
+
return fastmcp.__version__
|
|
183
|
+
except (ImportError, AttributeError):
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
def _is_fastmcp_version_gte(self, target_version: str) -> bool:
|
|
187
|
+
"""Check if installed FastMCP version is >= target version.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
target_version: Version string to compare against (e.g., "2.12.0")
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
True if FastMCP version >= target_version, False otherwise
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
from packaging import version
|
|
197
|
+
|
|
198
|
+
current_version = self._get_fastmcp_version()
|
|
199
|
+
if current_version is None:
|
|
200
|
+
# Default to older behavior for safety
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
return version.parse(current_version) >= version.parse(target_version)
|
|
204
|
+
except (ImportError, ValueError):
|
|
205
|
+
# Default to older behavior for safety
|
|
206
|
+
return False
|
|
207
|
+
|
|
173
208
|
|
|
174
209
|
def build_manifest(project_path: Path, settings: Settings) -> dict[str, Any]:
|
|
175
210
|
"""Build a FastMCP manifest from parsed components.
|
|
@@ -511,6 +546,41 @@ class CodeGenerator:
|
|
|
511
546
|
and len(component.parameters) > 0
|
|
512
547
|
)
|
|
513
548
|
|
|
549
|
+
def _get_fastmcp_version(self) -> str | None:
|
|
550
|
+
"""Get the installed FastMCP version.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
FastMCP version string (e.g., "2.12.0") or None if not available
|
|
554
|
+
"""
|
|
555
|
+
try:
|
|
556
|
+
import fastmcp
|
|
557
|
+
|
|
558
|
+
return fastmcp.__version__
|
|
559
|
+
except (ImportError, AttributeError):
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
def _is_fastmcp_version_gte(self, target_version: str) -> bool:
|
|
563
|
+
"""Check if installed FastMCP version is >= target version.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
target_version: Version string to compare against (e.g., "2.12.0")
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
True if FastMCP version >= target_version, False otherwise
|
|
570
|
+
"""
|
|
571
|
+
try:
|
|
572
|
+
from packaging import version
|
|
573
|
+
|
|
574
|
+
current_version = self._get_fastmcp_version()
|
|
575
|
+
if current_version is None:
|
|
576
|
+
# Default to older behavior for safety
|
|
577
|
+
return False
|
|
578
|
+
|
|
579
|
+
return version.parse(current_version) >= version.parse(target_version)
|
|
580
|
+
except (ImportError, ValueError):
|
|
581
|
+
# Default to older behavior for safety
|
|
582
|
+
return False
|
|
583
|
+
|
|
514
584
|
def _generate_server(self) -> None:
|
|
515
585
|
"""Generate the main server entry point."""
|
|
516
586
|
server_file = self.output_dir / "server.py"
|
|
@@ -956,23 +1026,40 @@ class CodeGenerator:
|
|
|
956
1026
|
main_code.extend(middleware_setup)
|
|
957
1027
|
main_code.append(f" middleware = [{', '.join(middleware_list)}]")
|
|
958
1028
|
main_code.append("")
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1029
|
+
if self._is_fastmcp_version_gte("2.12.0"):
|
|
1030
|
+
main_code.extend(
|
|
1031
|
+
[
|
|
1032
|
+
" # Run SSE server with middleware using FastMCP's run method",
|
|
1033
|
+
' mcp.run(transport="sse", host=host, port=port, '
|
|
1034
|
+
'log_level="info", middleware=middleware, show_banner=False)',
|
|
1035
|
+
]
|
|
1036
|
+
)
|
|
1037
|
+
else:
|
|
1038
|
+
main_code.extend(
|
|
1039
|
+
[
|
|
1040
|
+
" # Run SSE server with middleware using FastMCP's run method",
|
|
1041
|
+
f' mcp.run(transport="sse", host=host, port=port, '
|
|
1042
|
+
f'path="{endpoint_path}", log_level="info", '
|
|
1043
|
+
f"middleware=middleware, show_banner=False)",
|
|
1044
|
+
]
|
|
1045
|
+
)
|
|
967
1046
|
else:
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1047
|
+
if self._is_fastmcp_version_gte("2.12.0"):
|
|
1048
|
+
main_code.extend(
|
|
1049
|
+
[
|
|
1050
|
+
" # Run SSE server using FastMCP's run method",
|
|
1051
|
+
' mcp.run(transport="sse", host=host, port=port, log_level="info", show_banner=False)',
|
|
1052
|
+
]
|
|
1053
|
+
)
|
|
1054
|
+
else:
|
|
1055
|
+
main_code.extend(
|
|
1056
|
+
[
|
|
1057
|
+
" # Run SSE server using FastMCP's run method",
|
|
1058
|
+
f' mcp.run(transport="sse", host=host, port=port, '
|
|
1059
|
+
f'path="{endpoint_path}", log_level="info", '
|
|
1060
|
+
f"show_banner=False)",
|
|
1061
|
+
]
|
|
1062
|
+
)
|
|
976
1063
|
|
|
977
1064
|
elif self.settings.transport in ["streamable-http", "http"]:
|
|
978
1065
|
# Check if we need middleware for streamable-http
|
|
@@ -999,23 +1086,41 @@ class CodeGenerator:
|
|
|
999
1086
|
main_code.extend(middleware_setup)
|
|
1000
1087
|
main_code.append(f" middleware = [{', '.join(middleware_list)}]")
|
|
1001
1088
|
main_code.append("")
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1089
|
+
if self._is_fastmcp_version_gte("2.12.0"):
|
|
1090
|
+
main_code.extend(
|
|
1091
|
+
[
|
|
1092
|
+
" # Run HTTP server with middleware using FastMCP's run method",
|
|
1093
|
+
' mcp.run(transport="streamable-http", host=host, '
|
|
1094
|
+
'port=port, log_level="info", middleware=middleware, show_banner=False)',
|
|
1095
|
+
]
|
|
1096
|
+
)
|
|
1097
|
+
else:
|
|
1098
|
+
main_code.extend(
|
|
1099
|
+
[
|
|
1100
|
+
" # Run HTTP server with middleware using FastMCP's run method",
|
|
1101
|
+
f' mcp.run(transport="streamable-http", host=host, '
|
|
1102
|
+
f'port=port, path="{endpoint_path}", log_level="info", '
|
|
1103
|
+
f"middleware=middleware, show_banner=False)",
|
|
1104
|
+
]
|
|
1105
|
+
)
|
|
1010
1106
|
else:
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1107
|
+
if self._is_fastmcp_version_gte("2.12.0"):
|
|
1108
|
+
main_code.extend(
|
|
1109
|
+
[
|
|
1110
|
+
" # Run HTTP server using FastMCP's run method",
|
|
1111
|
+
' mcp.run(transport="streamable-http", host=host, '
|
|
1112
|
+
'port=port, log_level="info", show_banner=False)',
|
|
1113
|
+
]
|
|
1114
|
+
)
|
|
1115
|
+
else:
|
|
1116
|
+
main_code.extend(
|
|
1117
|
+
[
|
|
1118
|
+
" # Run HTTP server using FastMCP's run method",
|
|
1119
|
+
f' mcp.run(transport="streamable-http", host=host, '
|
|
1120
|
+
f'port=port, path="{endpoint_path}", log_level="info", '
|
|
1121
|
+
f"show_banner=False)",
|
|
1122
|
+
]
|
|
1123
|
+
)
|
|
1019
1124
|
else:
|
|
1020
1125
|
# For stdio transport, use mcp.run()
|
|
1021
1126
|
main_code.extend([" # Run with stdio transport", ' mcp.run(transport="stdio", show_banner=False)'])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: golf-mcp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Framework for building MCP servers
|
|
5
5
|
Author-email: Antoni Gmitruk <antoni@golf.dev>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -132,7 +132,6 @@ A Golf project initialized with `golf init` will have a structure similar to thi
|
|
|
132
132
|
- **`auth.py`**: Dedicated authentication configuration file (new in v0.2.0, breaking change from v0.1.x authentication API) for JWT, OAuth Server, API key, or development authentication.
|
|
133
133
|
- **`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.
|
|
134
134
|
- 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).
|
|
135
|
-
- **`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.
|
|
136
135
|
|
|
137
136
|
## Example: Defining a Tool
|
|
138
137
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|