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.
- golf/__init__.py +1 -1
- golf/auth/__init__.py +38 -26
- golf/auth/api_key.py +16 -23
- golf/auth/helpers.py +68 -54
- golf/auth/oauth.py +340 -277
- golf/auth/provider.py +58 -53
- golf/cli/__init__.py +1 -1
- golf/cli/main.py +209 -87
- golf/commands/__init__.py +1 -1
- golf/commands/build.py +31 -25
- golf/commands/init.py +81 -53
- golf/commands/run.py +30 -15
- golf/core/__init__.py +1 -1
- golf/core/builder.py +493 -362
- golf/core/builder_auth.py +115 -107
- golf/core/builder_telemetry.py +12 -9
- golf/core/config.py +62 -46
- golf/core/parser.py +174 -136
- golf/core/telemetry.py +216 -95
- golf/core/transformer.py +53 -55
- golf/examples/__init__.py +0 -1
- golf/examples/api_key/pre_build.py +2 -2
- golf/examples/api_key/tools/issues/create.py +35 -36
- golf/examples/api_key/tools/issues/list.py +42 -37
- golf/examples/api_key/tools/repos/list.py +50 -29
- golf/examples/api_key/tools/search/code.py +50 -37
- golf/examples/api_key/tools/users/get.py +21 -20
- golf/examples/basic/pre_build.py +4 -4
- golf/examples/basic/prompts/welcome.py +6 -7
- golf/examples/basic/resources/current_time.py +10 -9
- golf/examples/basic/resources/info.py +6 -5
- golf/examples/basic/resources/weather/common.py +16 -10
- golf/examples/basic/resources/weather/current.py +15 -11
- golf/examples/basic/resources/weather/forecast.py +15 -11
- golf/examples/basic/tools/github_user.py +19 -21
- golf/examples/basic/tools/hello.py +10 -6
- golf/examples/basic/tools/payments/charge.py +34 -25
- golf/examples/basic/tools/payments/common.py +8 -6
- golf/examples/basic/tools/payments/refund.py +29 -25
- golf/telemetry/__init__.py +6 -6
- golf/telemetry/instrumentation.py +455 -310
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/METADATA +1 -1
- golf_mcp-0.1.13.dist-info/RECORD +55 -0
- golf_mcp-0.1.11.dist-info/RECORD +0 -55
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/WHEEL +0 -0
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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__(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 (
|
|
118
|
-
|
|
119
|
-
isinstance(tree.body[0]
|
|
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,
|
|
120
|
+
if isinstance(node, ast.Import | ast.ImportFrom):
|
|
125
121
|
imports.append(node)
|
|
126
|
-
|
|
127
|
-
|
|
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,
|
|
143
|
+
if isinstance(node, ast.Import | ast.ImportFrom):
|
|
148
144
|
continue
|
|
149
|
-
|
|
145
|
+
|
|
150
146
|
# Skip the original docstring
|
|
151
|
-
if (
|
|
152
|
-
isinstance(node
|
|
153
|
-
isinstance(node.value
|
|
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
|
|
4
|
-
|
|
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:
|
|
14
|
-
issue_url:
|
|
15
|
-
error:
|
|
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[
|
|
22
|
-
|
|
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
|
|
38
|
-
return Output(
|
|
39
|
-
|
|
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(
|
|
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,
|
|
4
|
-
|
|
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
|
-
|
|
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[
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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,
|
|
4
|
-
|
|
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
|
-
|
|
13
|
+
|
|
14
|
+
repositories: list[dict[str, Any]]
|
|
13
15
|
total_count: int
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
async def list(
|
|
17
|
-
username: Annotated[
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|