Skip to content

Commit e6fbbd6

Browse files
authored
refactor: migrate to dynamic tool registration with inspect module and modernise configuration (#107)
* feat: refactor tool registration to use inspect for dynamic method addition * feat: update startCommand type to http and add runtime and env configuration * feat: add update_assignees method to modify assignees for GitHub issues and PRs
1 parent 10e3efa commit e6fbbd6

File tree

5 files changed

+142
-388
lines changed

5 files changed

+142
-388
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "mcp-github-pr-issue-analyser"
3-
version = "2.8.0"
3+
version = "3.0.0"
44
description = "MCP GitHub Issues Create/Update and PR Analyse"
55
readme = "README.md"
66
requires-python = ">=3.12"

smithery.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# Smithery configuration file: https://smithery.ai/docs/use/session-config
22

3+
runtime: "python"
4+
env:
5+
MCP_ENABLE_REMOTE: "true"
6+
37
startCommand:
4-
type: stdio
8+
type: "http"
59
configSchema:
610
type: object
711
required: ["githubToken"]

src/mcp_github/github_integration.py

Lines changed: 98 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,11 @@ class GitHubIntegration:
3535

3636
def __init__(self):
3737
"""
38-
Initialise the GitHubIntegration class.
39-
38+
Initializes the GitHubIntegration instance by setting up the GitHub token from environment variables.
4039
Returns:
4140
None
42-
43-
Raises:
44-
ValueError: If the GitHub token is not found in environment variables.
41+
Error Handling:
42+
Raises ValueError if the GITHUB_TOKEN environment variable is not set.
4543
"""
4644
self.github_token = GITHUB_TOKEN
4745
if not self.github_token:
@@ -51,24 +49,23 @@ def __init__(self):
5149

5250
def _get_headers(self):
5351
"""
54-
Return the headers required for GitHub API requests.
52+
Constructs the HTTP headers required for GitHub API requests, including the authorization token.
5553
Returns:
5654
dict: A dictionary containing the required HTTP headers.
57-
Raises:
58-
ValueError: If the GitHub token is missing.
5955
Error Handling:
6056
Raises ValueError if the GitHub token is not set.
6157
"""
6258
if not self.github_token:
6359
raise ValueError("GitHub token is missing for API requests")
64-
return {
60+
headers = {
6561
'Authorization': f'token {self.github_token}',
6662
'Accept': 'application/vnd.github.v3+json'
6763
}
64+
return headers
6865

6966
def _get_pr_url(self, repo_owner: str, repo_name: str, pr_number: int) -> str:
7067
"""
71-
Generates the GitHub API URL for a specific pull request in a given repository.
68+
Construct the GitHub API URL for a specific pull request.
7269
Args:
7370
repo_owner (str): The owner of the GitHub repository.
7471
repo_name (str): The name of the GitHub repository.
@@ -78,12 +75,12 @@ def _get_pr_url(self, repo_owner: str, repo_name: str, pr_number: int) -> str:
7875
Raises:
7976
ValueError: If any of the arguments are empty or if pr_number is not a positive integer.
8077
"""
81-
82-
return f"https://api.github.com/repos/{repo_owner}/{repo_name}/pulls/{pr_number}"
78+
url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/pulls/{pr_number}"
79+
return url
8380

8481
def get_pr_diff(self, repo_owner: str, repo_name: str, pr_number: int) -> str:
8582
"""
86-
Fetches the diff/patch of a pull request from a GitHub repository.
83+
Fetches the diff/patch of a specific pull request from a GitHub repository.
8784
Args:
8885
repo_owner (str): The owner of the GitHub repository.
8986
repo_name (str): The name of the GitHub repository.
@@ -107,11 +104,11 @@ def get_pr_diff(self, repo_owner: str, repo_name: str, pr_number: int) -> str:
107104
except Exception as e:
108105
logging.error(f"Error fetching PR diff: {str(e)}")
109106
traceback.print_exc()
110-
return None
107+
return str(e)
111108

112109
def get_pr_content(self, repo_owner: str, repo_name: str, pr_number: int) -> Dict[str, Any]:
113110
"""
114-
Fetches the content of a specific pull request from a GitHub repository.
111+
Fetches the content/details of a specific pull request from a GitHub repository.
115112
Args:
116113
repo_owner (str): The owner of the repository.
117114
repo_name (str): The name of the repository.
@@ -149,11 +146,11 @@ def get_pr_content(self, repo_owner: str, repo_name: str, pr_number: int) -> Dic
149146
except Exception as e:
150147
logging.error(f"Error fetching PR content: {str(e)}")
151148
traceback.print_exc()
152-
return None
149+
return {"status": "error", "message": str(e)}
153150

154151
def add_pr_comments(self, repo_owner: str, repo_name: str, pr_number: int, comment: str) -> Dict[str, Any]:
155152
"""
156-
Adds a comment to a specified pull request on GitHub.
153+
Adds a comment to a specific pull request on GitHub.
157154
Args:
158155
repo_owner (str): The owner of the repository.
159156
repo_name (str): The name of the repository.
@@ -182,24 +179,21 @@ def add_pr_comments(self, repo_owner: str, repo_name: str, pr_number: int, comme
182179
except Exception as e:
183180
logging.error(f"Error adding comment: {str(e)}")
184181
traceback.print_exc()
185-
return None
182+
return {"status": "error", "message": str(e)}
186183

187184
def add_inline_pr_comment(self, repo_owner: str, repo_name: str, pr_number: int, path: str, line: int, comment_body: str) -> Dict[str, Any]:
188185
"""
189-
Adds an inline review comment to a specific line in a pull request file.
190-
186+
Adds an inline review comment to a specific line in a file within a pull request on GitHub.
191187
Args:
192188
repo_owner (str): The owner of the repository.
193189
repo_name (str): The name of the repository.
194190
pr_number (int): The pull request number.
195191
path (str): The relative path to the file (e.g., 'src/main.py').
196192
line (int): The line number in the file to comment on.
197193
comment_body (str): The content of the review comment.
198-
199194
Returns:
200195
Dict[str, Any]: The JSON response from the GitHub API containing the comment data if successful.
201196
None: If an error occurs while adding the comment.
202-
203197
Error Handling:
204198
Logs an error message and prints the traceback if the request fails or an exception is raised.
205199
"""
@@ -233,11 +227,11 @@ def add_inline_pr_comment(self, repo_owner: str, repo_name: str, pr_number: int,
233227
except Exception as e:
234228
logging.error(f"Error adding inline review comment: {str(e)}")
235229
traceback.print_exc()
236-
return None
230+
return {"status": "error", "message": str(e)}
237231

238232
def update_pr_description(self, repo_owner: str, repo_name: str, pr_number: int, new_title: str, new_description: str) -> Dict[str, Any]:
239233
"""
240-
Updates the title and description of a pull request on GitHub.
234+
Updates the title and description (body) of a specific pull request on GitHub.
241235
Args:
242236
repo_owner (str): The owner of the repository.
243237
repo_name (str): The name of the repository.
@@ -268,7 +262,7 @@ def update_pr_description(self, repo_owner: str, repo_name: str, pr_number: int,
268262
except Exception as e:
269263
logging.error(f"Error updating PR description: {str(e)}")
270264
traceback.print_exc()
271-
return None
265+
return {"status": "error", "message": str(e)}
272266

273267
def list_open_issues_prs(
274268
self,
@@ -279,7 +273,7 @@ def list_open_issues_prs(
279273
page: int = 1
280274
) -> Dict[str, Any]:
281275
"""
282-
Lists all open Issues or Pull Requests for a given repository owner.
276+
Lists open pull requests or issues for a specified GitHub repository owner.
283277
Args:
284278
repo_owner (str): The owner of the repository.
285279
issue (Literal['pr', 'issue']): The type of items to list, either 'pr' for pull requests or 'issue' for issues. Defaults to 'pr'.
@@ -325,12 +319,12 @@ def list_open_issues_prs(
325319
except Exception as e:
326320
logging.error(f"Error listing open {issue}s: {str(e)}")
327321
traceback.print_exc()
328-
return None
322+
return {"status": "error", "message": str(e)}
329323

330324
def create_issue(self, repo_owner: str, repo_name: str, title: str, body: str, labels: list[str]) -> Dict[str, Any]:
331325
"""
332326
Creates a new issue in the specified GitHub repository.
333-
When issue is created add comment to PR with "Resolves: #<issue_number>" using update_pr_description function.
327+
If the issue is created successfully, a link to the issue must be appended in the PR's description.
334328
Args:
335329
repo_owner (str): The owner of the repository.
336330
repo_name (str): The name of the repository.
@@ -365,11 +359,11 @@ def create_issue(self, repo_owner: str, repo_name: str, title: str, body: str, l
365359
except Exception as e:
366360
logging.error(f"Error creating issue: {str(e)}")
367361
traceback.print_exc()
368-
return None
362+
return {"status": "error", "message": str(e)}
369363

370364
def merge_pr(self, repo_owner: str, repo_name: str, pr_number: int, commit_title: Optional[str] = None, commit_message: Optional[str] = None, merge_method: Literal['merge', 'squash', 'rebase'] = 'squash') -> Dict[str, Any]:
371365
"""
372-
Merges a pull request in the specified GitHub repository.
366+
Merges a specific pull request in a GitHub repository using the specified merge method.
373367
Args:
374368
repo_owner (str): The owner of the repository.
375369
repo_name (str): The name of the repository.
@@ -406,7 +400,7 @@ def merge_pr(self, repo_owner: str, repo_name: str, pr_number: int, commit_title
406400

407401
def update_issue(self, repo_owner: str, repo_name: str, issue_number: int, title: str, body: str, labels: list[str] = [], state: Literal['open', 'closed'] = 'open') -> Dict[str, Any]:
408402
"""
409-
Updates an existing GitHub issue with the specified parameters.
403+
Updates an existing issue in the specified GitHub repository.
410404
Args:
411405
repo_owner (str): The owner of the repository.
412406
repo_name (str): The name of the repository.
@@ -441,11 +435,78 @@ def update_issue(self, repo_owner: str, repo_name: str, issue_number: int, title
441435
except Exception as e:
442436
logging.error(f"Error updating issue: {str(e)}")
443437
traceback.print_exc()
444-
return None
438+
return {"status": "error", "message": str(e)}
439+
440+
def update_reviews(self, repo_owner: str, repo_name: str, pr_number: int, event: Literal['APPROVE', 'REQUEST_CHANGES', 'COMMENT'], body: Optional[str] = None) -> Dict[str, Any]:
441+
"""
442+
Submits a review for a specific pull request in a GitHub repository.
443+
Args:
444+
repo_owner (str): The owner of the repository.
445+
repo_name (str): The name of the repository.
446+
pr_number (int): The pull request number to review.
447+
event (Literal['APPROVE', 'REQUEST_CHANGES', 'COMMENT']): The type of review event.
448+
body (str, optional): Required when using REQUEST_CHANGES or COMMENT for the event parameter. Defaults to None.
449+
Returns:
450+
Dict[str, Any]: The JSON response from the GitHub API containing review information if successful.
451+
None: If an error occurs during the review submission process.
452+
Error Handling:
453+
Logs errors and prints the traceback if the review submission fails, returning None.
454+
"""
455+
logging.info(f"Submitting review for PR {repo_owner}/{repo_name}#{pr_number}")
456+
457+
# Construct the reviews URL
458+
reviews_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/pulls/{pr_number}/reviews"
459+
460+
try:
461+
response = requests.post(reviews_url, headers=self._get_headers(), json={
462+
'body': body,
463+
'event': event
464+
})
465+
response.raise_for_status()
466+
review_data = response.json()
467+
468+
logging.info(f"Review submitted successfully")
469+
return review_data
470+
471+
except Exception as e:
472+
logging.error(f"Error submitting review: {str(e)}")
473+
traceback.print_exc()
474+
return {"status": "error", "message": str(e)}
475+
476+
def update_assignees(self, repo_owner: str, repo_name: str, issue_number: int, assignees: list[str]) -> Dict[str, Any]:
477+
"""
478+
Updates the assignees for a specific issue or pull request in a GitHub repository.
479+
Args:
480+
repo_owner (str): The owner of the repository.
481+
repo_name (str): The name of the repository.
482+
issue_number (int): The issue or pull request number to update.
483+
assignees (list[str]): A list of usernames to assign to the issue or pull request.
484+
Returns:
485+
Dict[str, Any]: The updated issue or pull request data as returned by the GitHub API if the update is successful.
486+
None: If an error occurs during the update process.
487+
Error Handling:
488+
Logs an error message and prints the traceback if the request fails or an exception is raised.
489+
"""
490+
logging.info(f"Updating assignees for issue/PR {repo_owner}/{repo_name}#{issue_number}")
491+
# Construct the issue URL
492+
issue_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/issues/{issue_number}"
493+
try:
494+
# Update the assignees
495+
response = requests.patch(issue_url, headers=self._get_headers(), json={
496+
'assignees': assignees
497+
})
498+
response.raise_for_status()
499+
issue_data = response.json()
500+
logging.info(f"Assignees updated successfully")
501+
return issue_data
502+
except Exception as e:
503+
logging.error(f"Error updating assignees: {str(e)}")
504+
traceback.print_exc()
505+
return {"status": "error", "message": str(e)}
445506

446507
def get_latest_sha(self, repo_owner: str, repo_name: str) -> Optional[str]:
447508
"""
448-
Fetches the latest commit SHA from a specified GitHub repository.
509+
Fetches the SHA of the latest commit in the specified GitHub repository.
449510
Args:
450511
repo_owner (str): The owner of the GitHub repository.
451512
repo_name (str): The name of the GitHub repository.
@@ -472,12 +533,12 @@ def get_latest_sha(self, repo_owner: str, repo_name: str) -> Optional[str]:
472533
return latest_sha
473534
else:
474535
logging.warning({"status": "warning", "message": "No commits found in the repository"})
475-
return None
536+
return "No commits found in the repository"
476537

477538
except Exception as e:
478539
logging.error(f"Error fetching latest commit SHA: {str(e)}")
479540
traceback.print_exc()
480-
return None
541+
return str(e)
481542

482543
def create_tag(self, repo_owner: str, repo_name: str, tag_name: str, message: str) -> Dict[str, Any]:
483544
"""
@@ -517,7 +578,7 @@ def create_tag(self, repo_owner: str, repo_name: str, tag_name: str, message: st
517578
except Exception as e:
518579
logging.error(f"Error creating tag: {str(e)}")
519580
traceback.print_exc()
520-
return None
581+
return {"status": "error", "message": str(e)}
521582

522583
def create_release(self, repo_owner: str, repo_name: str, tag_name: str, release_name: str, body: str) -> Dict[str, Any]:
523584
"""
@@ -558,4 +619,4 @@ def create_release(self, repo_owner: str, repo_name: str, tag_name: str, release
558619
except Exception as e:
559620
logging.error(f"Error creating release: {str(e)}")
560621
traceback.print_exc()
561-
return None
622+
return {"status": "error", "message": str(e)}

0 commit comments

Comments
 (0)