MatteoMass commited on
Commit
98ea76a
·
2 Parent(s): ce2a7d1 7a451f9

Merge branch 'feat/mcp_servers' into feat/matt

Browse files
Files changed (41) hide show
  1. app.py +11 -3
  2. pmcp/mcp_server/github/__init__.py +0 -0
  3. pmcp/mcp_server/github/github.py +40 -0
  4. pmcp/mcp_server/github/mcp_github_main.py +35 -0
  5. pmcp/mcp_server/github/models.py +20 -0
  6. pmcp/mcp_server/github/services/__init__.py +0 -0
  7. pmcp/mcp_server/github/services/branches.py +21 -0
  8. pmcp/mcp_server/github/services/contents.py +44 -0
  9. pmcp/mcp_server/github/services/issues.py +86 -0
  10. pmcp/mcp_server/github/services/pull_requests.py +58 -0
  11. pmcp/mcp_server/github/services/repo.py +21 -0
  12. pmcp/mcp_server/github/services/repo_to_text.py +26 -0
  13. pmcp/mcp_server/github/tools/__init__.py +1 -0
  14. pmcp/mcp_server/github/tools/branches.py +31 -0
  15. pmcp/mcp_server/github/tools/contents.py +62 -0
  16. pmcp/mcp_server/github/tools/issues.py +110 -0
  17. pmcp/mcp_server/github/tools/pull_requests.py +68 -0
  18. pmcp/mcp_server/github/tools/repo.py +35 -0
  19. pmcp/mcp_server/github/tools/repo_to_text.py +28 -0
  20. pmcp/mcp_server/github/tools/tools.py +31 -0
  21. pmcp/mcp_server/github/utils/__init__.py +0 -0
  22. pmcp/mcp_server/github/utils/github_api.py +67 -0
  23. pmcp/mcp_server/github/utils/repo_to_text_utils.py +83 -0
  24. pmcp/mcp_server/trello/__init__.py +0 -0
  25. pmcp/mcp_server/trello/dtos/update_card.py +22 -0
  26. pmcp/mcp_server/trello/mcp_trello_main.py +35 -0
  27. pmcp/mcp_server/trello/models.py +47 -0
  28. pmcp/mcp_server/trello/services/__init__.py +0 -0
  29. pmcp/mcp_server/trello/services/board.py +53 -0
  30. pmcp/mcp_server/trello/services/card.py +84 -0
  31. pmcp/mcp_server/trello/services/checklist.py +162 -0
  32. pmcp/mcp_server/trello/services/list.py +82 -0
  33. pmcp/mcp_server/trello/tools/__init__.py +1 -0
  34. pmcp/mcp_server/trello/tools/board.py +67 -0
  35. pmcp/mcp_server/trello/tools/card.py +115 -0
  36. pmcp/mcp_server/trello/tools/checklist.py +139 -0
  37. pmcp/mcp_server/trello/tools/list.py +112 -0
  38. pmcp/mcp_server/trello/tools/tools.py +37 -0
  39. pmcp/mcp_server/trello/trello.py +56 -0
  40. pmcp/mcp_server/trello/utils/__init__.py +0 -0
  41. 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 an assistant that can manage Trello boards and projects.
16
- You will be given a set of tools to work with. Each time you decide to use a tool that modifies in any way a Trello board, you MUST ask the user if wants to proceed.
17
- If the user's answer is negative, then you have to abort everything and end the conversation.
 
 
 
 
 
 
 
 
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)}")