golf-mcp 0.1.11__py3-none-any.whl → 0.1.13__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.

Potentially problematic release.


This version of golf-mcp might be problematic. Click here for more details.

Files changed (48) hide show
  1. golf/__init__.py +1 -1
  2. golf/auth/__init__.py +38 -26
  3. golf/auth/api_key.py +16 -23
  4. golf/auth/helpers.py +68 -54
  5. golf/auth/oauth.py +340 -277
  6. golf/auth/provider.py +58 -53
  7. golf/cli/__init__.py +1 -1
  8. golf/cli/main.py +209 -87
  9. golf/commands/__init__.py +1 -1
  10. golf/commands/build.py +31 -25
  11. golf/commands/init.py +81 -53
  12. golf/commands/run.py +30 -15
  13. golf/core/__init__.py +1 -1
  14. golf/core/builder.py +493 -362
  15. golf/core/builder_auth.py +115 -107
  16. golf/core/builder_telemetry.py +12 -9
  17. golf/core/config.py +62 -46
  18. golf/core/parser.py +174 -136
  19. golf/core/telemetry.py +216 -95
  20. golf/core/transformer.py +53 -55
  21. golf/examples/__init__.py +0 -1
  22. golf/examples/api_key/pre_build.py +2 -2
  23. golf/examples/api_key/tools/issues/create.py +35 -36
  24. golf/examples/api_key/tools/issues/list.py +42 -37
  25. golf/examples/api_key/tools/repos/list.py +50 -29
  26. golf/examples/api_key/tools/search/code.py +50 -37
  27. golf/examples/api_key/tools/users/get.py +21 -20
  28. golf/examples/basic/pre_build.py +4 -4
  29. golf/examples/basic/prompts/welcome.py +6 -7
  30. golf/examples/basic/resources/current_time.py +10 -9
  31. golf/examples/basic/resources/info.py +6 -5
  32. golf/examples/basic/resources/weather/common.py +16 -10
  33. golf/examples/basic/resources/weather/current.py +15 -11
  34. golf/examples/basic/resources/weather/forecast.py +15 -11
  35. golf/examples/basic/tools/github_user.py +19 -21
  36. golf/examples/basic/tools/hello.py +10 -6
  37. golf/examples/basic/tools/payments/charge.py +34 -25
  38. golf/examples/basic/tools/payments/common.py +8 -6
  39. golf/examples/basic/tools/payments/refund.py +29 -25
  40. golf/telemetry/__init__.py +6 -6
  41. golf/telemetry/instrumentation.py +455 -310
  42. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/METADATA +1 -1
  43. golf_mcp-0.1.13.dist-info/RECORD +55 -0
  44. golf_mcp-0.1.11.dist-info/RECORD +0 -55
  45. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/WHEEL +0 -0
  46. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
  47. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
  48. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/top_level.txt +0 -0
golf/core/transformer.py CHANGED
@@ -6,21 +6,23 @@ into explicit FastMCP component registrations.
6
6
 
7
7
  import ast
8
8
  from pathlib import Path
9
- from typing import Dict, Any
9
+ from typing import Any
10
10
 
11
11
  from golf.core.parser import ParsedComponent
12
12
 
13
13
 
14
14
  class ImportTransformer(ast.NodeTransformer):
15
15
  """AST transformer for rewriting imports in component files."""
16
-
17
- def __init__(self,
18
- original_path: Path,
19
- target_path: Path,
20
- import_map: Dict[str, str],
21
- project_root: Path):
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
+ ) -> None:
22
24
  """Initialize the import transformer.
23
-
25
+
24
26
  Args:
25
27
  original_path: Path to the original file
26
28
  target_path: Path to the target file
@@ -31,39 +33,35 @@ class ImportTransformer(ast.NodeTransformer):
31
33
  self.target_path = target_path
32
34
  self.import_map = import_map
33
35
  self.project_root = project_root
34
-
36
+
35
37
  def visit_Import(self, node: ast.Import) -> Any:
36
38
  """Transform import statements."""
37
39
  return node
38
-
40
+
39
41
  def visit_ImportFrom(self, node: ast.ImportFrom) -> Any:
40
42
  """Transform import from statements."""
41
43
  if node.module is None:
42
44
  return node
43
-
45
+
44
46
  # Handle relative imports
45
47
  if node.level > 0:
46
48
  # Calculate the source module path
47
49
  source_dir = self.original_path.parent
48
50
  for _ in range(node.level - 1):
49
51
  source_dir = source_dir.parent
50
-
52
+
51
53
  if node.module:
52
54
  source_module = source_dir / node.module.replace(".", "/")
53
55
  else:
54
56
  source_module = source_dir
55
-
57
+
56
58
  # Check if this is a common module import
57
59
  source_str = str(source_module.relative_to(self.project_root))
58
60
  if source_str in self.import_map:
59
61
  # Replace with absolute import
60
62
  new_module = self.import_map[source_str]
61
- return ast.ImportFrom(
62
- module=new_module,
63
- names=node.names,
64
- level=0
65
- )
66
-
63
+ return ast.ImportFrom(module=new_module, names=node.names, level=0)
64
+
67
65
  return node
68
66
 
69
67
 
@@ -71,18 +69,18 @@ def transform_component(
71
69
  component: ParsedComponent,
72
70
  output_file: Path,
73
71
  project_path: Path,
74
- import_map: Dict[str, str],
72
+ import_map: dict[str, str],
75
73
  source_file: Path = None,
76
74
  ) -> str:
77
75
  """Transform a GolfMCP component into a standalone FastMCP component.
78
-
76
+
79
77
  Args:
80
78
  component: Parsed component to transform
81
79
  output_file: Path to write the transformed component to
82
80
  project_path: Path to the project root
83
81
  import_map: Mapping of original module paths to generated paths
84
82
  source_file: Optional path to source file (for common.py files)
85
-
83
+
86
84
  Returns:
87
85
  Generated component code
88
86
  """
@@ -93,43 +91,41 @@ def transform_component(
93
91
  file_path = Path(component.file_path)
94
92
  else:
95
93
  raise ValueError("Either component or source_file must be provided")
96
-
97
- with open(file_path, "r") as f:
94
+
95
+ with open(file_path) as f:
98
96
  source_code = f.read()
99
-
97
+
100
98
  # Parse the source code into an AST
101
99
  tree = ast.parse(source_code)
102
-
100
+
103
101
  # Transform imports
104
- transformer = ImportTransformer(
105
- file_path,
106
- output_file,
107
- import_map,
108
- project_path
109
- )
102
+ transformer = ImportTransformer(file_path, output_file, import_map, project_path)
110
103
  tree = transformer.visit(tree)
111
-
104
+
112
105
  # Get all imports and docstring
113
106
  imports = []
114
107
  docstring = None
115
-
108
+
116
109
  # Find the module docstring if present
117
- if (len(tree.body) > 0 and isinstance(tree.body[0], ast.Expr) and
118
- isinstance(tree.body[0].value, ast.Constant) and
119
- isinstance(tree.body[0].value.value, str)):
110
+ if (
111
+ len(tree.body) > 0
112
+ and isinstance(tree.body[0], ast.Expr)
113
+ and isinstance(tree.body[0].value, ast.Constant)
114
+ and isinstance(tree.body[0].value.value, str)
115
+ ):
120
116
  docstring = tree.body[0].value.value
121
-
117
+
122
118
  # Find imports
123
119
  for node in tree.body:
124
- if isinstance(node, (ast.Import, ast.ImportFrom)):
120
+ if isinstance(node, ast.Import | ast.ImportFrom):
125
121
  imports.append(node)
126
-
127
- # Generate the transformed code
122
+
123
+ # Generate the transformed code
128
124
  transformed_imports = ast.unparse(ast.Module(body=imports, type_ignores=[]))
129
-
125
+
130
126
  # Build full transformed code
131
127
  transformed_code = transformed_imports + "\n\n"
132
-
128
+
133
129
  # Add docstring if present, using proper triple quotes for multi-line docstrings
134
130
  if docstring:
135
131
  # Check if docstring contains newlines
@@ -139,30 +135,32 @@ def transform_component(
139
135
  else:
140
136
  # Use single quotes for single-line docstrings
141
137
  transformed_code += f'"{docstring}"\n\n'
142
-
138
+
143
139
  # Add the rest of the code except imports and the original docstring
144
140
  remaining_nodes = []
145
141
  for node in tree.body:
146
142
  # Skip imports
147
- if isinstance(node, (ast.Import, ast.ImportFrom)):
143
+ if isinstance(node, ast.Import | ast.ImportFrom):
148
144
  continue
149
-
145
+
150
146
  # Skip the original docstring
151
- if (isinstance(node, ast.Expr) and
152
- isinstance(node.value, ast.Constant) and
153
- isinstance(node.value.value, str)):
147
+ if (
148
+ isinstance(node, ast.Expr)
149
+ and isinstance(node.value, ast.Constant)
150
+ and isinstance(node.value.value, str)
151
+ ):
154
152
  continue
155
-
153
+
156
154
  remaining_nodes.append(node)
157
-
155
+
158
156
  remaining_code = ast.unparse(ast.Module(body=remaining_nodes, type_ignores=[]))
159
157
  transformed_code += remaining_code
160
-
158
+
161
159
  # Ensure the directory exists
162
160
  output_file.parent.mkdir(parents=True, exist_ok=True)
163
-
161
+
164
162
  # Write the transformed code to the output file
165
163
  with open(output_file, "w") as f:
166
164
  f.write(transformed_code)
167
-
168
- return transformed_code
165
+
166
+ return transformed_code
golf/examples/__init__.py CHANGED
@@ -1 +0,0 @@
1
-
@@ -7,5 +7,5 @@ from golf.auth import configure_api_key
7
7
  configure_api_key(
8
8
  header_name="Authorization",
9
9
  header_prefix="Bearer ", # Will handle both "Bearer " and "token " prefixes
10
- required=True # Reject requests without a valid API key
11
- )
10
+ required=True, # Reject requests without a valid API key
11
+ )
@@ -1,55 +1,55 @@
1
1
  """Create a new issue in a GitHub repository."""
2
2
 
3
- from typing import Annotated, List, Optional
4
- from pydantic import BaseModel, Field
3
+ from typing import Annotated
4
+
5
5
  import httpx
6
+ from pydantic import BaseModel, Field
6
7
 
7
8
  from golf.auth import get_api_key
8
9
 
9
10
 
10
11
  class Output(BaseModel):
11
12
  """Response from creating an issue."""
13
+
12
14
  success: bool
13
- issue_number: Optional[int] = None
14
- issue_url: Optional[str] = None
15
- error: Optional[str] = None
15
+ issue_number: int | None = None
16
+ issue_url: str | None = None
17
+ error: str | None = None
16
18
 
17
19
 
18
20
  async def create(
19
21
  repo: Annotated[str, Field(description="Repository name in format 'owner/repo'")],
20
22
  title: Annotated[str, Field(description="Issue title")],
21
- body: Annotated[str, Field(description="Issue description/body (supports Markdown)")] = "",
22
- labels: Annotated[Optional[List[str]], Field(description="List of label names to apply")] = None
23
+ body: Annotated[
24
+ str, Field(description="Issue description/body (supports Markdown)")
25
+ ] = "",
26
+ labels: Annotated[
27
+ list[str] | None, Field(description="List of label names to apply")
28
+ ] = None,
23
29
  ) -> Output:
24
30
  """Create a new issue.
25
-
31
+
26
32
  Requires authentication with appropriate permissions.
27
33
  """
28
34
  github_token = get_api_key()
29
-
35
+
30
36
  if not github_token:
31
37
  return Output(
32
38
  success=False,
33
- error="Authentication required. Please provide a GitHub token."
39
+ error="Authentication required. Please provide a GitHub token.",
34
40
  )
35
-
41
+
36
42
  # Validate repo format
37
- if '/' not in repo:
38
- return Output(
39
- success=False,
40
- error="Repository must be in format 'owner/repo'"
41
- )
42
-
43
+ if "/" not in repo:
44
+ return Output(success=False, error="Repository must be in format 'owner/repo'")
45
+
43
46
  url = f"https://api.github.com/repos/{repo}/issues"
44
-
47
+
45
48
  # Build request payload
46
- payload = {
47
- "title": title,
48
- "body": body
49
- }
49
+ payload = {"title": title, "body": body}
50
50
  if labels:
51
51
  payload["labels"] = labels
52
-
52
+
53
53
  try:
54
54
  async with httpx.AsyncClient() as client:
55
55
  response = await client.post(
@@ -57,38 +57,37 @@ async def create(
57
57
  headers={
58
58
  "Authorization": f"Bearer {github_token}",
59
59
  "Accept": "application/vnd.github.v3+json",
60
- "User-Agent": "Golf-GitHub-MCP-Server"
60
+ "User-Agent": "Golf-GitHub-MCP-Server",
61
61
  },
62
62
  json=payload,
63
- timeout=10.0
63
+ timeout=10.0,
64
64
  )
65
-
65
+
66
66
  response.raise_for_status()
67
67
  issue_data = response.json()
68
-
68
+
69
69
  return Output(
70
70
  success=True,
71
71
  issue_number=issue_data["number"],
72
- issue_url=issue_data["html_url"]
72
+ issue_url=issue_data["html_url"],
73
73
  )
74
-
74
+
75
75
  except httpx.HTTPStatusError as e:
76
76
  error_messages = {
77
77
  401: "Invalid or missing authentication token",
78
78
  403: "Insufficient permissions to create issues in this repository",
79
79
  404: "Repository not found",
80
- 422: "Invalid request data"
80
+ 422: "Invalid request data",
81
81
  }
82
82
  return Output(
83
83
  success=False,
84
- error=error_messages.get(e.response.status_code, f"GitHub API error: {e.response.status_code}")
84
+ error=error_messages.get(
85
+ e.response.status_code, f"GitHub API error: {e.response.status_code}"
86
+ ),
85
87
  )
86
88
  except Exception as e:
87
- return Output(
88
- success=False,
89
- error=f"Failed to create issue: {str(e)}"
90
- )
89
+ return Output(success=False, error=f"Failed to create issue: {str(e)}")
91
90
 
92
91
 
93
92
  # Export the function to be used as the tool
94
- export = create
93
+ export = create
@@ -1,87 +1,92 @@
1
1
  """List issues in a GitHub repository."""
2
2
 
3
- from typing import Annotated, List, Optional, Dict, Any
4
- from pydantic import BaseModel, Field
3
+ from typing import Annotated, Any
4
+
5
5
  import httpx
6
+ from pydantic import BaseModel, Field
6
7
 
7
8
  from golf.auth import get_api_key
8
9
 
9
10
 
10
11
  class Output(BaseModel):
11
12
  """List of issues from the repository."""
12
- issues: List[Dict[str, Any]]
13
+
14
+ issues: list[dict[str, Any]]
13
15
  total_count: int
14
16
 
15
17
 
16
18
  async def list(
17
19
  repo: Annotated[str, Field(description="Repository name in format 'owner/repo'")],
18
- state: Annotated[str, Field(description="Filter by state - 'open', 'closed', or 'all'")] = "open",
19
- labels: Annotated[Optional[str], Field(description="Comma-separated list of label names to filter by")] = None,
20
- per_page: Annotated[int, Field(description="Number of results per page (max 100)")] = 20
20
+ state: Annotated[
21
+ str, Field(description="Filter by state - 'open', 'closed', or 'all'")
22
+ ] = "open",
23
+ labels: Annotated[
24
+ str | None,
25
+ Field(description="Comma-separated list of label names to filter by"),
26
+ ] = None,
27
+ per_page: Annotated[
28
+ int, Field(description="Number of results per page (max 100)")
29
+ ] = 20,
21
30
  ) -> Output:
22
31
  """List issues in a repository.
23
-
32
+
24
33
  Returns issues with their number, title, state, and other metadata.
25
34
  Pull requests are filtered out from the results.
26
35
  """
27
36
  github_token = get_api_key()
28
-
37
+
29
38
  # Validate repo format
30
- if '/' not in repo:
39
+ if "/" not in repo:
31
40
  return Output(issues=[], total_count=0)
32
-
41
+
33
42
  url = f"https://api.github.com/repos/{repo}/issues"
34
-
43
+
35
44
  # Prepare headers
36
45
  headers = {
37
46
  "Accept": "application/vnd.github.v3+json",
38
- "User-Agent": "Golf-GitHub-MCP-Server"
47
+ "User-Agent": "Golf-GitHub-MCP-Server",
39
48
  }
40
49
  if github_token:
41
50
  headers["Authorization"] = f"Bearer {github_token}"
42
-
51
+
43
52
  # Build query parameters
44
- params = {
45
- "state": state,
46
- "per_page": min(per_page, 100)
47
- }
53
+ params = {"state": state, "per_page": min(per_page, 100)}
48
54
  if labels:
49
55
  params["labels"] = labels
50
-
56
+
51
57
  try:
52
58
  async with httpx.AsyncClient() as client:
53
59
  response = await client.get(
54
- url,
55
- headers=headers,
56
- params=params,
57
- timeout=10.0
60
+ url, headers=headers, params=params, timeout=10.0
58
61
  )
59
-
62
+
60
63
  response.raise_for_status()
61
64
  issues_data = response.json()
62
-
65
+
63
66
  # Filter out pull requests and format issues
64
67
  issues = []
65
68
  for issue in issues_data:
66
69
  # Skip pull requests (they appear in issues endpoint too)
67
70
  if "pull_request" in issue:
68
71
  continue
69
-
70
- issues.append({
71
- "number": issue["number"],
72
- "title": issue["title"],
73
- "body": issue.get("body", ""),
74
- "state": issue["state"],
75
- "url": issue["html_url"],
76
- "user": issue["user"]["login"],
77
- "labels": [label["name"] for label in issue.get("labels", [])]
78
- })
79
-
72
+
73
+ issues.append(
74
+ {
75
+ "number": issue["number"],
76
+ "title": issue["title"],
77
+ "body": issue.get("body", ""),
78
+ "state": issue["state"],
79
+ "url": issue["html_url"],
80
+ "user": issue["user"]["login"],
81
+ "labels": [label["name"] for label in issue.get("labels", [])],
82
+ }
83
+ )
84
+
80
85
  return Output(issues=issues, total_count=len(issues))
81
-
86
+
82
87
  except Exception:
83
88
  return Output(issues=[], total_count=0)
84
89
 
85
90
 
86
91
  # Export the function to be used as the tool
87
- export = list
92
+ export = list
@@ -1,31 +1,50 @@
1
1
  """List GitHub repositories for a user or organization."""
2
2
 
3
- from typing import Annotated, List, Optional, Dict, Any
4
- from pydantic import BaseModel, Field
3
+ from typing import Annotated, Any
4
+
5
5
  import httpx
6
+ from pydantic import BaseModel, Field
6
7
 
7
8
  from golf.auth import get_api_key
8
9
 
9
10
 
10
11
  class Output(BaseModel):
11
12
  """List of repositories."""
12
- repositories: List[Dict[str, Any]]
13
+
14
+ repositories: list[dict[str, Any]]
13
15
  total_count: int
14
16
 
15
17
 
16
18
  async def list(
17
- username: Annotated[Optional[str], Field(description="GitHub username (lists public repos, or all repos if authenticated as this user)")] = None,
18
- org: Annotated[Optional[str], Field(description="GitHub organization name (lists public repos, or all repos if authenticated member)")] = None,
19
- sort: Annotated[str, Field(description="How to sort results - 'created', 'updated', 'pushed', 'full_name'")] = "updated",
20
- per_page: Annotated[int, Field(description="Number of results per page (max 100)")] = 20
19
+ username: Annotated[
20
+ str | None,
21
+ Field(
22
+ description="GitHub username (lists public repos, or all repos if authenticated as this user)"
23
+ ),
24
+ ] = None,
25
+ org: Annotated[
26
+ str | None,
27
+ Field(
28
+ description="GitHub organization name (lists public repos, or all repos if authenticated member)"
29
+ ),
30
+ ] = None,
31
+ sort: Annotated[
32
+ str,
33
+ Field(
34
+ description="How to sort results - 'created', 'updated', 'pushed', 'full_name'"
35
+ ),
36
+ ] = "updated",
37
+ per_page: Annotated[
38
+ int, Field(description="Number of results per page (max 100)")
39
+ ] = 20,
21
40
  ) -> Output:
22
41
  """List GitHub repositories.
23
-
42
+
24
43
  If neither username nor org is provided, lists repositories for the authenticated user.
25
44
  """
26
45
  # Get the GitHub token from the request context
27
46
  github_token = get_api_key()
28
-
47
+
29
48
  # Determine the API endpoint
30
49
  if org:
31
50
  url = f"https://api.github.com/orgs/{org}/repos"
@@ -36,15 +55,15 @@ async def list(
36
55
  if not github_token:
37
56
  return Output(repositories=[], total_count=0)
38
57
  url = "https://api.github.com/user/repos"
39
-
58
+
40
59
  # Prepare headers
41
60
  headers = {
42
61
  "Accept": "application/vnd.github.v3+json",
43
- "User-Agent": "Golf-GitHub-MCP-Server"
62
+ "User-Agent": "Golf-GitHub-MCP-Server",
44
63
  }
45
64
  if github_token:
46
65
  headers["Authorization"] = f"Bearer {github_token}"
47
-
66
+
48
67
  try:
49
68
  async with httpx.AsyncClient() as client:
50
69
  response = await client.get(
@@ -53,30 +72,32 @@ async def list(
53
72
  params={
54
73
  "sort": sort,
55
74
  "per_page": min(per_page, 100),
56
- "type": "all" if not username and not org else None
75
+ "type": "all" if not username and not org else None,
57
76
  },
58
- timeout=10.0
77
+ timeout=10.0,
59
78
  )
60
-
79
+
61
80
  response.raise_for_status()
62
81
  repos_data = response.json()
63
-
82
+
64
83
  # Format repositories
65
84
  repositories = []
66
85
  for repo in repos_data:
67
- repositories.append({
68
- "name": repo["name"],
69
- "full_name": repo["full_name"],
70
- "description": repo.get("description", ""),
71
- "private": repo.get("private", False),
72
- "stars": repo.get("stargazers_count", 0),
73
- "forks": repo.get("forks_count", 0),
74
- "language": repo.get("language", ""),
75
- "url": repo["html_url"]
76
- })
77
-
86
+ repositories.append(
87
+ {
88
+ "name": repo["name"],
89
+ "full_name": repo["full_name"],
90
+ "description": repo.get("description", ""),
91
+ "private": repo.get("private", False),
92
+ "stars": repo.get("stargazers_count", 0),
93
+ "forks": repo.get("forks_count", 0),
94
+ "language": repo.get("language", ""),
95
+ "url": repo["html_url"],
96
+ }
97
+ )
98
+
78
99
  return Output(repositories=repositories, total_count=len(repositories))
79
-
100
+
80
101
  except httpx.HTTPStatusError as e:
81
102
  if e.response.status_code in [401, 404]:
82
103
  return Output(repositories=[], total_count=0)
@@ -87,4 +108,4 @@ async def list(
87
108
 
88
109
 
89
110
  # Export the function to be used as the tool
90
- export = list
111
+ export = list