Merge branch 'feat/mcp_servers' into feat/matt
Browse files- app.py +11 -3
- pmcp/mcp_server/github/__init__.py +0 -0
- pmcp/mcp_server/github/github.py +40 -0
- pmcp/mcp_server/github/mcp_github_main.py +35 -0
- pmcp/mcp_server/github/models.py +20 -0
- pmcp/mcp_server/github/services/__init__.py +0 -0
- pmcp/mcp_server/github/services/branches.py +21 -0
- pmcp/mcp_server/github/services/contents.py +44 -0
- pmcp/mcp_server/github/services/issues.py +86 -0
- pmcp/mcp_server/github/services/pull_requests.py +58 -0
- pmcp/mcp_server/github/services/repo.py +21 -0
- pmcp/mcp_server/github/services/repo_to_text.py +26 -0
- pmcp/mcp_server/github/tools/__init__.py +1 -0
- pmcp/mcp_server/github/tools/branches.py +31 -0
- pmcp/mcp_server/github/tools/contents.py +62 -0
- pmcp/mcp_server/github/tools/issues.py +110 -0
- pmcp/mcp_server/github/tools/pull_requests.py +68 -0
- pmcp/mcp_server/github/tools/repo.py +35 -0
- pmcp/mcp_server/github/tools/repo_to_text.py +28 -0
- pmcp/mcp_server/github/tools/tools.py +31 -0
- pmcp/mcp_server/github/utils/__init__.py +0 -0
- pmcp/mcp_server/github/utils/github_api.py +67 -0
- pmcp/mcp_server/github/utils/repo_to_text_utils.py +83 -0
- pmcp/mcp_server/trello/__init__.py +0 -0
- pmcp/mcp_server/trello/dtos/update_card.py +22 -0
- pmcp/mcp_server/trello/mcp_trello_main.py +35 -0
- pmcp/mcp_server/trello/models.py +47 -0
- pmcp/mcp_server/trello/services/__init__.py +0 -0
- pmcp/mcp_server/trello/services/board.py +53 -0
- pmcp/mcp_server/trello/services/card.py +84 -0
- pmcp/mcp_server/trello/services/checklist.py +162 -0
- pmcp/mcp_server/trello/services/list.py +82 -0
- pmcp/mcp_server/trello/tools/__init__.py +1 -0
- pmcp/mcp_server/trello/tools/board.py +67 -0
- pmcp/mcp_server/trello/tools/card.py +115 -0
- pmcp/mcp_server/trello/tools/checklist.py +139 -0
- pmcp/mcp_server/trello/tools/list.py +112 -0
- pmcp/mcp_server/trello/tools/tools.py +37 -0
- pmcp/mcp_server/trello/trello.py +56 -0
- pmcp/mcp_server/trello/utils/__init__.py +0 -0
- pmcp/mcp_server/trello/utils/trello_api.py +88 -0
app.py
CHANGED
@@ -12,9 +12,17 @@ from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
|
|
12 |
from langgraph.checkpoint.memory import MemorySaver
|
13 |
|
14 |
SYSTEM_PROMPT = """
|
15 |
-
You are
|
16 |
-
|
17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
"""
|
19 |
|
20 |
|
|
|
12 |
from langgraph.checkpoint.memory import MemorySaver
|
13 |
|
14 |
SYSTEM_PROMPT = """
|
15 |
+
You are Trello-Assistant: you can read and write Trello boards via provided tools.
|
16 |
+
|
17 |
+
SAFETY RULE
|
18 |
+
Before using any tool that changes data, briefly state the action and ask:
|
19 |
+
“Proceed? (yes / no)”
|
20 |
+
|
21 |
+
• Only run the tool after an explicit “yes/confirm”.
|
22 |
+
• If the reply is “no” or anything else, cancel the action and end the chat with “Understood—no changes made.”
|
23 |
+
|
24 |
+
Think through tool choices silently; never reveal those thoughts.
|
25 |
+
|
26 |
"""
|
27 |
|
28 |
|
pmcp/mcp_server/github/__init__.py
ADDED
File without changes
|
pmcp/mcp_server/github/github.py
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
|
3 |
+
from dotenv import load_dotenv
|
4 |
+
|
5 |
+
from pmcp.mcp_server.github.utils.github_api import GithubClient
|
6 |
+
|
7 |
+
|
8 |
+
# Load environment variables
|
9 |
+
load_dotenv()
|
10 |
+
|
11 |
+
|
12 |
+
# Initialize Github client and service
|
13 |
+
try:
|
14 |
+
api_key = os.getenv("GITHUB_API_KEY")
|
15 |
+
|
16 |
+
if not api_key:
|
17 |
+
raise ValueError(
|
18 |
+
"GITHUB_API_KEY must be set in environment variables"
|
19 |
+
)
|
20 |
+
client = GithubClient(api_key=api_key)
|
21 |
+
except Exception as e:
|
22 |
+
raise
|
23 |
+
|
24 |
+
|
25 |
+
# Add a prompt for common Github operations
|
26 |
+
def github_help() -> str:
|
27 |
+
"""Provides help information about available Github operations."""
|
28 |
+
return """
|
29 |
+
Available Github Operations:
|
30 |
+
1. get issues
|
31 |
+
2. create issue
|
32 |
+
3. comment issue
|
33 |
+
4. close issue
|
34 |
+
5. get pull requests
|
35 |
+
6. create pull request
|
36 |
+
7. get repo stats
|
37 |
+
8. list branches
|
38 |
+
9. get recent commits
|
39 |
+
10. get file contents
|
40 |
+
"""
|
pmcp/mcp_server/github/mcp_github_main.py
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
|
3 |
+
from dotenv import load_dotenv
|
4 |
+
from mcp.server.fastmcp import FastMCP
|
5 |
+
|
6 |
+
from pmcp.mcp_server.github.tools.tools import register_tools
|
7 |
+
|
8 |
+
# Configure logging
|
9 |
+
logging.basicConfig(
|
10 |
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
11 |
+
)
|
12 |
+
logger = logging.getLogger(__name__)
|
13 |
+
|
14 |
+
# Load environment variables
|
15 |
+
load_dotenv()
|
16 |
+
|
17 |
+
|
18 |
+
# Initialize MCP server
|
19 |
+
mcp = FastMCP("Github MCP Server")
|
20 |
+
|
21 |
+
# Register tools
|
22 |
+
register_tools(mcp)
|
23 |
+
|
24 |
+
|
25 |
+
|
26 |
+
if __name__ == "__main__":
|
27 |
+
try:
|
28 |
+
logger.info("Starting Github MCP Server in Stdio...")
|
29 |
+
mcp.run()
|
30 |
+
logger.info("Github MCP Server started successfully")
|
31 |
+
except KeyboardInterrupt:
|
32 |
+
logger.info("Shutting down server...")
|
33 |
+
except Exception as e:
|
34 |
+
logger.error(f"Server error: {str(e)}")
|
35 |
+
raise
|
pmcp/mcp_server/github/models.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel
|
2 |
+
from typing import List
|
3 |
+
|
4 |
+
class IssuesList(BaseModel):
|
5 |
+
issues: List[str]
|
6 |
+
|
7 |
+
class PullRequestList(BaseModel):
|
8 |
+
pull_requests: List[str]
|
9 |
+
|
10 |
+
class BranchList(BaseModel):
|
11 |
+
branches: List[str]
|
12 |
+
|
13 |
+
class CommitsList(BaseModel):
|
14 |
+
commits: List[str]
|
15 |
+
|
16 |
+
class RepoStats(BaseModel):
|
17 |
+
stars: int
|
18 |
+
forks: int
|
19 |
+
watchers: int
|
20 |
+
open_issues: int
|
pmcp/mcp_server/github/services/__init__.py
ADDED
File without changes
|
pmcp/mcp_server/github/services/branches.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pmcp.mcp_server.github.utils.github_api import GithubClient
|
2 |
+
|
3 |
+
|
4 |
+
class BranchService:
|
5 |
+
"""Branch enumeration service."""
|
6 |
+
|
7 |
+
def __init__(self, client: GithubClient):
|
8 |
+
self.client = client
|
9 |
+
|
10 |
+
async def list_branches(self, owner: str, repo: str):
|
11 |
+
"""
|
12 |
+
Return branch objects for a repository.
|
13 |
+
|
14 |
+
Args:
|
15 |
+
owner (str): Repository owner.
|
16 |
+
repo (str): Repository name.
|
17 |
+
|
18 |
+
Returns:
|
19 |
+
List of branch dicts (each containing ``name`` and ``commit`` keys).
|
20 |
+
"""
|
21 |
+
return await self.client.GET(f"{owner}/{repo}/branches")
|
pmcp/mcp_server/github/services/contents.py
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pmcp.mcp_server.github.utils.github_api import GithubClient
|
2 |
+
|
3 |
+
|
4 |
+
class ContentService:
|
5 |
+
"""Commits and file-content helpers."""
|
6 |
+
|
7 |
+
def __init__(self, client: GithubClient):
|
8 |
+
self.client = client
|
9 |
+
|
10 |
+
async def recent_commits(
|
11 |
+
self, owner: str, repo: str, branch: str, per_page: int = 10
|
12 |
+
):
|
13 |
+
"""
|
14 |
+
Retrieve the most recent commits on a branch.
|
15 |
+
|
16 |
+
Args:
|
17 |
+
owner (str): Repository owner.
|
18 |
+
repo (str): Repository name.
|
19 |
+
branch (str): Branch ref (e.g. ``main``).
|
20 |
+
per_page (int): Max commits to return (<=100).
|
21 |
+
|
22 |
+
Returns:
|
23 |
+
List of commit dicts.
|
24 |
+
"""
|
25 |
+
params = {"sha": branch, "per_page": per_page}
|
26 |
+
return await self.client.GET(f"{owner}/{repo}/commits", params=params)
|
27 |
+
|
28 |
+
async def get_file(
|
29 |
+
self, owner: str, repo: str, path: str, ref: str | None = None
|
30 |
+
):
|
31 |
+
"""
|
32 |
+
Download a file’s blob (Base64) from a repo.
|
33 |
+
|
34 |
+
Args:
|
35 |
+
owner (str): Repository owner.
|
36 |
+
repo (str): Repository name.
|
37 |
+
path (str): File path within the repo.
|
38 |
+
ref (str): Optional commit SHA / branch / tag.
|
39 |
+
|
40 |
+
Returns:
|
41 |
+
GitHub ``contents`` API response including ``content``.
|
42 |
+
"""
|
43 |
+
params = {"ref": ref} if ref else None
|
44 |
+
return await self.client.GET(f"{owner}/{repo}/contents/{path}", params=params)
|
pmcp/mcp_server/github/services/issues.py
ADDED
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Service for managing GitHub issues in the MCP server.
|
3 |
+
"""
|
4 |
+
from pmcp.mcp_server.github.utils.github_api import GithubClient
|
5 |
+
|
6 |
+
|
7 |
+
|
8 |
+
class IssueService:
|
9 |
+
"""Business-logic layer for issue operations."""
|
10 |
+
|
11 |
+
def __init__(self, client: GithubClient):
|
12 |
+
self.client = client
|
13 |
+
|
14 |
+
# ────────────────────────────────────────────────────────────────── #
|
15 |
+
# READ
|
16 |
+
async def get_issues(self, owner: str, repo: str) -> any:
|
17 |
+
"""
|
18 |
+
Return every open-issue JSON object for ``owner/repo``.
|
19 |
+
|
20 |
+
Args:
|
21 |
+
owner (str): Repository owner/organisation.
|
22 |
+
repo (str): Repository name.
|
23 |
+
|
24 |
+
Returns:
|
25 |
+
any: List of issue dicts exactly as returned by the GitHub REST API.
|
26 |
+
"""
|
27 |
+
return await self.client.GET(f"{owner}/{repo}/issues")
|
28 |
+
|
29 |
+
# ────────────────────────────────────────────────────────────────── #
|
30 |
+
# WRITE
|
31 |
+
async def create_issue(
|
32 |
+
self, owner: str, repo: str, title: str, body: str | None = None
|
33 |
+
):
|
34 |
+
"""
|
35 |
+
Create a new issue.
|
36 |
+
|
37 |
+
Args:
|
38 |
+
owner (str): Repository owner.
|
39 |
+
repo (str): Repository name.
|
40 |
+
title (str): Issue title.
|
41 |
+
body (str): Optional Markdown body.
|
42 |
+
|
43 |
+
Returns:
|
44 |
+
JSON payload describing the created issue.
|
45 |
+
"""
|
46 |
+
payload = {"title": title, "body": body or ""}
|
47 |
+
return await self.client.POST(f"{owner}/{repo}/issues", json=payload)
|
48 |
+
|
49 |
+
async def comment_issue(
|
50 |
+
self, owner: str, repo: str, issue_number: int, body: str
|
51 |
+
):
|
52 |
+
"""
|
53 |
+
Add a comment to an existing issue.
|
54 |
+
|
55 |
+
Args:
|
56 |
+
owner (str): Repository owner.
|
57 |
+
repo (str): Repository name.
|
58 |
+
issue_number (int): Target issue number.
|
59 |
+
body (str): Comment body (Markdown).
|
60 |
+
|
61 |
+
Returns:
|
62 |
+
JSON with the new comment metadata.
|
63 |
+
"""
|
64 |
+
payload = {"body": body}
|
65 |
+
return await self.client.POST(
|
66 |
+
f"{owner}/{repo}/issues/{issue_number}/comments", json=payload
|
67 |
+
)
|
68 |
+
|
69 |
+
async def close_issue(self, owner: str, repo: str, issue_number: int):
|
70 |
+
"""
|
71 |
+
Close an issue by setting its state to ``closed``.
|
72 |
+
|
73 |
+
Args:
|
74 |
+
owner (str): Repository owner.
|
75 |
+
repo (str): Repository name.
|
76 |
+
issue_number (int): Issue to close.
|
77 |
+
|
78 |
+
Returns:
|
79 |
+
JSON for the updated issue.
|
80 |
+
"""
|
81 |
+
payload = {"state": "closed"}
|
82 |
+
return await self.client.PATCH(
|
83 |
+
f"{owner}/{repo}/issues/{issue_number}", json=payload
|
84 |
+
)
|
85 |
+
|
86 |
+
|
pmcp/mcp_server/github/services/pull_requests.py
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pmcp.mcp_server.github.utils.github_api import GithubClient
|
2 |
+
|
3 |
+
|
4 |
+
class PullRequestService:
|
5 |
+
"""Read-only pull-request queries."""
|
6 |
+
|
7 |
+
def __init__(self, client: GithubClient):
|
8 |
+
self.client = client
|
9 |
+
|
10 |
+
async def get_pr_list(self, owner: str, repo: str, state: str = "open"):
|
11 |
+
"""
|
12 |
+
List pull requests for a repository.
|
13 |
+
|
14 |
+
Args:
|
15 |
+
owner (str): Repository owner.
|
16 |
+
repo (str): Repository name.
|
17 |
+
state (str): ``open``, ``closed`` or ``all``.
|
18 |
+
|
19 |
+
Returns:
|
20 |
+
List of PR dicts.
|
21 |
+
"""
|
22 |
+
params = {"state": state}
|
23 |
+
return await self.client.GET(f"{owner}/{repo}/pulls", params=params)
|
24 |
+
|
25 |
+
|
26 |
+
async def create(
|
27 |
+
self,
|
28 |
+
owner: str,
|
29 |
+
repo: str,
|
30 |
+
title: str,
|
31 |
+
head: str,
|
32 |
+
base: str,
|
33 |
+
body: str | None = None,
|
34 |
+
draft: bool = False,
|
35 |
+
):
|
36 |
+
"""
|
37 |
+
Create a pull request (`POST /pulls`).
|
38 |
+
|
39 |
+
Args:
|
40 |
+
owner: Repository owner.
|
41 |
+
repo: Repository name.
|
42 |
+
title: PR title.
|
43 |
+
head: The branch/tag you want to merge **from** (`user:branch` accepted).
|
44 |
+
base: The branch you want to merge **into** (usually `main`).
|
45 |
+
body: Optional Markdown description.
|
46 |
+
draft: Whether to open as a draft PR.
|
47 |
+
|
48 |
+
Returns:
|
49 |
+
JSON describing the new pull request.
|
50 |
+
"""
|
51 |
+
payload = {
|
52 |
+
"title": title,
|
53 |
+
"head": head,
|
54 |
+
"base": base,
|
55 |
+
"body": body or "",
|
56 |
+
"draft": draft,
|
57 |
+
}
|
58 |
+
return await self.client.POST(f"{owner}/{repo}/pulls", json=payload)
|
pmcp/mcp_server/github/services/repo.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pmcp.mcp_server.github.utils.github_api import GithubClient
|
2 |
+
|
3 |
+
|
4 |
+
class RepoService:
|
5 |
+
"""General repository information."""
|
6 |
+
|
7 |
+
def __init__(self, client: GithubClient):
|
8 |
+
self.client = client
|
9 |
+
|
10 |
+
async def get_stats(self, owner: str, repo: str):
|
11 |
+
"""
|
12 |
+
Fetch high-level repo metadata (stars, forks, etc.).
|
13 |
+
|
14 |
+
Args:
|
15 |
+
owner (str): Repository owner.
|
16 |
+
repo (str): Repository name.
|
17 |
+
|
18 |
+
Returns:
|
19 |
+
JSON with the repository resource.
|
20 |
+
"""
|
21 |
+
return await self.client.GET(f"{owner}/{repo}")
|
pmcp/mcp_server/github/services/repo_to_text.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Service for managing Github issues in MCP server.
|
3 |
+
"""
|
4 |
+
from pmcp.mcp_server.github.utils.github_api import GithubClient
|
5 |
+
|
6 |
+
|
7 |
+
|
8 |
+
class RepoToTextService:
|
9 |
+
"""
|
10 |
+
Service class for managing Github repo
|
11 |
+
"""
|
12 |
+
|
13 |
+
def __init__(self, client: GithubClient):
|
14 |
+
self.client = client
|
15 |
+
|
16 |
+
async def get_repo_to_text(self, repo: str) -> any:
|
17 |
+
"""Retrieves the repo and file structure as text.
|
18 |
+
|
19 |
+
Args:
|
20 |
+
owner (str): The owner of the repository.
|
21 |
+
|
22 |
+
Returns:
|
23 |
+
any: The list of issues.
|
24 |
+
"""
|
25 |
+
response = await self.client.REPO_TO_TEXT(repo)
|
26 |
+
return response
|
pmcp/mcp_server/github/tools/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
|
pmcp/mcp_server/github/tools/branches.py
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Branch listing tool.
|
3 |
+
"""
|
4 |
+
from typing import List, Dict
|
5 |
+
from mcp.server.fastmcp import Context
|
6 |
+
from pmcp.mcp_server.github.services.branches import BranchService
|
7 |
+
from pmcp.mcp_server.github import client
|
8 |
+
|
9 |
+
service = BranchService(client)
|
10 |
+
|
11 |
+
|
12 |
+
async def list_branches(ctx: Context, owner: str, repo: str) -> Dict[str, List[str]]:
|
13 |
+
"""
|
14 |
+
Gets the list of branches.
|
15 |
+
|
16 |
+
Args:
|
17 |
+
ctx: FastMCP request context (handles errors).
|
18 |
+
owner (str): Repository owner.
|
19 |
+
repo (str): Repository name.
|
20 |
+
|
21 |
+
Returns:
|
22 |
+
{"branches": ["main", "dev", …]}
|
23 |
+
"""
|
24 |
+
try:
|
25 |
+
branches = await service.list_branches(owner, repo)
|
26 |
+
names = [b["name"] for b in branches]
|
27 |
+
return {"branches": names}
|
28 |
+
except Exception as exc:
|
29 |
+
error_msg = f"Error while getting the list of branches for repository {repo}. Error: {str(exc)}"
|
30 |
+
await ctx.error(str(exc))
|
31 |
+
raise
|
pmcp/mcp_server/github/tools/contents.py
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Commit and file-content tools.
|
3 |
+
"""
|
4 |
+
import base64
|
5 |
+
from typing import List, Dict
|
6 |
+
from mcp.server.fastmcp import Context
|
7 |
+
from pmcp.mcp_server.github.services.contents import ContentService
|
8 |
+
from pmcp.mcp_server.github import client
|
9 |
+
|
10 |
+
service = ContentService(client)
|
11 |
+
|
12 |
+
|
13 |
+
async def get_recent_commits(
|
14 |
+
ctx: Context, owner: str, repo: str, branch: str, per_page: int = 10
|
15 |
+
) -> Dict[str, List[str]]:
|
16 |
+
"""
|
17 |
+
Retrieves the most recent commits.
|
18 |
+
|
19 |
+
Args:
|
20 |
+
ctx: FastMCP request context (handles errors).
|
21 |
+
owner (str): Repository owner.
|
22 |
+
repo (str): Repository name.
|
23 |
+
branch (str): Name of the branch
|
24 |
+
per_page (int): Max commits to return
|
25 |
+
|
26 |
+
Returns:
|
27 |
+
{"commits": ["abc123", …]}
|
28 |
+
"""
|
29 |
+
try:
|
30 |
+
commits = await service.recent_commits(owner, repo, branch, per_page)
|
31 |
+
shas = [c["sha"] for c in commits]
|
32 |
+
return {"commits": shas}
|
33 |
+
except Exception as exc:
|
34 |
+
error_msg = f"Error while getting the commits for branch {branch} in {repo}. Error {str(exc)}"
|
35 |
+
await ctx.error(str(exc))
|
36 |
+
raise
|
37 |
+
|
38 |
+
|
39 |
+
async def get_file_contents(
|
40 |
+
ctx: Context, owner: str, repo: str, path: str, ref: str | None = None
|
41 |
+
) -> Dict[str, str]:
|
42 |
+
"""
|
43 |
+
Retrieves the most recent commits.
|
44 |
+
|
45 |
+
Args:
|
46 |
+
ctx: FastMCP request context (handles errors).
|
47 |
+
owner (str): Repository owner.
|
48 |
+
repo (str): Repository name.
|
49 |
+
path (str): File path within the repo
|
50 |
+
ref (int): Optional commit SHA / branch / tag.
|
51 |
+
|
52 |
+
Returns:
|
53 |
+
{"path": "...", "content": "..."}
|
54 |
+
"""
|
55 |
+
try:
|
56 |
+
blob = await service.get_file(owner, repo, path, ref)
|
57 |
+
content = base64.b64decode(blob["content"]).decode()
|
58 |
+
return {"path": path, "content": content}
|
59 |
+
except Exception as exc:
|
60 |
+
error_msg = f"Error while getting the content of file {path} in repository {repo}. Error: {exc}"
|
61 |
+
await ctx.error(str(error_msg))
|
62 |
+
raise
|
pmcp/mcp_server/github/tools/issues.py
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
MCP Tools exposing GitHub issue operations.
|
3 |
+
"""
|
4 |
+
from typing import List, Dict, Any
|
5 |
+
from mcp.server.fastmcp import Context
|
6 |
+
|
7 |
+
from pmcp.mcp_server.github.services.issues import IssueService
|
8 |
+
from pmcp.mcp_server.github import client
|
9 |
+
|
10 |
+
|
11 |
+
service = IssueService(client)
|
12 |
+
|
13 |
+
|
14 |
+
# ───────────────────────────────────────────────────────────────────────── #
|
15 |
+
# READ
|
16 |
+
|
17 |
+
async def get_issues(ctx: Context, owner: str, repo: str) -> Dict[str, List[str]]:
|
18 |
+
"""
|
19 |
+
Retrieves issues from repo
|
20 |
+
|
21 |
+
Args:
|
22 |
+
ctx: FastMCP request context (handles errors).
|
23 |
+
owner (str): Repository owner.
|
24 |
+
repo (str): Repository name.
|
25 |
+
|
26 |
+
Returns:
|
27 |
+
{"issues": [title, …]}
|
28 |
+
"""
|
29 |
+
try:
|
30 |
+
issues = await service.get_issues(owner, repo)
|
31 |
+
titles = [i["title"] for i in issues if "pull_request" not in i]
|
32 |
+
return {"issues": titles}
|
33 |
+
except Exception as exc:
|
34 |
+
error_msg = f"Failed to get issues: {str(exc)}"
|
35 |
+
await ctx.error(str(exc))
|
36 |
+
raise
|
37 |
+
|
38 |
+
|
39 |
+
# ───────────────────────────────────────────────────────────────────────── #
|
40 |
+
# WRITE
|
41 |
+
|
42 |
+
async def create_issue(
|
43 |
+
ctx: Context, owner: str, repo: str, title: str, body: str | None = None
|
44 |
+
) -> Dict[str, Any]:
|
45 |
+
"""
|
46 |
+
Opens a new issue.
|
47 |
+
|
48 |
+
Args:
|
49 |
+
ctx: FastMCP request context (handles errors).
|
50 |
+
owner (str): Repository owner.
|
51 |
+
repo (str): Repository name.
|
52 |
+
title (str): Issue title.
|
53 |
+
body (str): Body of the issue.
|
54 |
+
|
55 |
+
Returns:
|
56 |
+
{"url": "...", "number": 123}
|
57 |
+
"""
|
58 |
+
try:
|
59 |
+
result = await service.create_issue(owner, repo, title, body)
|
60 |
+
return {"url": result["html_url"], "number": result["number"]}
|
61 |
+
except Exception as exc:
|
62 |
+
error_msg = f"Failed to create issue {str(exc)}"
|
63 |
+
await ctx.error(str(exc))
|
64 |
+
raise
|
65 |
+
|
66 |
+
|
67 |
+
async def comment_issue(
|
68 |
+
ctx: Context, owner: str, repo: str, issue_number: int, body: str
|
69 |
+
) -> Dict[str, str]:
|
70 |
+
"""
|
71 |
+
Adds a comment on an existing issue.
|
72 |
+
|
73 |
+
Args:
|
74 |
+
ctx: FastMCP request context (handles errors).
|
75 |
+
owner (str): Repository owner.
|
76 |
+
repo (str): Repository name.
|
77 |
+
issue_number (int): Issue number.
|
78 |
+
body (str): Body of the issue.
|
79 |
+
"""
|
80 |
+
try:
|
81 |
+
result = await service.comment_issue(owner, repo, issue_number, body)
|
82 |
+
return {"url": result["html_url"]}
|
83 |
+
except Exception as exc:
|
84 |
+
error_msg = f"Failed to add the comment {str(exc)}"
|
85 |
+
await ctx.error(str(exc))
|
86 |
+
raise
|
87 |
+
|
88 |
+
|
89 |
+
async def close_issue(
|
90 |
+
ctx: Context, owner: str, repo: str, issue_number: int
|
91 |
+
) -> Dict[str, str]:
|
92 |
+
"""
|
93 |
+
Closes an issue.
|
94 |
+
|
95 |
+
Args:
|
96 |
+
ctx: FastMCP request context (handles errors).
|
97 |
+
owner (str): Repository owner.
|
98 |
+
repo (str): Repository name.
|
99 |
+
issue_number (int): Issue number.
|
100 |
+
|
101 |
+
Returns:
|
102 |
+
Dict
|
103 |
+
"""
|
104 |
+
try:
|
105 |
+
await service.close_issue(owner, repo, issue_number)
|
106 |
+
return {"status": "closed"}
|
107 |
+
except Exception as exc:
|
108 |
+
error_msg = f"Failed to close the issue number {issue_number} in repo {repo}"
|
109 |
+
await ctx.error(str(exc))
|
110 |
+
raise
|
pmcp/mcp_server/github/tools/pull_requests.py
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Pull-request utilities (read-only).
|
3 |
+
"""
|
4 |
+
from typing import List, Dict
|
5 |
+
from mcp.server.fastmcp import Context
|
6 |
+
from pmcp.mcp_server.github.services.pull_requests import PullRequestService
|
7 |
+
from pmcp.mcp_server.github import client
|
8 |
+
|
9 |
+
service = PullRequestService(client)
|
10 |
+
|
11 |
+
|
12 |
+
async def get_pull_requests(
|
13 |
+
ctx: Context, owner: str, repo: str, state: str = "open"
|
14 |
+
) -> Dict[str, List[str]]:
|
15 |
+
"""
|
16 |
+
Gets the pull requests
|
17 |
+
|
18 |
+
Args:
|
19 |
+
ctx: FastMCP request context (handles errors).
|
20 |
+
owner (str): Repository owner.
|
21 |
+
repo (str): Repository name.
|
22 |
+
state (str): State of the pull request
|
23 |
+
|
24 |
+
Returns:
|
25 |
+
{"pull_requests": ["#1 - Add feature (open)", …]}
|
26 |
+
"""
|
27 |
+
try:
|
28 |
+
pulls = await service.get_pr_list(owner, repo, state)
|
29 |
+
titles = [f"#{pr['number']} - {pr['title']} ({pr['state']})" for pr in pulls]
|
30 |
+
return {"pull_requests": titles}
|
31 |
+
except Exception as exc:
|
32 |
+
error_msg = f"Failed to get pull requests. Error: {str(exc)}"
|
33 |
+
await ctx.error(str(exc))
|
34 |
+
raise
|
35 |
+
|
36 |
+
|
37 |
+
async def create_pull_request(
|
38 |
+
ctx: Context,
|
39 |
+
owner: str,
|
40 |
+
repo: str,
|
41 |
+
title: str,
|
42 |
+
head: str,
|
43 |
+
base: str,
|
44 |
+
body: str | None = None,
|
45 |
+
draft: bool = False,
|
46 |
+
) -> Dict[str, str]:
|
47 |
+
"""
|
48 |
+
Create a pull request.
|
49 |
+
|
50 |
+
Args:
|
51 |
+
owner: Repository owner.
|
52 |
+
repo: Repository name.
|
53 |
+
title: PR title.
|
54 |
+
head: The branch/tag you want to merge **from** (`user:branch` accepted).
|
55 |
+
base: The branch you want to merge **into** (usually `main`).
|
56 |
+
body: Optional Markdown description.
|
57 |
+
draft: Whether to open as a draft PR.
|
58 |
+
|
59 |
+
Returns:
|
60 |
+
{"url": pull_request_url, "number": pr_number}
|
61 |
+
"""
|
62 |
+
try:
|
63 |
+
pr = await service.create(owner, repo, title, head, base, body, draft)
|
64 |
+
return {"url": pr["html_url"], "number": pr["number"]}
|
65 |
+
except Exception as exc:
|
66 |
+
error_msg = f"Error creating the pull request. Error {str(exc)}"
|
67 |
+
await ctx.error(str(exc))
|
68 |
+
raise
|
pmcp/mcp_server/github/tools/repo.py
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Repository-level stats tool.
|
3 |
+
"""
|
4 |
+
from mcp.server.fastmcp import Context
|
5 |
+
from pmcp.mcp_server.github.services.repo import RepoService
|
6 |
+
from pmcp.mcp_server.github import client
|
7 |
+
|
8 |
+
|
9 |
+
service = RepoService(client)
|
10 |
+
|
11 |
+
|
12 |
+
async def get_repo_stats(ctx: Context, owner: str, repo: str):
|
13 |
+
"""
|
14 |
+
Gets the statistics of the repository
|
15 |
+
|
16 |
+
Args:
|
17 |
+
ctx: FastMCP request context (handles errors).
|
18 |
+
owner (str): Repository owner.
|
19 |
+
repo (str): Repository name.
|
20 |
+
|
21 |
+
Returns:
|
22 |
+
{"stars": 0, "forks": 0, "watchers": 0, "open_issues": 0}
|
23 |
+
"""
|
24 |
+
try:
|
25 |
+
data = await service.get_stats(owner, repo)
|
26 |
+
return {
|
27 |
+
"stars": data["stargazers_count"],
|
28 |
+
"forks": data["forks_count"],
|
29 |
+
"watchers": data["watchers_count"],
|
30 |
+
"open_issues": data["open_issues_count"],
|
31 |
+
}
|
32 |
+
except Exception as exc:
|
33 |
+
error_msg = f"Failed to get statistics of repository {repo}. Error: {str(exc)}"
|
34 |
+
await ctx.error(str(exc))
|
35 |
+
raise
|
pmcp/mcp_server/github/tools/repo_to_text.py
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This module contains tools for managing Github.
|
3 |
+
"""
|
4 |
+
|
5 |
+
from pmcp.mcp_server.github.services.repo_to_text import RepoToTextService
|
6 |
+
from pmcp.mcp_server.github import client
|
7 |
+
|
8 |
+
from mcp.server.fastmcp import Context
|
9 |
+
|
10 |
+
|
11 |
+
service = RepoToTextService(client)
|
12 |
+
|
13 |
+
async def get_repo_to_text(ctx: Context, repo: str) -> str:
|
14 |
+
"""Retrieves the content of a repository as text.
|
15 |
+
|
16 |
+
Args:
|
17 |
+
repo (str): The name of the repository.
|
18 |
+
|
19 |
+
Returns:
|
20 |
+
str: The content of the repository as text.
|
21 |
+
"""
|
22 |
+
try:
|
23 |
+
content = await service.get_repo_to_text(repo)
|
24 |
+
return content
|
25 |
+
except Exception as e:
|
26 |
+
error_msg = f"Failed to get content from repo: {str(e)}"
|
27 |
+
await ctx.error(error_msg)
|
28 |
+
raise
|
pmcp/mcp_server/github/tools/tools.py
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This module contains tools for managing Github Issues
|
3 |
+
"""
|
4 |
+
|
5 |
+
from pmcp.mcp_server.github.tools import issues, pull_requests, repo, repo_to_text,branches, contents
|
6 |
+
|
7 |
+
|
8 |
+
def register_tools(mcp):
|
9 |
+
"""Register tools with the MCP server."""
|
10 |
+
|
11 |
+
# ISSUES
|
12 |
+
mcp.add_tool(issues.get_issues)
|
13 |
+
mcp.add_tool(issues.create_issue)
|
14 |
+
mcp.add_tool(issues.comment_issue)
|
15 |
+
mcp.add_tool(issues.close_issue)
|
16 |
+
|
17 |
+
# PULL REQUESTS
|
18 |
+
mcp.add_tool(pull_requests.get_pull_requests)
|
19 |
+
mcp.add_tool(pull_requests.create_pull_request)
|
20 |
+
|
21 |
+
# REPOSITORY
|
22 |
+
mcp.add_tool(repo.get_repo_stats)
|
23 |
+
|
24 |
+
# BRANCHES & COMMITS
|
25 |
+
mcp.add_tool(branches.list_branches)
|
26 |
+
mcp.add_tool(contents.get_recent_commits)
|
27 |
+
mcp.add_tool(contents.get_file_contents)
|
28 |
+
|
29 |
+
|
30 |
+
|
31 |
+
mcp.add_tool(repo_to_text.get_repo_to_text)
|
pmcp/mcp_server/github/utils/__init__.py
ADDED
File without changes
|
pmcp/mcp_server/github/utils/github_api.py
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import httpx
|
3 |
+
from pmcp.mcp_server.github.utils.repo_to_text_utils import fetch_file_contents, fetch_repo_sha, fetch_repo_tree, format_repo_contents, parse_repo_url
|
4 |
+
|
5 |
+
|
6 |
+
GITHUB_API_BASE = "https://api.github.com/repos"
|
7 |
+
|
8 |
+
|
9 |
+
class GithubClient:
|
10 |
+
"""
|
11 |
+
Client class for interacting with the Github API over REST.
|
12 |
+
"""
|
13 |
+
|
14 |
+
def __init__(self, api_key: str):
|
15 |
+
self.api_key = api_key
|
16 |
+
self.base_url = GITHUB_API_BASE
|
17 |
+
self.client = httpx.AsyncClient(base_url=self.base_url)
|
18 |
+
|
19 |
+
async def close(self):
|
20 |
+
await self.client.aclose()
|
21 |
+
|
22 |
+
async def GET(self, endpoint: str, params: dict = None):
|
23 |
+
return await self._request("get", endpoint, params=params)
|
24 |
+
|
25 |
+
async def POST(self, endpoint: str, json: dict):
|
26 |
+
return await self._request("post", endpoint, json=json)
|
27 |
+
|
28 |
+
async def PATCH(self, endpoint: str, json: dict):
|
29 |
+
return await self._request("patch", endpoint, json=json)
|
30 |
+
|
31 |
+
async def PUT(self, endpoint: str, json: dict = None):
|
32 |
+
return await self._request("put", endpoint, json=json)
|
33 |
+
|
34 |
+
async def _request(self, method: str, endpoint: str, **kwargs):
|
35 |
+
headers = self._get_headers()
|
36 |
+
|
37 |
+
try:
|
38 |
+
response = await self.client.request(method, endpoint, headers=headers, **kwargs)
|
39 |
+
response.raise_for_status()
|
40 |
+
return response.json()
|
41 |
+
except httpx.HTTPStatusError as e:
|
42 |
+
raise httpx.HTTPStatusError(
|
43 |
+
f"Failed to {method.upper()} {endpoint}: {str(e)}",
|
44 |
+
request=e.request,
|
45 |
+
response=e.response,
|
46 |
+
)
|
47 |
+
except httpx.RequestError as e:
|
48 |
+
raise httpx.RequestError(f"Failed to {method.upper()} {endpoint}: {str(e)}")
|
49 |
+
|
50 |
+
def _get_headers(self):
|
51 |
+
return {
|
52 |
+
"Accept": "application/vnd.github+json",
|
53 |
+
"Authorization": f"Bearer {self.api_key}",
|
54 |
+
"X-GitHub-Api-Version": "2022-11-28",
|
55 |
+
}
|
56 |
+
|
57 |
+
async def REPO_TO_TEXT(self, repo_url: str):
|
58 |
+
owner, repo, ref, path = parse_repo_url(repo_url)
|
59 |
+
sha = fetch_repo_sha(owner, repo, ref, path, self.api_key)
|
60 |
+
tree = fetch_repo_tree(owner, repo, sha, self.api_key)
|
61 |
+
blobs = [item for item in tree if item['type'] == 'blob']
|
62 |
+
common_exts = ('.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.cpp', '.html', '.css')
|
63 |
+
selected_files = [item for item in blobs if item['path'].lower().endswith(common_exts)]
|
64 |
+
for item in selected_files:
|
65 |
+
item['url'] = f"https://api.github.com/repos/{owner}/{repo}/contents/{item['path']}?ref={ref}" if ref else f"https://api.github.com/repos/{owner}/{repo}/contents/{item['path']}"
|
66 |
+
contents = fetch_file_contents(selected_files,self. api_key)
|
67 |
+
return format_repo_contents(contents)
|
pmcp/mcp_server/github/utils/repo_to_text_utils.py
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
import re
|
3 |
+
import os
|
4 |
+
from typing import List, Dict, Tuple
|
5 |
+
|
6 |
+
def parse_repo_url(url: str) -> Tuple[str, str, str, str]:
|
7 |
+
url = url.rstrip('/')
|
8 |
+
pattern = r'^https:\/\/github\.com\/([^\/]+)\/([^\/]+)(\/tree\/([^\/]+)(\/(.+))?)?$'
|
9 |
+
match = re.match(pattern, url)
|
10 |
+
if not match:
|
11 |
+
raise ValueError('Invalid GitHub repository URL format.')
|
12 |
+
return match[1], match[2], match[4] or '', match[6] or ''
|
13 |
+
|
14 |
+
def fetch_repo_sha(owner: str, repo: str, ref: str, path: str, token: str) -> str:
|
15 |
+
url = f'https://api.github.com/repos/{owner}/{repo}/contents/{path}?ref={ref}' if ref else f'https://api.github.com/repos/{owner}/{repo}/contents/{path}'
|
16 |
+
headers = {'Accept': 'application/vnd.github.object+json'}
|
17 |
+
if token:
|
18 |
+
headers['Authorization'] = f'token {token}'
|
19 |
+
resp = requests.get(url, headers=headers)
|
20 |
+
if resp.status_code == 403 and resp.headers.get('X-RateLimit-Remaining') == '0':
|
21 |
+
raise Exception('GitHub API rate limit exceeded.')
|
22 |
+
if resp.status_code == 404:
|
23 |
+
raise Exception('Repository, branch, or path not found.')
|
24 |
+
if not resp.ok:
|
25 |
+
raise Exception(f'Failed to fetch SHA. Status: {resp.status_code}')
|
26 |
+
return resp.json()['sha']
|
27 |
+
|
28 |
+
def fetch_repo_tree(owner: str, repo: str, sha: str, token: str) -> List[Dict]:
|
29 |
+
url = f'https://api.github.com/repos/{owner}/{repo}/git/trees/{sha}?recursive=1'
|
30 |
+
headers = {'Accept': 'application/vnd.github+json'}
|
31 |
+
if token:
|
32 |
+
headers['Authorization'] = f'token {token}'
|
33 |
+
resp = requests.get(url, headers=headers)
|
34 |
+
if resp.status_code == 403 and resp.headers.get('X-RateLimit-Remaining') == '0':
|
35 |
+
raise Exception('GitHub API rate limit exceeded.')
|
36 |
+
if not resp.ok:
|
37 |
+
raise Exception(f'Failed to fetch repo tree. Status: {resp.status_code}')
|
38 |
+
return resp.json()['tree']
|
39 |
+
|
40 |
+
def fetch_file_contents(files: List[Dict], token: str) -> List[Dict]:
|
41 |
+
headers = {'Accept': 'application/vnd.github.v3.raw'}
|
42 |
+
if token:
|
43 |
+
headers['Authorization'] = f'token {token}'
|
44 |
+
contents = []
|
45 |
+
for file in files:
|
46 |
+
resp = requests.get(file['url'], headers=headers)
|
47 |
+
if not resp.ok:
|
48 |
+
raise Exception(f'Failed to fetch file: {file["path"]} (status {resp.status_code})')
|
49 |
+
contents.append({'path': file['path'], 'text': resp.text})
|
50 |
+
return contents
|
51 |
+
|
52 |
+
def build_tree_structure(paths: List[str]) -> Dict:
|
53 |
+
tree = {}
|
54 |
+
for path in paths:
|
55 |
+
parts = path.split('/')
|
56 |
+
current = tree
|
57 |
+
for i, part in enumerate(parts):
|
58 |
+
if part not in current:
|
59 |
+
current[part] = {} if i < len(parts) - 1 else None
|
60 |
+
current = current[part] if current[part] is not None else {}
|
61 |
+
return tree
|
62 |
+
|
63 |
+
def format_tree_index(tree: Dict, prefix: str = '') -> str:
|
64 |
+
output = ''
|
65 |
+
entries = list(tree.items())
|
66 |
+
for i, (name, sub_tree) in enumerate(entries):
|
67 |
+
is_last = i == len(entries) - 1
|
68 |
+
line_prefix = '└── ' if is_last else '├── '
|
69 |
+
child_prefix = ' ' if is_last else '│ '
|
70 |
+
output += f"{prefix}{line_prefix}{name}\n"
|
71 |
+
if sub_tree:
|
72 |
+
output += format_tree_index(sub_tree, prefix + child_prefix)
|
73 |
+
return output
|
74 |
+
|
75 |
+
def format_repo_contents(contents: List[Dict]) -> str:
|
76 |
+
contents.sort(key=lambda x: x['path'].lower())
|
77 |
+
paths = [item['path'] for item in contents]
|
78 |
+
tree = build_tree_structure(paths)
|
79 |
+
index = format_tree_index(tree)
|
80 |
+
result = f"Directory Structure:\n\n{index}"
|
81 |
+
for item in contents:
|
82 |
+
result += f"\n\n---\nFile: {item['path']}\n---\n\n{item['text']}\n"
|
83 |
+
return result
|
pmcp/mcp_server/trello/__init__.py
ADDED
File without changes
|
pmcp/mcp_server/trello/dtos/update_card.py
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel
|
2 |
+
|
3 |
+
|
4 |
+
class UpdateCardPayload(BaseModel):
|
5 |
+
"""
|
6 |
+
Payload for updating a card.
|
7 |
+
|
8 |
+
Attributes:
|
9 |
+
name (str): The name of the card.
|
10 |
+
desc (str): The description of the card.
|
11 |
+
pos (str | int): The position of the card.
|
12 |
+
closed (bool): Whether the card is closed or not.
|
13 |
+
due (str): The due date of the card in ISO 8601 format.
|
14 |
+
idLabels (str): Comma-separated list of label IDs for the card.
|
15 |
+
"""
|
16 |
+
|
17 |
+
name: str | None = None
|
18 |
+
desc: str | None = None
|
19 |
+
pos: str | None = None
|
20 |
+
closed: bool | None = None
|
21 |
+
due: str | None = None
|
22 |
+
idLabels: str | None = None
|
pmcp/mcp_server/trello/mcp_trello_main.py
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
|
3 |
+
from dotenv import load_dotenv
|
4 |
+
from mcp.server.fastmcp import FastMCP
|
5 |
+
|
6 |
+
from pmcp.mcp_server.trello.tools.tools import register_tools
|
7 |
+
|
8 |
+
# Configure logging
|
9 |
+
logging.basicConfig(
|
10 |
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
11 |
+
)
|
12 |
+
logger = logging.getLogger(__name__)
|
13 |
+
|
14 |
+
# Load environment variables
|
15 |
+
load_dotenv()
|
16 |
+
|
17 |
+
|
18 |
+
# Initialize MCP server
|
19 |
+
mcp = FastMCP("Trello MCP Server")
|
20 |
+
|
21 |
+
# Register tools
|
22 |
+
register_tools(mcp)
|
23 |
+
|
24 |
+
|
25 |
+
|
26 |
+
if __name__ == "__main__":
|
27 |
+
try:
|
28 |
+
logger.info("Starting Trello MCP Server in Stdio...")
|
29 |
+
mcp.run()
|
30 |
+
logger.info("Github MCP Server started successfully")
|
31 |
+
except KeyboardInterrupt:
|
32 |
+
logger.info("Shutting down server...")
|
33 |
+
except Exception as e:
|
34 |
+
logger.error(f"Server error: {str(e)}")
|
35 |
+
raise
|
pmcp/mcp_server/trello/models.py
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Optional
|
2 |
+
|
3 |
+
from pydantic import BaseModel
|
4 |
+
|
5 |
+
|
6 |
+
class TrelloBoard(BaseModel):
|
7 |
+
"""Model representing a Trello board."""
|
8 |
+
|
9 |
+
id: str
|
10 |
+
name: str
|
11 |
+
desc: Optional[str] = None
|
12 |
+
closed: bool = False
|
13 |
+
idOrganization: Optional[str] = None
|
14 |
+
url: str
|
15 |
+
|
16 |
+
|
17 |
+
class TrelloList(BaseModel):
|
18 |
+
"""Model representing a Trello list."""
|
19 |
+
|
20 |
+
id: str
|
21 |
+
name: str
|
22 |
+
closed: bool = False
|
23 |
+
idBoard: str
|
24 |
+
pos: float
|
25 |
+
|
26 |
+
|
27 |
+
class TrelloLabel(BaseModel):
|
28 |
+
"""Model representing a Trello label."""
|
29 |
+
|
30 |
+
id: str
|
31 |
+
name: str
|
32 |
+
color: Optional[str] = None
|
33 |
+
|
34 |
+
|
35 |
+
class TrelloCard(BaseModel):
|
36 |
+
"""Model representing a Trello card."""
|
37 |
+
|
38 |
+
id: str
|
39 |
+
name: str
|
40 |
+
desc: Optional[str] = None
|
41 |
+
closed: bool = False
|
42 |
+
idList: str
|
43 |
+
idBoard: str
|
44 |
+
url: str
|
45 |
+
pos: float
|
46 |
+
labels: List[TrelloLabel] = []
|
47 |
+
due: Optional[str] = None
|
pmcp/mcp_server/trello/services/__init__.py
ADDED
File without changes
|
pmcp/mcp_server/trello/services/board.py
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Service for managing Trello boards in MCP server.
|
3 |
+
"""
|
4 |
+
|
5 |
+
from typing import List
|
6 |
+
|
7 |
+
from pmcp.mcp_server.trello.models import TrelloBoard, TrelloLabel
|
8 |
+
from pmcp.mcp_server.trello.utils.trello_api import TrelloClient
|
9 |
+
|
10 |
+
|
11 |
+
class BoardService:
|
12 |
+
"""
|
13 |
+
Service class for managing Trello boards
|
14 |
+
"""
|
15 |
+
|
16 |
+
def __init__(self, client: TrelloClient):
|
17 |
+
self.client = client
|
18 |
+
|
19 |
+
async def get_board(self, board_id: str) -> TrelloBoard:
|
20 |
+
"""Retrieves a specific board by its ID.
|
21 |
+
|
22 |
+
Args:
|
23 |
+
board_id (str): The ID of the board to retrieve.
|
24 |
+
|
25 |
+
Returns:
|
26 |
+
TrelloBoard: The board object containing board details.
|
27 |
+
"""
|
28 |
+
response = await self.client.GET(f"/boards/{board_id}")
|
29 |
+
return TrelloBoard(**response)
|
30 |
+
|
31 |
+
async def get_boards(self, member_id: str = "me") -> List[TrelloBoard]:
|
32 |
+
"""Retrieves all boards for a given member.
|
33 |
+
|
34 |
+
Args:
|
35 |
+
member_id (str): The ID of the member whose boards to retrieve. Defaults to "me" for the authenticated user.
|
36 |
+
|
37 |
+
Returns:
|
38 |
+
List[TrelloBoard]: A list of board objects.
|
39 |
+
"""
|
40 |
+
response = await self.client.GET(f"/members/{member_id}/boards")
|
41 |
+
return [TrelloBoard(**board) for board in response]
|
42 |
+
|
43 |
+
async def get_board_labels(self, board_id: str) -> List[TrelloLabel]:
|
44 |
+
"""Retrieves all labels for a specific board.
|
45 |
+
|
46 |
+
Args:
|
47 |
+
board_id (str): The ID of the board whose labels to retrieve.
|
48 |
+
|
49 |
+
Returns:
|
50 |
+
List[TrelloLabel]: A list of label objects for the board.
|
51 |
+
"""
|
52 |
+
response = await self.client.GET(f"/boards/{board_id}/labels")
|
53 |
+
return [TrelloLabel(**label) for label in response]
|
pmcp/mcp_server/trello/services/card.py
ADDED
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Service for managing Trello cards in MCP server.
|
3 |
+
"""
|
4 |
+
|
5 |
+
from typing import Any, Dict, List
|
6 |
+
|
7 |
+
from pmcp.mcp_server.trello.models import TrelloCard
|
8 |
+
from pmcp.mcp_server.trello.utils.trello_api import TrelloClient
|
9 |
+
|
10 |
+
|
11 |
+
class CardService:
|
12 |
+
"""
|
13 |
+
Service class for managing Trello cards.
|
14 |
+
"""
|
15 |
+
|
16 |
+
def __init__(self, client: TrelloClient):
|
17 |
+
self.client = client
|
18 |
+
|
19 |
+
async def get_card(self, card_id: str) -> TrelloCard:
|
20 |
+
"""Retrieves a specific card by its ID.
|
21 |
+
|
22 |
+
Args:
|
23 |
+
card_id (str): The ID of the card to retrieve.
|
24 |
+
|
25 |
+
Returns:
|
26 |
+
TrelloCard: The card object containing card details.
|
27 |
+
"""
|
28 |
+
response = await self.client.GET(f"/cards/{card_id}")
|
29 |
+
return TrelloCard(**response)
|
30 |
+
|
31 |
+
async def get_cards(self, list_id: str) -> List[TrelloCard]:
|
32 |
+
"""Retrieves all cards in a given list.
|
33 |
+
|
34 |
+
Args:
|
35 |
+
list_id (str): The ID of the list whose cards to retrieve.
|
36 |
+
|
37 |
+
Returns:
|
38 |
+
List[TrelloCard]: A list of card objects.
|
39 |
+
"""
|
40 |
+
response = await self.client.GET(f"/lists/{list_id}/cards")
|
41 |
+
return [TrelloCard(**card) for card in response]
|
42 |
+
|
43 |
+
async def create_card(
|
44 |
+
self, list_id: str, name: str, desc: str | None = None
|
45 |
+
) -> TrelloCard:
|
46 |
+
"""Creates a new card in a given list.
|
47 |
+
|
48 |
+
Args:
|
49 |
+
list_id (str): The ID of the list to create the card in.
|
50 |
+
name (str): The name of the new card.
|
51 |
+
desc (str, optional): The description of the new card. Defaults to None.
|
52 |
+
|
53 |
+
Returns:
|
54 |
+
TrelloCard: The newly created card object.
|
55 |
+
"""
|
56 |
+
data = {"name": name, "idList": list_id}
|
57 |
+
if desc:
|
58 |
+
data["desc"] = desc
|
59 |
+
response = await self.client.POST("/cards", data=data)
|
60 |
+
return TrelloCard(**response)
|
61 |
+
|
62 |
+
async def update_card(self, card_id: str, **kwargs) -> TrelloCard:
|
63 |
+
"""Updates a card's attributes.
|
64 |
+
|
65 |
+
Args:
|
66 |
+
card_id (str): The ID of the card to update.
|
67 |
+
**kwargs: Keyword arguments representing the attributes to update on the card.
|
68 |
+
|
69 |
+
Returns:
|
70 |
+
TrelloCard: The updated card object.
|
71 |
+
"""
|
72 |
+
response = await self.client.PUT(f"/cards/{card_id}", data=kwargs)
|
73 |
+
return TrelloCard(**response)
|
74 |
+
|
75 |
+
async def delete_card(self, card_id: str) -> Dict[str, Any]:
|
76 |
+
"""Deletes a card.
|
77 |
+
|
78 |
+
Args:
|
79 |
+
card_id (str): The ID of the card to delete.
|
80 |
+
|
81 |
+
Returns:
|
82 |
+
Dict[str, Any]: The response from the delete operation.
|
83 |
+
"""
|
84 |
+
return await self.client.DELETE(f"/cards/{card_id}")
|
pmcp/mcp_server/trello/services/checklist.py
ADDED
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
from typing import Dict, List, Optional
|
3 |
+
|
4 |
+
from pmcp.mcp_server.trello.utils.trello_api import TrelloClient
|
5 |
+
|
6 |
+
|
7 |
+
class ChecklistService:
|
8 |
+
"""
|
9 |
+
Service class for handling Trello checklist operations.
|
10 |
+
"""
|
11 |
+
|
12 |
+
def __init__(self, client: TrelloClient):
|
13 |
+
self.client = client
|
14 |
+
|
15 |
+
async def get_checklist(self, checklist_id: str) -> Dict:
|
16 |
+
"""
|
17 |
+
Get a specific checklist by ID.
|
18 |
+
|
19 |
+
Args:
|
20 |
+
checklist_id (str): The ID of the checklist to retrieve
|
21 |
+
|
22 |
+
Returns:
|
23 |
+
Dict: The checklist data
|
24 |
+
"""
|
25 |
+
return await self.client.GET(f"/checklists/{checklist_id}")
|
26 |
+
|
27 |
+
async def get_card_checklists(self, card_id: str) -> List[Dict]:
|
28 |
+
"""
|
29 |
+
Get all checklists for a specific card.
|
30 |
+
|
31 |
+
Args:
|
32 |
+
card_id (str): The ID of the card to get checklists for
|
33 |
+
|
34 |
+
Returns:
|
35 |
+
List[Dict]: List of checklists on the card
|
36 |
+
"""
|
37 |
+
return await self.client.GET(f"/cards/{card_id}/checklists")
|
38 |
+
|
39 |
+
async def create_checklist(
|
40 |
+
self, card_id: str, name: str, pos: Optional[str] = None
|
41 |
+
) -> Dict:
|
42 |
+
"""
|
43 |
+
Create a new checklist on a card.
|
44 |
+
|
45 |
+
Args:
|
46 |
+
card_id (str): The ID of the card to create the checklist on
|
47 |
+
name (str): The name of the checklist
|
48 |
+
pos (Optional[str]): The position of the checklist (top, bottom, or a positive number)
|
49 |
+
|
50 |
+
Returns:
|
51 |
+
Dict: The created checklist data
|
52 |
+
"""
|
53 |
+
data = {"name": name}
|
54 |
+
if pos:
|
55 |
+
data["pos"] = pos
|
56 |
+
return await self.client.POST(f"/checklists", data={"idCard": card_id, **data})
|
57 |
+
|
58 |
+
async def update_checklist(
|
59 |
+
self, checklist_id: str, name: Optional[str] = None, pos: Optional[str] = None
|
60 |
+
) -> Dict:
|
61 |
+
"""
|
62 |
+
Update an existing checklist.
|
63 |
+
|
64 |
+
Args:
|
65 |
+
checklist_id (str): The ID of the checklist to update
|
66 |
+
name (Optional[str]): New name for the checklist
|
67 |
+
pos (Optional[str]): New position for the checklist
|
68 |
+
|
69 |
+
Returns:
|
70 |
+
Dict: The updated checklist data
|
71 |
+
"""
|
72 |
+
data = {}
|
73 |
+
if name:
|
74 |
+
data["name"] = name
|
75 |
+
if pos:
|
76 |
+
data["pos"] = pos
|
77 |
+
return await self.client.PUT(f"/checklists/{checklist_id}", data=data)
|
78 |
+
|
79 |
+
async def delete_checklist(self, checklist_id: str) -> Dict:
|
80 |
+
"""
|
81 |
+
Delete a checklist.
|
82 |
+
|
83 |
+
Args:
|
84 |
+
checklist_id (str): The ID of the checklist to delete
|
85 |
+
|
86 |
+
Returns:
|
87 |
+
Dict: The response from the delete operation
|
88 |
+
"""
|
89 |
+
return await self.client.DELETE(f"/checklists/{checklist_id}")
|
90 |
+
|
91 |
+
async def add_checkitem(
|
92 |
+
self,
|
93 |
+
checklist_id: str,
|
94 |
+
name: str,
|
95 |
+
checked: bool = False,
|
96 |
+
pos: Optional[str] = None,
|
97 |
+
) -> Dict:
|
98 |
+
"""
|
99 |
+
Add a new item to a checklist.
|
100 |
+
|
101 |
+
Args:
|
102 |
+
checklist_id (str): The ID of the checklist to add the item to
|
103 |
+
name (str): The name of the checkitem
|
104 |
+
checked (bool): Whether the item is checked
|
105 |
+
pos (Optional[str]): The position of the item
|
106 |
+
|
107 |
+
Returns:
|
108 |
+
Dict: The created checkitem data
|
109 |
+
"""
|
110 |
+
data = {"name": name, "checked": checked}
|
111 |
+
if pos:
|
112 |
+
data["pos"] = pos
|
113 |
+
return await self.client.POST(
|
114 |
+
f"/checklists/{checklist_id}/checkItems", data=data
|
115 |
+
)
|
116 |
+
|
117 |
+
async def update_checkitem(
|
118 |
+
self,
|
119 |
+
checklist_id: str,
|
120 |
+
checkitem_id: str,
|
121 |
+
name: Optional[str] = None,
|
122 |
+
checked: Optional[bool] = None,
|
123 |
+
pos: Optional[str] = None,
|
124 |
+
) -> Dict:
|
125 |
+
"""
|
126 |
+
Update a checkitem in a checklist.
|
127 |
+
|
128 |
+
Args:
|
129 |
+
checklist_id (str): The ID of the checklist containing the item
|
130 |
+
checkitem_id (str): The ID of the checkitem to update
|
131 |
+
name (Optional[str]): New name for the checkitem
|
132 |
+
checked (Optional[bool]): New checked state
|
133 |
+
pos (Optional[str]): New position for the item
|
134 |
+
|
135 |
+
Returns:
|
136 |
+
Dict: The updated checkitem data
|
137 |
+
"""
|
138 |
+
data = {}
|
139 |
+
if name:
|
140 |
+
data["name"] = name
|
141 |
+
if checked is not None:
|
142 |
+
data["checked"] = checked
|
143 |
+
if pos:
|
144 |
+
data["pos"] = pos
|
145 |
+
return await self.client.PUT(
|
146 |
+
f"/checklists/{checklist_id}/checkItems/{checkitem_id}", data=data
|
147 |
+
)
|
148 |
+
|
149 |
+
async def delete_checkitem(self, checklist_id: str, checkitem_id: str) -> Dict:
|
150 |
+
"""
|
151 |
+
Delete a checkitem from a checklist.
|
152 |
+
|
153 |
+
Args:
|
154 |
+
checklist_id (str): The ID of the checklist containing the item
|
155 |
+
checkitem_id (str): The ID of the checkitem to delete
|
156 |
+
|
157 |
+
Returns:
|
158 |
+
Dict: The response from the delete operation
|
159 |
+
"""
|
160 |
+
return await self.client.DELETE(
|
161 |
+
f"/checklists/{checklist_id}/checkItems/{checkitem_id}"
|
162 |
+
)
|
pmcp/mcp_server/trello/services/list.py
ADDED
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List
|
2 |
+
|
3 |
+
from pmcp.mcp_server.trello.models import TrelloList
|
4 |
+
from pmcp.mcp_server.trello.utils.trello_api import TrelloClient
|
5 |
+
|
6 |
+
|
7 |
+
class ListService:
|
8 |
+
"""
|
9 |
+
Service class for managing Trello lists.
|
10 |
+
"""
|
11 |
+
|
12 |
+
def __init__(self, client: TrelloClient):
|
13 |
+
self.client = client
|
14 |
+
|
15 |
+
# Lists
|
16 |
+
async def get_list(self, list_id: str) -> TrelloList:
|
17 |
+
"""Retrieves a specific list by its ID.
|
18 |
+
|
19 |
+
Args:
|
20 |
+
list_id (str): The ID of the list to retrieve.
|
21 |
+
|
22 |
+
Returns:
|
23 |
+
TrelloList: The list object containing list details.
|
24 |
+
"""
|
25 |
+
response = await self.client.GET(f"/lists/{list_id}")
|
26 |
+
return TrelloList(**response)
|
27 |
+
|
28 |
+
async def get_lists(self, board_id: str) -> List[TrelloList]:
|
29 |
+
"""Retrieves all lists on a given board.
|
30 |
+
|
31 |
+
Args:
|
32 |
+
board_id (str): The ID of the board whose lists to retrieve.
|
33 |
+
|
34 |
+
Returns:
|
35 |
+
List[TrelloList]: A list of list objects.
|
36 |
+
"""
|
37 |
+
response = await self.client.GET(f"/boards/{board_id}/lists")
|
38 |
+
return [TrelloList(**list_data) for list_data in response]
|
39 |
+
|
40 |
+
async def create_list(
|
41 |
+
self, board_id: str, name: str, pos: str = "bottom"
|
42 |
+
) -> TrelloList:
|
43 |
+
"""Creates a new list on a given board.
|
44 |
+
|
45 |
+
Args:
|
46 |
+
board_id (str): The ID of the board to create the list in.
|
47 |
+
name (str): The name of the new list.
|
48 |
+
pos (str, optional): The position of the new list. Can be "top" or "bottom". Defaults to "bottom".
|
49 |
+
|
50 |
+
Returns:
|
51 |
+
TrelloList: The newly created list object.
|
52 |
+
"""
|
53 |
+
data = {"name": name, "idBoard": board_id, "pos": pos}
|
54 |
+
response = await self.client.POST("/lists", data=data)
|
55 |
+
return TrelloList(**response)
|
56 |
+
|
57 |
+
async def update_list(self, list_id: str, name: str) -> TrelloList:
|
58 |
+
"""Updates the name of a list.
|
59 |
+
|
60 |
+
Args:
|
61 |
+
list_id (str): The ID of the list to update.
|
62 |
+
name (str): The new name for the list.
|
63 |
+
|
64 |
+
Returns:
|
65 |
+
TrelloList: The updated list object.
|
66 |
+
"""
|
67 |
+
response = await self.client.PUT(f"/lists/{list_id}", data={"name": name})
|
68 |
+
return TrelloList(**response)
|
69 |
+
|
70 |
+
async def delete_list(self, list_id: str) -> TrelloList:
|
71 |
+
"""Archives a list.
|
72 |
+
|
73 |
+
Args:
|
74 |
+
list_id (str): The ID of the list to close.
|
75 |
+
|
76 |
+
Returns:
|
77 |
+
TrelloList: The archived list object.
|
78 |
+
"""
|
79 |
+
response = await self.client.PUT(
|
80 |
+
f"/lists/{list_id}/closed", data={"value": "true"}
|
81 |
+
)
|
82 |
+
return TrelloList(**response)
|
pmcp/mcp_server/trello/tools/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
|
pmcp/mcp_server/trello/tools/board.py
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This module contains tools for managing Trello boards.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import logging
|
6 |
+
from typing import List
|
7 |
+
|
8 |
+
from mcp.server.fastmcp import Context
|
9 |
+
|
10 |
+
from pmcp.mcp_server.trello.models import TrelloBoard, TrelloLabel
|
11 |
+
from pmcp.mcp_server.trello.services.board import BoardService
|
12 |
+
from pmcp.mcp_server.trello.trello import client
|
13 |
+
|
14 |
+
|
15 |
+
|
16 |
+
service = BoardService(client)
|
17 |
+
|
18 |
+
|
19 |
+
async def get_board(ctx: Context, board_id: str) -> TrelloBoard:
|
20 |
+
"""Retrieves a specific board by its ID.
|
21 |
+
|
22 |
+
Args:
|
23 |
+
board_id (str): The ID of the board to retrieve.
|
24 |
+
|
25 |
+
Returns:
|
26 |
+
TrelloBoard: The board object containing board details.
|
27 |
+
"""
|
28 |
+
try:
|
29 |
+
result = await service.get_board(board_id)
|
30 |
+
return result
|
31 |
+
except Exception as e:
|
32 |
+
error_msg = f"Failed to get board: {str(e)}"
|
33 |
+
await ctx.error(error_msg)
|
34 |
+
raise
|
35 |
+
|
36 |
+
|
37 |
+
async def get_boards(ctx: Context) -> List[TrelloBoard]:
|
38 |
+
"""Retrieves all boards for the authenticated user.
|
39 |
+
|
40 |
+
Returns:
|
41 |
+
List[TrelloBoard]: A list of board objects.
|
42 |
+
"""
|
43 |
+
try:
|
44 |
+
result = await service.get_boards()
|
45 |
+
return result
|
46 |
+
except Exception as e:
|
47 |
+
error_msg = f"Failed to get boards: {str(e)}"
|
48 |
+
await ctx.error(error_msg)
|
49 |
+
raise
|
50 |
+
|
51 |
+
|
52 |
+
async def get_board_labels(ctx: Context, board_id: str) -> List[TrelloLabel]:
|
53 |
+
"""Retrieves all labels for a specific board.
|
54 |
+
|
55 |
+
Args:
|
56 |
+
board_id (str): The ID of the board whose labels to retrieve.
|
57 |
+
|
58 |
+
Returns:
|
59 |
+
List[TrelloLabel]: A list of label objects for the board.
|
60 |
+
"""
|
61 |
+
try:
|
62 |
+
result = await service.get_board_labels(board_id)
|
63 |
+
return result
|
64 |
+
except Exception as e:
|
65 |
+
error_msg = f"Failed to get board labels: {str(e)}"
|
66 |
+
await ctx.error(error_msg)
|
67 |
+
raise
|
pmcp/mcp_server/trello/tools/card.py
ADDED
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This module contains tools for managing Trello cards.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import logging
|
6 |
+
from typing import List
|
7 |
+
|
8 |
+
from mcp.server.fastmcp import Context
|
9 |
+
|
10 |
+
from pmcp.mcp_server.trello.models import TrelloCard
|
11 |
+
from pmcp.mcp_server.trello.services.card import CardService
|
12 |
+
from pmcp.mcp_server.trello.trello import client
|
13 |
+
from pmcp.mcp_server.trello.dtos.update_card import UpdateCardPayload
|
14 |
+
|
15 |
+
|
16 |
+
service = CardService(client)
|
17 |
+
|
18 |
+
|
19 |
+
async def get_card(ctx: Context, card_id: str) -> TrelloCard:
|
20 |
+
"""Retrieves a specific card by its ID.
|
21 |
+
|
22 |
+
Args:
|
23 |
+
card_id (str): The ID of the card to retrieve.
|
24 |
+
|
25 |
+
Returns:
|
26 |
+
TrelloCard: The card object containing card details.
|
27 |
+
"""
|
28 |
+
try:
|
29 |
+
result = await service.get_card(card_id)
|
30 |
+
return result
|
31 |
+
except Exception as e:
|
32 |
+
error_msg = f"Failed to get card: {str(e)}"
|
33 |
+
await ctx.error(error_msg)
|
34 |
+
raise
|
35 |
+
|
36 |
+
|
37 |
+
async def get_cards(ctx: Context, list_id: str) -> List[TrelloCard]:
|
38 |
+
"""Retrieves all cards in a given list.
|
39 |
+
|
40 |
+
Args:
|
41 |
+
list_id (str): The ID of the list whose cards to retrieve.
|
42 |
+
|
43 |
+
Returns:
|
44 |
+
List[TrelloCard]: A list of card objects.
|
45 |
+
"""
|
46 |
+
try:
|
47 |
+
result = await service.get_cards(list_id)
|
48 |
+
return result
|
49 |
+
except Exception as e:
|
50 |
+
error_msg = f"Failed to get cards: {str(e)}"
|
51 |
+
await ctx.error(error_msg)
|
52 |
+
raise
|
53 |
+
|
54 |
+
|
55 |
+
async def create_card(
|
56 |
+
ctx: Context, list_id: str, name: str, desc: str | None = None
|
57 |
+
) -> TrelloCard:
|
58 |
+
"""Creates a new card in a given list.
|
59 |
+
|
60 |
+
Args:
|
61 |
+
list_id (str): The ID of the list to create the card in.
|
62 |
+
name (str): The name of the new card.
|
63 |
+
desc (str, optional): The description of the new card. Defaults to None.
|
64 |
+
|
65 |
+
Returns:
|
66 |
+
TrelloCard: The newly created card object.
|
67 |
+
"""
|
68 |
+
try:
|
69 |
+
result = await service.create_card(list_id, name, desc)
|
70 |
+
return result
|
71 |
+
except Exception as e:
|
72 |
+
error_msg = f"Failed to create card: {str(e)}"
|
73 |
+
await ctx.error(error_msg)
|
74 |
+
raise
|
75 |
+
|
76 |
+
|
77 |
+
async def update_card(
|
78 |
+
ctx: Context, card_id: str, payload: UpdateCardPayload
|
79 |
+
) -> TrelloCard:
|
80 |
+
"""Updates a card's attributes.
|
81 |
+
|
82 |
+
Args:
|
83 |
+
card_id (str): The ID of the card to update.
|
84 |
+
**kwargs: Keyword arguments representing the attributes to update on the card.
|
85 |
+
|
86 |
+
Returns:
|
87 |
+
TrelloCard: The updated card object.
|
88 |
+
"""
|
89 |
+
try:
|
90 |
+
result = await service.update_card(
|
91 |
+
card_id, **payload.model_dump(exclude_unset=True)
|
92 |
+
)
|
93 |
+
return result
|
94 |
+
except Exception as e:
|
95 |
+
error_msg = f"Failed to update card: {str(e)}"
|
96 |
+
await ctx.error(error_msg)
|
97 |
+
raise
|
98 |
+
|
99 |
+
|
100 |
+
async def delete_card(ctx: Context, card_id: str) -> dict:
|
101 |
+
"""Deletes a card.
|
102 |
+
|
103 |
+
Args:
|
104 |
+
card_id (str): The ID of the card to delete.
|
105 |
+
|
106 |
+
Returns:
|
107 |
+
dict: The response from the delete operation.
|
108 |
+
"""
|
109 |
+
try:
|
110 |
+
result = await service.delete_card(card_id)
|
111 |
+
return result
|
112 |
+
except Exception as e:
|
113 |
+
error_msg = f"Failed to delete card: {str(e)}"
|
114 |
+
await ctx.error(error_msg)
|
115 |
+
raise
|
pmcp/mcp_server/trello/tools/checklist.py
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This module contains tools for managing Trello checklists.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import logging
|
6 |
+
from typing import Dict, List, Optional
|
7 |
+
|
8 |
+
from pmcp.mcp_server.trello.services.checklist import ChecklistService
|
9 |
+
from pmcp.mcp_server.trello.trello import client
|
10 |
+
|
11 |
+
service = ChecklistService(client)
|
12 |
+
|
13 |
+
|
14 |
+
async def get_checklist(checklist_id: str) -> Dict:
|
15 |
+
"""
|
16 |
+
Get a specific checklist by ID.
|
17 |
+
|
18 |
+
Args:
|
19 |
+
checklist_id (str): The ID of the checklist to retrieve
|
20 |
+
|
21 |
+
Returns:
|
22 |
+
Dict: The checklist data
|
23 |
+
"""
|
24 |
+
return await service.get_checklist(checklist_id)
|
25 |
+
|
26 |
+
|
27 |
+
async def get_card_checklists(card_id: str) -> List[Dict]:
|
28 |
+
"""
|
29 |
+
Get all checklists for a specific card.
|
30 |
+
|
31 |
+
Args:
|
32 |
+
card_id (str): The ID of the card to get checklists for
|
33 |
+
|
34 |
+
Returns:
|
35 |
+
List[Dict]: List of checklists on the card
|
36 |
+
"""
|
37 |
+
return await service.get_card_checklists(card_id)
|
38 |
+
|
39 |
+
|
40 |
+
async def create_checklist(card_id: str, name: str, pos: Optional[str] = None) -> Dict:
|
41 |
+
"""
|
42 |
+
Create a new checklist on a card.
|
43 |
+
|
44 |
+
Args:
|
45 |
+
card_id (str): The ID of the card to create the checklist on
|
46 |
+
name (str): The name of the checklist
|
47 |
+
pos (Optional[str]): The position of the checklist (top, bottom, or a positive number)
|
48 |
+
|
49 |
+
Returns:
|
50 |
+
Dict: The created checklist data
|
51 |
+
"""
|
52 |
+
return await service.create_checklist(card_id, name, pos)
|
53 |
+
|
54 |
+
|
55 |
+
async def update_checklist(
|
56 |
+
checklist_id: str, name: Optional[str] = None, pos: Optional[str] = None
|
57 |
+
) -> Dict:
|
58 |
+
"""
|
59 |
+
Update an existing checklist.
|
60 |
+
|
61 |
+
Args:
|
62 |
+
checklist_id (str): The ID of the checklist to update
|
63 |
+
name (Optional[str]): New name for the checklist
|
64 |
+
pos (Optional[str]): New position for the checklist
|
65 |
+
|
66 |
+
Returns:
|
67 |
+
Dict: The updated checklist data
|
68 |
+
"""
|
69 |
+
return await service.update_checklist(checklist_id, name, pos)
|
70 |
+
|
71 |
+
|
72 |
+
async def delete_checklist(checklist_id: str) -> Dict:
|
73 |
+
"""
|
74 |
+
Delete a checklist.
|
75 |
+
|
76 |
+
Args:
|
77 |
+
checklist_id (str): The ID of the checklist to delete
|
78 |
+
|
79 |
+
Returns:
|
80 |
+
Dict: The response from the delete operation
|
81 |
+
"""
|
82 |
+
return await service.delete_checklist(checklist_id)
|
83 |
+
|
84 |
+
|
85 |
+
async def add_checkitem(
|
86 |
+
checklist_id: str, name: str, checked: bool = False, pos: Optional[str] = None
|
87 |
+
) -> Dict:
|
88 |
+
"""
|
89 |
+
Add a new item to a checklist.
|
90 |
+
|
91 |
+
Args:
|
92 |
+
checklist_id (str): The ID of the checklist to add the item to
|
93 |
+
name (str): The name of the checkitem
|
94 |
+
checked (bool): Whether the item is checked
|
95 |
+
pos (Optional[str]): The position of the item
|
96 |
+
|
97 |
+
Returns:
|
98 |
+
Dict: The created checkitem data
|
99 |
+
"""
|
100 |
+
return await service.add_checkitem(checklist_id, name, checked, pos)
|
101 |
+
|
102 |
+
|
103 |
+
async def update_checkitem(
|
104 |
+
checklist_id: str,
|
105 |
+
checkitem_id: str,
|
106 |
+
name: Optional[str] = None,
|
107 |
+
checked: Optional[bool] = None,
|
108 |
+
pos: Optional[str] = None,
|
109 |
+
) -> Dict:
|
110 |
+
"""
|
111 |
+
Update a checkitem in a checklist.
|
112 |
+
|
113 |
+
Args:
|
114 |
+
checklist_id (str): The ID of the checklist containing the item
|
115 |
+
checkitem_id (str): The ID of the checkitem to update
|
116 |
+
name (Optional[str]): New name for the checkitem
|
117 |
+
checked (Optional[bool]): New checked state
|
118 |
+
pos (Optional[str]): New position for the item
|
119 |
+
|
120 |
+
Returns:
|
121 |
+
Dict: The updated checkitem data
|
122 |
+
"""
|
123 |
+
return await service.update_checkitem(
|
124 |
+
checklist_id, checkitem_id, name, checked, pos
|
125 |
+
)
|
126 |
+
|
127 |
+
|
128 |
+
async def delete_checkitem(checklist_id: str, checkitem_id: str) -> Dict:
|
129 |
+
"""
|
130 |
+
Delete a checkitem from a checklist.
|
131 |
+
|
132 |
+
Args:
|
133 |
+
checklist_id (str): The ID of the checklist containing the item
|
134 |
+
checkitem_id (str): The ID of the checkitem to delete
|
135 |
+
|
136 |
+
Returns:
|
137 |
+
Dict: The response from the delete operation
|
138 |
+
"""
|
139 |
+
return await service.delete_checkitem(checklist_id, checkitem_id)
|
pmcp/mcp_server/trello/tools/list.py
ADDED
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This module contains tools for managing Trello lists.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import logging
|
6 |
+
from typing import List
|
7 |
+
|
8 |
+
from mcp.server.fastmcp import Context
|
9 |
+
|
10 |
+
from pmcp.mcp_server.trello.models import TrelloList
|
11 |
+
from pmcp.mcp_server.trello.services.list import ListService
|
12 |
+
from pmcp.mcp_server.trello.trello import client
|
13 |
+
|
14 |
+
|
15 |
+
|
16 |
+
service = ListService(client)
|
17 |
+
|
18 |
+
|
19 |
+
# List Tools
|
20 |
+
async def get_list(ctx: Context, list_id: str) -> TrelloList:
|
21 |
+
"""Retrieves a specific list by its ID.
|
22 |
+
|
23 |
+
Args:
|
24 |
+
list_id (str): The ID of the list to retrieve.
|
25 |
+
|
26 |
+
Returns:
|
27 |
+
TrelloList: The list object containing list details.
|
28 |
+
"""
|
29 |
+
try:
|
30 |
+
result = await service.get_list(list_id)
|
31 |
+
return result
|
32 |
+
except Exception as e:
|
33 |
+
error_msg = f"Failed to get list: {str(e)}"
|
34 |
+
await ctx.error(error_msg)
|
35 |
+
raise
|
36 |
+
|
37 |
+
|
38 |
+
async def get_lists(ctx: Context, board_id: str) -> List[TrelloList]:
|
39 |
+
"""Retrieves all lists on a given board.
|
40 |
+
|
41 |
+
Args:
|
42 |
+
board_id (str): The ID of the board whose lists to retrieve.
|
43 |
+
|
44 |
+
Returns:
|
45 |
+
List[TrelloList]: A list of list objects.
|
46 |
+
"""
|
47 |
+
try:
|
48 |
+
result = await service.get_lists(board_id)
|
49 |
+
return result
|
50 |
+
except Exception as e:
|
51 |
+
error_msg = f"Failed to get lists: {str(e)}"
|
52 |
+
await ctx.error(error_msg)
|
53 |
+
raise
|
54 |
+
|
55 |
+
|
56 |
+
async def create_list(
|
57 |
+
ctx: Context, board_id: str, name: str, pos: str = "bottom"
|
58 |
+
) -> TrelloList:
|
59 |
+
"""Creates a new list on a given board.
|
60 |
+
|
61 |
+
Args:
|
62 |
+
board_id (str): The ID of the board to create the list in.
|
63 |
+
name (str): The name of the new list.
|
64 |
+
pos (str, optional): The position of the new list. Can be "top" or "bottom". Defaults to "bottom".
|
65 |
+
|
66 |
+
Returns:
|
67 |
+
TrelloList: The newly created list object.
|
68 |
+
"""
|
69 |
+
try:
|
70 |
+
result = await service.create_list(board_id, name, pos)
|
71 |
+
return result
|
72 |
+
except Exception as e:
|
73 |
+
error_msg = f"Failed to create list: {str(e)}"
|
74 |
+
await ctx.error(error_msg)
|
75 |
+
raise
|
76 |
+
|
77 |
+
|
78 |
+
async def update_list(ctx: Context, list_id: str, name: str) -> TrelloList:
|
79 |
+
"""Updates the name of a list.
|
80 |
+
|
81 |
+
Args:
|
82 |
+
list_id (str): The ID of the list to update.
|
83 |
+
name (str): The new name for the list.
|
84 |
+
|
85 |
+
Returns:
|
86 |
+
TrelloList: The updated list object.
|
87 |
+
"""
|
88 |
+
try:
|
89 |
+
result = await service.update_list(list_id, name)
|
90 |
+
return result
|
91 |
+
except Exception as e:
|
92 |
+
error_msg = f"Failed to update list: {str(e)}"
|
93 |
+
await ctx.error(error_msg)
|
94 |
+
raise
|
95 |
+
|
96 |
+
|
97 |
+
async def delete_list(ctx: Context, list_id: str) -> TrelloList:
|
98 |
+
"""Archives a list.
|
99 |
+
|
100 |
+
Args:
|
101 |
+
list_id (str): The ID of the list to close.
|
102 |
+
|
103 |
+
Returns:
|
104 |
+
TrelloList: The archived list object.
|
105 |
+
"""
|
106 |
+
try:
|
107 |
+
result = await service.delete_list(list_id)
|
108 |
+
return result
|
109 |
+
except Exception as e:
|
110 |
+
error_msg = f"Failed to delete list: {str(e)}"
|
111 |
+
await ctx.error(error_msg)
|
112 |
+
raise
|
pmcp/mcp_server/trello/tools/tools.py
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This module contains tools for managing Trello boards, lists, and cards.
|
3 |
+
"""
|
4 |
+
|
5 |
+
from pmcp.mcp_server.trello.tools import board, card, checklist, list
|
6 |
+
|
7 |
+
|
8 |
+
def register_tools(mcp):
|
9 |
+
"""Register tools with the MCP server."""
|
10 |
+
# Board Tools
|
11 |
+
mcp.add_tool(board.get_board)
|
12 |
+
mcp.add_tool(board.get_boards)
|
13 |
+
mcp.add_tool(board.get_board_labels)
|
14 |
+
|
15 |
+
# List Tools
|
16 |
+
mcp.add_tool(list.get_list)
|
17 |
+
mcp.add_tool(list.get_lists)
|
18 |
+
mcp.add_tool(list.create_list)
|
19 |
+
mcp.add_tool(list.update_list)
|
20 |
+
mcp.add_tool(list.delete_list)
|
21 |
+
|
22 |
+
# Card Tools
|
23 |
+
mcp.add_tool(card.get_card)
|
24 |
+
mcp.add_tool(card.get_cards)
|
25 |
+
mcp.add_tool(card.create_card)
|
26 |
+
mcp.add_tool(card.update_card)
|
27 |
+
mcp.add_tool(card.delete_card)
|
28 |
+
|
29 |
+
# Checklist Tools
|
30 |
+
mcp.add_tool(checklist.get_checklist)
|
31 |
+
mcp.add_tool(checklist.get_card_checklists)
|
32 |
+
mcp.add_tool(checklist.create_checklist)
|
33 |
+
mcp.add_tool(checklist.update_checklist)
|
34 |
+
mcp.add_tool(checklist.delete_checklist)
|
35 |
+
mcp.add_tool(checklist.add_checkitem)
|
36 |
+
mcp.add_tool(checklist.update_checkitem)
|
37 |
+
mcp.add_tool(checklist.delete_checkitem)
|
pmcp/mcp_server/trello/trello.py
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import os
|
3 |
+
|
4 |
+
from dotenv import load_dotenv
|
5 |
+
|
6 |
+
from pmcp.mcp_server.trello.utils.trello_api import TrelloClient
|
7 |
+
|
8 |
+
|
9 |
+
# Load environment variables
|
10 |
+
load_dotenv()
|
11 |
+
|
12 |
+
|
13 |
+
# Initialize Trello client and service
|
14 |
+
try:
|
15 |
+
api_key = os.getenv("TRELLO_API_KEY")
|
16 |
+
token = os.getenv("TRELLO_TOKEN")
|
17 |
+
if not api_key or not token:
|
18 |
+
raise ValueError(
|
19 |
+
"TRELLO_API_KEY and TRELLO_TOKEN must be set in environment variables"
|
20 |
+
)
|
21 |
+
client = TrelloClient(api_key=api_key, token=token)
|
22 |
+
|
23 |
+
except Exception as e:
|
24 |
+
raise
|
25 |
+
|
26 |
+
|
27 |
+
# Add a prompt for common Trello operations
|
28 |
+
def trello_help() -> str:
|
29 |
+
"""Provides help information about available Trello operations."""
|
30 |
+
return """
|
31 |
+
Available Trello Operations:
|
32 |
+
1. Board Operations:
|
33 |
+
- Get a specific board
|
34 |
+
- List all boards
|
35 |
+
2. List Operations:
|
36 |
+
- Get a specific list
|
37 |
+
- List all lists in a board
|
38 |
+
- Create a new list
|
39 |
+
- Update a list's name
|
40 |
+
- Archive a list
|
41 |
+
3. Card Operations:
|
42 |
+
- Get a specific card
|
43 |
+
- List all cards in a list
|
44 |
+
- Create a new card
|
45 |
+
- Update a card's attributes
|
46 |
+
- Delete a card
|
47 |
+
4. Checklist Operations:
|
48 |
+
- Get a specific checklist
|
49 |
+
- List all checklists in a card
|
50 |
+
- Create a new checklist
|
51 |
+
- Update a checklist
|
52 |
+
- Delete a checklist
|
53 |
+
- Add checkitem to checklist
|
54 |
+
- Update checkitem
|
55 |
+
- Delete checkitem
|
56 |
+
"""
|
pmcp/mcp_server/trello/utils/__init__.py
ADDED
File without changes
|
pmcp/mcp_server/trello/utils/trello_api.py
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# trello_api.py
|
2 |
+
import logging
|
3 |
+
|
4 |
+
import httpx
|
5 |
+
|
6 |
+
|
7 |
+
|
8 |
+
|
9 |
+
TRELLO_API_BASE = "https://api.trello.com/1"
|
10 |
+
|
11 |
+
|
12 |
+
class TrelloClient:
|
13 |
+
"""
|
14 |
+
Client class for interacting with the Trello API over REST.
|
15 |
+
"""
|
16 |
+
|
17 |
+
def __init__(self, api_key: str, token: str):
|
18 |
+
self.api_key = api_key
|
19 |
+
self.token = token
|
20 |
+
self.base_url = TRELLO_API_BASE
|
21 |
+
self.client = httpx.AsyncClient(base_url=self.base_url)
|
22 |
+
|
23 |
+
async def close(self):
|
24 |
+
await self.client.aclose()
|
25 |
+
|
26 |
+
async def GET(self, endpoint: str, params: dict = None):
|
27 |
+
all_params = {"key": self.api_key, "token": self.token}
|
28 |
+
if params:
|
29 |
+
all_params.update(params)
|
30 |
+
try:
|
31 |
+
response = await self.client.get(endpoint, params=all_params)
|
32 |
+
response.raise_for_status()
|
33 |
+
return response.json()
|
34 |
+
except httpx.HTTPStatusError as e:
|
35 |
+
raise httpx.HTTPStatusError(
|
36 |
+
f"Failed to get {endpoint}: {str(e)}",
|
37 |
+
request=e.request,
|
38 |
+
response=e.response,
|
39 |
+
)
|
40 |
+
except httpx.RequestError as e:
|
41 |
+
raise httpx.RequestError(f"Failed to get {endpoint}: {str(e)}")
|
42 |
+
|
43 |
+
async def POST(self, endpoint: str, data: dict = None):
|
44 |
+
all_params = {"key": self.api_key, "token": self.token}
|
45 |
+
try:
|
46 |
+
response = await self.client.post(endpoint, params=all_params, json=data)
|
47 |
+
response.raise_for_status()
|
48 |
+
return response.json()
|
49 |
+
except httpx.HTTPStatusError as e:
|
50 |
+
raise httpx.HTTPStatusError(
|
51 |
+
f"Failed to post to {endpoint}: {str(e)}",
|
52 |
+
request=e.request,
|
53 |
+
response=e.response,
|
54 |
+
)
|
55 |
+
except httpx.RequestError as e:
|
56 |
+
raise httpx.RequestError(f"Failed to post to {endpoint}: {str(e)}")
|
57 |
+
|
58 |
+
async def PUT(self, endpoint: str, data: dict = None):
|
59 |
+
all_params = {"key": self.api_key, "token": self.token}
|
60 |
+
try:
|
61 |
+
response = await self.client.put(endpoint, params=all_params, json=data)
|
62 |
+
response.raise_for_status()
|
63 |
+
return response.json()
|
64 |
+
except httpx.HTTPStatusError as e:
|
65 |
+
raise httpx.HTTPStatusError(
|
66 |
+
f"Failed to put to {endpoint}: {str(e)}",
|
67 |
+
request=e.request,
|
68 |
+
response=e.response,
|
69 |
+
)
|
70 |
+
except httpx.RequestError as e:
|
71 |
+
raise httpx.RequestError(f"Failed to put to {endpoint}: {str(e)}")
|
72 |
+
|
73 |
+
async def DELETE(self, endpoint: str, params: dict = None):
|
74 |
+
all_params = {"key": self.api_key, "token": self.token}
|
75 |
+
if params:
|
76 |
+
all_params.update(params)
|
77 |
+
try:
|
78 |
+
response = await self.client.delete(endpoint, params=all_params)
|
79 |
+
response.raise_for_status()
|
80 |
+
return response.json()
|
81 |
+
except httpx.HTTPStatusError as e:
|
82 |
+
raise httpx.HTTPStatusError(
|
83 |
+
f"Failed to delete {endpoint}: {str(e)}",
|
84 |
+
request=e.request,
|
85 |
+
response=e.response,
|
86 |
+
)
|
87 |
+
except httpx.RequestError as e:
|
88 |
+
raise httpx.RequestError(f"Failed to delete {endpoint}: {str(e)}")
|