golf-mcp 0.2.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- golf/__init__.py +1 -0
- golf/auth/__init__.py +277 -0
- golf/auth/api_key.py +73 -0
- golf/auth/factory.py +360 -0
- golf/auth/helpers.py +175 -0
- golf/auth/providers.py +586 -0
- golf/auth/registry.py +256 -0
- golf/cli/__init__.py +1 -0
- golf/cli/branding.py +191 -0
- golf/cli/main.py +377 -0
- golf/commands/__init__.py +5 -0
- golf/commands/build.py +81 -0
- golf/commands/init.py +290 -0
- golf/commands/run.py +137 -0
- golf/core/__init__.py +1 -0
- golf/core/builder.py +1884 -0
- golf/core/builder_auth.py +209 -0
- golf/core/builder_metrics.py +221 -0
- golf/core/builder_telemetry.py +99 -0
- golf/core/config.py +199 -0
- golf/core/parser.py +1085 -0
- golf/core/telemetry.py +492 -0
- golf/core/transformer.py +231 -0
- golf/examples/__init__.py +0 -0
- golf/examples/basic/.env.example +4 -0
- golf/examples/basic/README.md +133 -0
- golf/examples/basic/auth.py +76 -0
- golf/examples/basic/golf.json +5 -0
- golf/examples/basic/prompts/welcome.py +27 -0
- golf/examples/basic/resources/current_time.py +34 -0
- golf/examples/basic/resources/info.py +28 -0
- golf/examples/basic/resources/weather/city.py +46 -0
- golf/examples/basic/resources/weather/client.py +48 -0
- golf/examples/basic/resources/weather/current.py +36 -0
- golf/examples/basic/resources/weather/forecast.py +36 -0
- golf/examples/basic/tools/calculator.py +94 -0
- golf/examples/basic/tools/say/hello.py +65 -0
- golf/metrics/__init__.py +10 -0
- golf/metrics/collector.py +320 -0
- golf/metrics/registry.py +12 -0
- golf/telemetry/__init__.py +23 -0
- golf/telemetry/instrumentation.py +1402 -0
- golf/utilities/__init__.py +12 -0
- golf/utilities/context.py +53 -0
- golf/utilities/elicitation.py +170 -0
- golf/utilities/sampling.py +221 -0
- golf_mcp-0.2.16.dist-info/METADATA +262 -0
- golf_mcp-0.2.16.dist-info/RECORD +52 -0
- golf_mcp-0.2.16.dist-info/WHEEL +5 -0
- golf_mcp-0.2.16.dist-info/entry_points.txt +2 -0
- golf_mcp-0.2.16.dist-info/licenses/LICENSE +201 -0
- golf_mcp-0.2.16.dist-info/top_level.txt +1 -0
golf/core/transformer.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Transform GolfMCP components into standalone FastMCP code.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for transforming GolfMCP's convention-based code
|
|
4
|
+
into explicit FastMCP component registrations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from golf.core.parser import ParsedComponent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ImportTransformer(ast.NodeTransformer):
|
|
15
|
+
"""AST transformer for rewriting imports in component files."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
original_path: Path,
|
|
20
|
+
target_path: Path,
|
|
21
|
+
import_map: dict[str, str],
|
|
22
|
+
project_root: Path,
|
|
23
|
+
root_file_modules: set[str] | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Initialize the import transformer.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
original_path: Path to the original file
|
|
29
|
+
target_path: Path to the target file
|
|
30
|
+
import_map: Mapping of original module paths to generated paths
|
|
31
|
+
project_root: Root path of the project
|
|
32
|
+
root_file_modules: Set of root file module names (without .py extension)
|
|
33
|
+
"""
|
|
34
|
+
self.original_path = original_path
|
|
35
|
+
self.target_path = target_path
|
|
36
|
+
self.import_map = import_map
|
|
37
|
+
self.project_root = project_root
|
|
38
|
+
self.root_file_modules = root_file_modules or set()
|
|
39
|
+
|
|
40
|
+
def _calculate_import_depth(self) -> int:
|
|
41
|
+
"""Calculate the relative import depth needed to reach build root from component location."""
|
|
42
|
+
try:
|
|
43
|
+
# Get component path relative to project root
|
|
44
|
+
relative_path = self.target_path.relative_to(self.project_root)
|
|
45
|
+
|
|
46
|
+
# Count directory levels: components/tools/weather.py = 2 levels, needs level=2
|
|
47
|
+
# components/tools/api/handler.py = 3 levels, needs level=3
|
|
48
|
+
# Build root contains the root files, so depth = number of path parts
|
|
49
|
+
return len(relative_path.parts) - 1 # Subtract 1 for the filename itself
|
|
50
|
+
|
|
51
|
+
except ValueError:
|
|
52
|
+
# Fallback to level=3 if path calculation fails
|
|
53
|
+
return 3
|
|
54
|
+
|
|
55
|
+
def visit_Import(self, node: ast.Import) -> Any:
|
|
56
|
+
"""Transform import statements."""
|
|
57
|
+
new_names = []
|
|
58
|
+
|
|
59
|
+
for alias in node.names:
|
|
60
|
+
module_name = alias.name
|
|
61
|
+
|
|
62
|
+
if module_name in self.root_file_modules:
|
|
63
|
+
# Keep original import unchanged - sys.path will handle resolution
|
|
64
|
+
new_names.append(alias)
|
|
65
|
+
continue
|
|
66
|
+
else:
|
|
67
|
+
new_names.append(alias)
|
|
68
|
+
|
|
69
|
+
# If no root modules, return original or modified import
|
|
70
|
+
if new_names != list(node.names):
|
|
71
|
+
return ast.Import(names=new_names)
|
|
72
|
+
return node
|
|
73
|
+
|
|
74
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> Any:
|
|
75
|
+
"""Transform import from statements."""
|
|
76
|
+
if node.module is None:
|
|
77
|
+
return node
|
|
78
|
+
|
|
79
|
+
# Check if this is importing from a root file module
|
|
80
|
+
if node.level == 0 and node.module in self.root_file_modules:
|
|
81
|
+
# Keep unchanged - sys.path will handle resolution
|
|
82
|
+
return node
|
|
83
|
+
|
|
84
|
+
# Handle relative imports
|
|
85
|
+
if node.level > 0:
|
|
86
|
+
# Calculate the source module path
|
|
87
|
+
source_dir = self.original_path.parent
|
|
88
|
+
for _ in range(node.level - 1):
|
|
89
|
+
source_dir = source_dir.parent
|
|
90
|
+
|
|
91
|
+
if node.module:
|
|
92
|
+
# Handle imports like `from .helpers import utils`
|
|
93
|
+
source_module = source_dir / node.module.replace(".", "/")
|
|
94
|
+
else:
|
|
95
|
+
# Handle imports like `from . import something`
|
|
96
|
+
source_module = source_dir
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
# Check if this is a shared module import
|
|
100
|
+
source_str = str(source_module.relative_to(self.project_root))
|
|
101
|
+
|
|
102
|
+
# First, try direct module path match (e.g., "tools/weather/helpers")
|
|
103
|
+
if source_str in self.import_map:
|
|
104
|
+
new_module = self.import_map[source_str]
|
|
105
|
+
return ast.ImportFrom(module=new_module, names=node.names, level=0)
|
|
106
|
+
|
|
107
|
+
# If direct match fails, try directory-based matching
|
|
108
|
+
# This handles cases like `from . import common` where the import_map
|
|
109
|
+
# has "tools/weather/common" but we're looking for "tools/weather"
|
|
110
|
+
source_dir_str = str(source_dir.relative_to(self.project_root))
|
|
111
|
+
if source_dir_str in self.import_map:
|
|
112
|
+
new_module = self.import_map[source_dir_str]
|
|
113
|
+
if node.module:
|
|
114
|
+
new_module = f"{new_module}.{node.module}"
|
|
115
|
+
return ast.ImportFrom(module=new_module, names=node.names, level=0)
|
|
116
|
+
|
|
117
|
+
# Check for specific module imports within the directory
|
|
118
|
+
for import_path, mapped_path in self.import_map.items():
|
|
119
|
+
# Handle cases where we import a specific module from a directory
|
|
120
|
+
# e.g., `from .common import something` should match "tools/weather/common"
|
|
121
|
+
if import_path.startswith(source_dir_str + "/") and node.module:
|
|
122
|
+
module_name = import_path.replace(source_dir_str + "/", "")
|
|
123
|
+
if module_name == node.module:
|
|
124
|
+
return ast.ImportFrom(module=mapped_path, names=node.names, level=0)
|
|
125
|
+
|
|
126
|
+
except ValueError:
|
|
127
|
+
# source_module is not relative to project_root, leave import unchanged
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
return node
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def transform_component(
|
|
134
|
+
component: ParsedComponent | None,
|
|
135
|
+
output_file: Path,
|
|
136
|
+
project_path: Path,
|
|
137
|
+
import_map: dict[str, str],
|
|
138
|
+
source_file: Path | None = None,
|
|
139
|
+
root_file_modules: set[str] | None = None,
|
|
140
|
+
) -> str:
|
|
141
|
+
"""Transform a GolfMCP component into a standalone FastMCP component.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
component: Parsed component to transform (optional if source_file provided)
|
|
145
|
+
output_file: Path to write the transformed component to
|
|
146
|
+
project_path: Path to the project root
|
|
147
|
+
import_map: Mapping of original module paths to generated paths
|
|
148
|
+
source_file: Optional path to source file (for shared files)
|
|
149
|
+
root_file_modules: Set of root file module names (without .py extension)
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Generated component code
|
|
153
|
+
"""
|
|
154
|
+
# Read the original file
|
|
155
|
+
if source_file is not None:
|
|
156
|
+
file_path = source_file
|
|
157
|
+
elif component is not None:
|
|
158
|
+
file_path = Path(component.file_path)
|
|
159
|
+
else:
|
|
160
|
+
raise ValueError("Either component or source_file must be provided")
|
|
161
|
+
|
|
162
|
+
with open(file_path) as f:
|
|
163
|
+
source_code = f.read()
|
|
164
|
+
|
|
165
|
+
# Parse the source code into an AST
|
|
166
|
+
tree = ast.parse(source_code)
|
|
167
|
+
|
|
168
|
+
# Transform imports
|
|
169
|
+
transformer = ImportTransformer(file_path, output_file, import_map, project_path, root_file_modules)
|
|
170
|
+
tree = transformer.visit(tree)
|
|
171
|
+
|
|
172
|
+
# Get all imports and docstring
|
|
173
|
+
imports = []
|
|
174
|
+
docstring = None
|
|
175
|
+
|
|
176
|
+
# Find the module docstring if present
|
|
177
|
+
if (
|
|
178
|
+
len(tree.body) > 0
|
|
179
|
+
and isinstance(tree.body[0], ast.Expr)
|
|
180
|
+
and isinstance(tree.body[0].value, ast.Constant)
|
|
181
|
+
and isinstance(tree.body[0].value.value, str)
|
|
182
|
+
):
|
|
183
|
+
docstring = tree.body[0].value.value
|
|
184
|
+
|
|
185
|
+
# Find imports
|
|
186
|
+
for node in tree.body:
|
|
187
|
+
if isinstance(node, ast.Import | ast.ImportFrom):
|
|
188
|
+
imports.append(node)
|
|
189
|
+
|
|
190
|
+
# Build full transformed code - start with docstring first (Python convention)
|
|
191
|
+
transformed_code = ""
|
|
192
|
+
|
|
193
|
+
# Add docstring first if present, using proper triple quotes for multi-line docstrings
|
|
194
|
+
if docstring:
|
|
195
|
+
# Check if docstring contains newlines
|
|
196
|
+
if "\n" in docstring:
|
|
197
|
+
# Use triple quotes for multi-line docstrings
|
|
198
|
+
transformed_code += f'"""{docstring}"""\n\n'
|
|
199
|
+
else:
|
|
200
|
+
# Use single quotes for single-line docstrings
|
|
201
|
+
transformed_code += f'"{docstring}"\n\n'
|
|
202
|
+
|
|
203
|
+
# Add transformed imports after docstring
|
|
204
|
+
if imports:
|
|
205
|
+
transformed_imports = ast.unparse(ast.Module(body=imports, type_ignores=[]))
|
|
206
|
+
transformed_code += transformed_imports + "\n\n"
|
|
207
|
+
|
|
208
|
+
# Add the rest of the code except imports and the original docstring
|
|
209
|
+
remaining_nodes = []
|
|
210
|
+
for node in tree.body:
|
|
211
|
+
# Skip imports
|
|
212
|
+
if isinstance(node, ast.Import | ast.ImportFrom):
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
# Skip the original docstring
|
|
216
|
+
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
remaining_nodes.append(node)
|
|
220
|
+
|
|
221
|
+
remaining_code = ast.unparse(ast.Module(body=remaining_nodes, type_ignores=[]))
|
|
222
|
+
transformed_code += remaining_code
|
|
223
|
+
|
|
224
|
+
# Ensure the directory exists
|
|
225
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
|
|
227
|
+
# Write the transformed code to the output file
|
|
228
|
+
with open(output_file, "w") as f:
|
|
229
|
+
f.write(transformed_code)
|
|
230
|
+
|
|
231
|
+
return transformed_code
|
|
File without changes
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Golf MCP Project Template (Basic)
|
|
2
|
+
|
|
3
|
+
This is a basic template for creating MCP servers with Golf. It includes development authentication for easy testing. Use `golf init <project-name>` to bootstrap new projects from this template.
|
|
4
|
+
|
|
5
|
+
## About Golf
|
|
6
|
+
|
|
7
|
+
Golf is a Python framework for building MCP (Model Context Protocol) servers with minimal boilerplate. Define your server's capabilities as simple Python files, and Golf automatically discovers and compiles them into a runnable FastMCP server.
|
|
8
|
+
|
|
9
|
+
## Getting Started
|
|
10
|
+
|
|
11
|
+
After initializing your project:
|
|
12
|
+
|
|
13
|
+
1. **Navigate to your project directory:**
|
|
14
|
+
```bash
|
|
15
|
+
cd your-project-name
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. **Configure authentication (optional):**
|
|
19
|
+
This template includes development authentication in `auth.py` with sample tokens. Edit the file to set up JWT, OAuth, or API key authentication for production use.
|
|
20
|
+
|
|
21
|
+
3. **Build and run your server:**
|
|
22
|
+
```bash
|
|
23
|
+
golf build dev # Development build
|
|
24
|
+
golf run # Start the server
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Project Structure
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
your-project/
|
|
31
|
+
├── tools/ # Tool implementations (functions LLMs can call)
|
|
32
|
+
├── resources/ # Resource implementations (data LLMs can read)
|
|
33
|
+
├── prompts/ # Prompt templates (conversation structures)
|
|
34
|
+
├── golf.json # Server configuration
|
|
35
|
+
└── auth.py # Authentication setup
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Adding Components
|
|
39
|
+
|
|
40
|
+
### Tools
|
|
41
|
+
Create `.py` files in `tools/` directory. Each file should export a single async function:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# tools/calculator.py
|
|
45
|
+
async def add(a: int, b: int) -> int:
|
|
46
|
+
"""Add two numbers together."""
|
|
47
|
+
return a + b
|
|
48
|
+
|
|
49
|
+
export = add
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Resources
|
|
53
|
+
Create `.py` files in `resources/` directory with a `resource_uri` and export function:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
# resources/status.py
|
|
57
|
+
resource_uri = "status://server"
|
|
58
|
+
|
|
59
|
+
async def status() -> dict:
|
|
60
|
+
"""Get server status information."""
|
|
61
|
+
return {"status": "running", "timestamp": "2024-01-01T00:00:00Z"}
|
|
62
|
+
|
|
63
|
+
export = status
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Prompts
|
|
67
|
+
Create `.py` files in `prompts/` directory that return message lists:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# prompts/assistant.py
|
|
71
|
+
async def assistant() -> list[dict]:
|
|
72
|
+
"""System prompt for a helpful assistant."""
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
"role": "system",
|
|
76
|
+
"content": "You are a helpful assistant for {{project_name}}."
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
export = assistant
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Authentication Examples
|
|
84
|
+
|
|
85
|
+
### No Authentication (Default)
|
|
86
|
+
Leave `auth.py` empty or remove it entirely.
|
|
87
|
+
|
|
88
|
+
### API Key Authentication
|
|
89
|
+
```python
|
|
90
|
+
# auth.py
|
|
91
|
+
from golf.auth import configure_api_key
|
|
92
|
+
|
|
93
|
+
configure_api_key(
|
|
94
|
+
header_name="Authorization",
|
|
95
|
+
header_prefix="Bearer ",
|
|
96
|
+
required=True
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### JWT Authentication
|
|
101
|
+
```python
|
|
102
|
+
# auth.py
|
|
103
|
+
from golf.auth import configure_jwt_auth
|
|
104
|
+
|
|
105
|
+
configure_jwt_auth(
|
|
106
|
+
jwks_uri="https://your-domain.auth0.com/.well-known/jwks.json",
|
|
107
|
+
issuer="https://your-domain.auth0.com/",
|
|
108
|
+
audience="https://your-api.example.com"
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Development Tokens
|
|
113
|
+
```python
|
|
114
|
+
# auth.py
|
|
115
|
+
from golf.auth import configure_dev_auth
|
|
116
|
+
|
|
117
|
+
configure_dev_auth(
|
|
118
|
+
tokens={
|
|
119
|
+
"dev-token-123": {
|
|
120
|
+
"client_id": "dev-client",
|
|
121
|
+
"scopes": ["read", "write"]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Documentation
|
|
128
|
+
|
|
129
|
+
For comprehensive documentation, visit: [https://docs.golf.dev](https://docs.golf.dev)
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
Happy building! 🏌️♂️
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Authentication configuration for the basic Golf MCP server example.
|
|
2
|
+
|
|
3
|
+
This example shows different authentication options available in Golf 0.2.x:
|
|
4
|
+
- JWT authentication with static keys or JWKS endpoints (production)
|
|
5
|
+
- Static token authentication (development/testing)
|
|
6
|
+
- OAuth Server mode (full OAuth 2.0 server)
|
|
7
|
+
- Remote Authorization Server integration
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Example 1: JWT authentication with a static public key
|
|
11
|
+
# from golf.auth import configure_auth, JWTAuthConfig
|
|
12
|
+
#
|
|
13
|
+
# configure_auth(
|
|
14
|
+
# JWTAuthConfig(
|
|
15
|
+
# public_key_env_var="JWT_PUBLIC_KEY", # PEM-encoded public key
|
|
16
|
+
# issuer="https://your-auth-server.com",
|
|
17
|
+
# audience="https://your-golf-server.com",
|
|
18
|
+
# required_scopes=["read:data"],
|
|
19
|
+
# )
|
|
20
|
+
# )
|
|
21
|
+
|
|
22
|
+
# Example 2: JWT authentication with JWKS (recommended for production)
|
|
23
|
+
# from golf.auth import configure_auth, JWTAuthConfig
|
|
24
|
+
#
|
|
25
|
+
# configure_auth(
|
|
26
|
+
# JWTAuthConfig(
|
|
27
|
+
# jwks_uri_env_var="JWKS_URI", # e.g., "https://your-domain.auth0.com/.well-known/jwks.json"
|
|
28
|
+
# issuer_env_var="JWT_ISSUER", # e.g., "https://your-domain.auth0.com/"
|
|
29
|
+
# audience_env_var="JWT_AUDIENCE", # e.g., "https://your-api.example.com"
|
|
30
|
+
# required_scopes=["read:user"],
|
|
31
|
+
# )
|
|
32
|
+
# )
|
|
33
|
+
|
|
34
|
+
# Example 3: OAuth Server mode - Golf acts as full OAuth 2.0 authorization server
|
|
35
|
+
# from golf.auth import configure_auth, OAuthServerConfig
|
|
36
|
+
#
|
|
37
|
+
# configure_auth(
|
|
38
|
+
# OAuthServerConfig(
|
|
39
|
+
# base_url_env_var="OAUTH_BASE_URL", # e.g., "https://auth.example.com"
|
|
40
|
+
# valid_scopes=["read", "write", "admin"], # Scopes clients can request
|
|
41
|
+
# default_scopes=["read"], # Default scopes for new clients
|
|
42
|
+
# required_scopes=["read"], # Scopes required for all requests
|
|
43
|
+
# )
|
|
44
|
+
# )
|
|
45
|
+
|
|
46
|
+
# Example 4: Remote Authorization Server integration
|
|
47
|
+
# from golf.auth import configure_auth, RemoteAuthConfig, JWTAuthConfig
|
|
48
|
+
#
|
|
49
|
+
# configure_auth(
|
|
50
|
+
# RemoteAuthConfig(
|
|
51
|
+
# authorization_servers_env_var="AUTH_SERVERS", # Comma-separated: "https://auth1.com,https://auth2.com"
|
|
52
|
+
# resource_server_url_env_var="RESOURCE_URL", # This server's URL
|
|
53
|
+
# token_verifier_config=JWTAuthConfig(
|
|
54
|
+
# jwks_uri_env_var="JWKS_URI"
|
|
55
|
+
# ),
|
|
56
|
+
# )
|
|
57
|
+
# )
|
|
58
|
+
|
|
59
|
+
# Example 5: Static token authentication for development (NOT for production)
|
|
60
|
+
from golf.auth import configure_auth, StaticTokenConfig
|
|
61
|
+
|
|
62
|
+
configure_auth(
|
|
63
|
+
StaticTokenConfig(
|
|
64
|
+
tokens={
|
|
65
|
+
"dev-token-123": {
|
|
66
|
+
"client_id": "dev-client",
|
|
67
|
+
"scopes": ["read", "write"],
|
|
68
|
+
},
|
|
69
|
+
"admin-token-456": {
|
|
70
|
+
"client_id": "admin-client",
|
|
71
|
+
"scopes": ["read", "write", "admin"],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
required_scopes=["read"],
|
|
75
|
+
)
|
|
76
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Welcome prompt for new users."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
async def welcome() -> list[dict]:
|
|
5
|
+
"""Provide a welcome prompt for new users.
|
|
6
|
+
|
|
7
|
+
This is a simple example prompt that demonstrates how to define
|
|
8
|
+
a prompt template in GolfMCP.
|
|
9
|
+
"""
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
"role": "system",
|
|
13
|
+
"content": (
|
|
14
|
+
"You are an assistant for the {{project_name}} application. "
|
|
15
|
+
"You help users understand how to interact with this system and "
|
|
16
|
+
"its capabilities."
|
|
17
|
+
),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"role": "user",
|
|
21
|
+
"content": ("Welcome to {{project_name}}! This is a project built with GolfMCP. How can I get started?"),
|
|
22
|
+
},
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Designate the entry point function
|
|
27
|
+
export = welcome
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Current time resource example."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
# The URI that clients will use to access this resource
|
|
7
|
+
resource_uri = "system://time"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def current_time() -> dict[str, Any]:
|
|
11
|
+
"""Provide the current time in various formats.
|
|
12
|
+
|
|
13
|
+
This is a simple resource example that returns time in all formats.
|
|
14
|
+
"""
|
|
15
|
+
now = datetime.now()
|
|
16
|
+
|
|
17
|
+
# Prepare all possible formats
|
|
18
|
+
all_formats = {
|
|
19
|
+
"iso": now.isoformat(),
|
|
20
|
+
"rfc": now.strftime("%a, %d %b %Y %H:%M:%S %z"),
|
|
21
|
+
"unix": int(now.timestamp()),
|
|
22
|
+
"formatted": {
|
|
23
|
+
"date": now.strftime("%Y-%m-%d"),
|
|
24
|
+
"time": now.strftime("%H:%M:%S"),
|
|
25
|
+
"timezone": now.astimezone().tzname(),
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Return all formats
|
|
30
|
+
return all_formats
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Designate the entry point function
|
|
34
|
+
export = current_time
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Example resource that provides information about the project."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
resource_uri = "info://system"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def info() -> dict[str, Any]:
|
|
11
|
+
"""Provide system information as a resource.
|
|
12
|
+
|
|
13
|
+
This is a simple example resource that demonstrates how to expose
|
|
14
|
+
data to an LLM client through the MCP protocol.
|
|
15
|
+
"""
|
|
16
|
+
return {
|
|
17
|
+
"project": "{{project_name}}",
|
|
18
|
+
"timestamp": datetime.now().isoformat(),
|
|
19
|
+
"platform": {
|
|
20
|
+
"system": platform.system(),
|
|
21
|
+
"python_version": platform.python_version(),
|
|
22
|
+
"architecture": platform.machine(),
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Designate the entry point function
|
|
28
|
+
export = info
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Weather resource template example with URI parameters."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .client import weather_client
|
|
7
|
+
|
|
8
|
+
# The URI template that clients will use to access this resource
|
|
9
|
+
# The {city} parameter makes this a resource template
|
|
10
|
+
resource_uri = "weather://city/{city}"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def get_weather_for_city(city: str) -> dict[str, Any]:
|
|
14
|
+
"""Provide current weather for a specific city.
|
|
15
|
+
|
|
16
|
+
This example demonstrates:
|
|
17
|
+
1. Resource templates with URI parameters ({city})
|
|
18
|
+
2. Dynamic resource access based on parameters
|
|
19
|
+
3. Using shared client from the client.py file
|
|
20
|
+
4. FastMCP 2.11+ ResourceTemplate.from_function() usage
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
city: The city name to get weather for
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Weather data for the specified city
|
|
27
|
+
"""
|
|
28
|
+
# Use the shared weather client from client.py
|
|
29
|
+
weather_data = await weather_client.get_current(city)
|
|
30
|
+
|
|
31
|
+
# Add some additional data
|
|
32
|
+
weather_data.update(
|
|
33
|
+
{
|
|
34
|
+
"city": city,
|
|
35
|
+
"time": datetime.now().isoformat(),
|
|
36
|
+
"source": "GolfMCP Weather API",
|
|
37
|
+
"unit": "fahrenheit",
|
|
38
|
+
"resource_type": "template",
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return weather_data
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Designate the entry point function
|
|
46
|
+
export = get_weather_for_city
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Weather shared functionality.
|
|
2
|
+
|
|
3
|
+
This file demonstrates the recommended pattern for
|
|
4
|
+
sharing functionality across multiple resources in a directory.
|
|
5
|
+
Golf automatically discovers and includes shared Python files in builds.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
# Read configuration from environment variables
|
|
12
|
+
WEATHER_API_KEY = os.environ.get("WEATHER_API_KEY", "mock_key")
|
|
13
|
+
WEATHER_API_URL = os.environ.get("WEATHER_API_URL", "https://api.example.com/weather")
|
|
14
|
+
TEMPERATURE_UNIT = os.environ.get("WEATHER_TEMP_UNIT", "fahrenheit")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WeatherApiClient:
|
|
18
|
+
"""Mock weather API client."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, api_key: str = WEATHER_API_KEY, api_url: str = WEATHER_API_URL) -> None:
|
|
21
|
+
self.api_key = api_key
|
|
22
|
+
self.api_url = api_url
|
|
23
|
+
self.unit = TEMPERATURE_UNIT
|
|
24
|
+
|
|
25
|
+
async def get_forecast(self, city: str, days: int = 3) -> dict[str, Any]:
|
|
26
|
+
"""Get weather forecast for a city (mock implementation)."""
|
|
27
|
+
# This would make an API call in a real implementation
|
|
28
|
+
return {
|
|
29
|
+
"city": city,
|
|
30
|
+
"unit": self.unit,
|
|
31
|
+
"forecast": [{"day": i, "temp": 70 + i} for i in range(days)],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async def get_current(self, city: str) -> dict[str, Any]:
|
|
35
|
+
"""Get current weather for a city (mock implementation)."""
|
|
36
|
+
return {
|
|
37
|
+
"city": city,
|
|
38
|
+
"unit": self.unit,
|
|
39
|
+
"temperature": 72,
|
|
40
|
+
"conditions": "Sunny",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Create a shared weather client that can be imported by all resources in this directory
|
|
45
|
+
weather_client = WeatherApiClient()
|
|
46
|
+
|
|
47
|
+
# This could also define shared models or other utilities
|
|
48
|
+
# that would be common across weather-related resources
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Current weather resource example."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .client import weather_client
|
|
7
|
+
|
|
8
|
+
# The URI that clients will use to access this resource
|
|
9
|
+
resource_uri = "weather://current"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def current_weather() -> dict[str, Any]:
|
|
13
|
+
"""Provide current weather for a default city.
|
|
14
|
+
|
|
15
|
+
This example demonstrates:
|
|
16
|
+
1. Nested resource organization (resources/weather/current.py)
|
|
17
|
+
2. Resource without URI parameters
|
|
18
|
+
3. Using shared client from the client.py file
|
|
19
|
+
"""
|
|
20
|
+
# Use the shared weather client from client.py
|
|
21
|
+
weather_data = await weather_client.get_current("New York")
|
|
22
|
+
|
|
23
|
+
# Add some additional data
|
|
24
|
+
weather_data.update(
|
|
25
|
+
{
|
|
26
|
+
"time": datetime.now().isoformat(),
|
|
27
|
+
"source": "GolfMCP Weather API",
|
|
28
|
+
"unit": "fahrenheit",
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return weather_data
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Designate the entry point function
|
|
36
|
+
export = current_weather
|