Gerlando Re commited on
Commit
cc24fae
·
2 Parent(s): 69dad1b40a493b

Merge pull request #4

Browse files

Gradio app, planner with interrupt and CI

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +4 -0
  2. .github/workflows/sync_to_hf.yml +20 -0
  3. .gitignore +1 -0
  4. README.md +16 -2
  5. app.py +72 -190
  6. app_new.py +189 -0
  7. main.py +44 -12
  8. mcpc_graph.py +112 -0
  9. pmcp/agents/executor.py +12 -13
  10. pmcp/agents/github_agent.py +1 -2
  11. pmcp/agents/planner.py +4 -4
  12. pmcp/agents/trello_agent.py +1 -2
  13. pmcp/mcp_server/__init__.py +0 -0
  14. pmcp/mcp_server/github_server/__init__.py +0 -0
  15. pmcp/mcp_server/github_server/connection_test.py +28 -0
  16. pmcp/mcp_server/github_server/github.py +34 -0
  17. pmcp/mcp_server/github_server/mcp_github_main.py +42 -0
  18. pmcp/mcp_server/github_server/models.py +20 -0
  19. pmcp/mcp_server/github_server/services/__init__.py +0 -0
  20. pmcp/mcp_server/github_server/services/branches.py +21 -0
  21. pmcp/mcp_server/github_server/services/contents.py +44 -0
  22. pmcp/mcp_server/github_server/services/issues.py +86 -0
  23. pmcp/mcp_server/github_server/services/pull_requests.py +58 -0
  24. pmcp/mcp_server/github_server/services/repo.py +21 -0
  25. pmcp/mcp_server/github_server/services/repo_to_text.py +26 -0
  26. pmcp/mcp_server/github_server/tools/__init__.py +1 -0
  27. pmcp/mcp_server/github_server/tools/branches.py +31 -0
  28. pmcp/mcp_server/github_server/tools/contents.py +62 -0
  29. pmcp/mcp_server/github_server/tools/issues.py +111 -0
  30. pmcp/mcp_server/github_server/tools/pull_requests.py +68 -0
  31. pmcp/mcp_server/github_server/tools/repo.py +35 -0
  32. pmcp/mcp_server/github_server/tools/repo_to_text.py +28 -0
  33. pmcp/mcp_server/github_server/tools/tools.py +31 -0
  34. pmcp/mcp_server/github_server/utils/__init__.py +0 -0
  35. pmcp/mcp_server/github_server/utils/github_api.py +67 -0
  36. pmcp/mcp_server/github_server/utils/repo_to_text_utils.py +83 -0
  37. pmcp/mcp_server/trello_server/__init__.py +0 -0
  38. pmcp/mcp_server/trello_server/connection_test.py +14 -0
  39. pmcp/mcp_server/trello_server/dtos/update_card.py +22 -0
  40. pmcp/mcp_server/trello_server/mcp_trello_main.py +44 -0
  41. pmcp/mcp_server/trello_server/models.py +47 -0
  42. pmcp/mcp_server/trello_server/services/__init__.py +0 -0
  43. pmcp/mcp_server/trello_server/services/board.py +53 -0
  44. pmcp/mcp_server/trello_server/services/card.py +84 -0
  45. pmcp/mcp_server/trello_server/services/checklist.py +162 -0
  46. pmcp/mcp_server/trello_server/services/list.py +82 -0
  47. pmcp/mcp_server/trello_server/tools/__init__.py +1 -0
  48. pmcp/mcp_server/trello_server/tools/board.py +68 -0
  49. pmcp/mcp_server/trello_server/tools/card.py +115 -0
  50. pmcp/mcp_server/trello_server/tools/checklist.py +139 -0
.env.example ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ MCP_TRELLO_PATH=C:/Users/Matteo/Desktop/dev/trello-mcp-server/main.py
2
+ MCP_GITHUB_PATH=C:/Users/Matteo/Desktop/dev/github-mcp-server/main.py
3
+
4
+ NEBIUS_API_KEY=
.github/workflows/sync_to_hf.yml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face hub
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ workflow_dispatch:
6
+
7
+ jobs:
8
+ sync-to-hub:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v3
12
+ with:
13
+ fetch-depth: 0
14
+ lfs: true
15
+ - name: Push to hub
16
+ env:
17
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
18
+ run: |
19
+ git remote add space https://huggingface.co/spaces/Agents-MCP-Hackathon/PMCP-AgenticProjectManager
20
+ git push -f https://CalleCaje:$HF_TOKEN@huggingface.co/spaces/Agents-MCP-Hackathon/PMCP-AgenticProjectManager
.gitignore CHANGED
@@ -9,3 +9,4 @@ wheels/
9
 
10
  # Virtual environments
11
  .venv
 
 
9
 
10
  # Virtual environments
11
  .venv
12
+ .env
README.md CHANGED
@@ -1,2 +1,16 @@
1
- # pmcp
2
- Project Manager with MCP
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: PMCP AgenticProjectManager
3
+ emoji: ⚡
4
+ colorFrom: yellow
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 5.32.1
8
+ app_file: app.py
9
+ pinned: false
10
+ license: apache-2.0
11
+ short_description: PMCP is an agent that uses MCP to manage a software project
12
+ tags:
13
+ - agent-demo-track
14
+ ---
15
+
16
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py CHANGED
@@ -11,172 +11,8 @@ from langgraph.graph import MessagesState, END, StateGraph
11
  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
 
21
- class LangGraphAgent:
22
- def __init__(self):
23
- self.agent_app = None
24
- self.config = {"configurable": {"thread_id": f"{str(uuid.uuid4())}"}}
25
- self.memory = MemorySaver()
26
-
27
- def reset_thread(self):
28
- """Resets the conversation thread for the agent."""
29
- self.config = {"configurable": {"thread_id": f"{str(uuid.uuid4())}"}}
30
- print(
31
- f"Chat thread reset. New Thread ID: {self.config['configurable']['thread_id']}"
32
- )
33
-
34
- async def setup(self):
35
- print("Setting up LangGraphAgent...")
36
- mcp_client = MultiServerMCPClient(
37
- {
38
- "trello": {
39
- "url": "http://localhost:8000/sse",
40
- "transport": "sse",
41
- }
42
- }
43
- )
44
-
45
- tools = await mcp_client.get_tools()
46
- tool_node = ToolNode(tools)
47
-
48
- # Ensure NEBIUS_API_KEY is set
49
- api_key = os.getenv("NEBIUS_API_KEY")
50
- if not api_key:
51
- raise ValueError("NEBIUS_API_KEY environment variable not set.")
52
-
53
- llm_with_tools = ChatOpenAI(
54
- model="meta-llama/Meta-Llama-3.1-8B-Instruct",
55
- temperature=0.0,
56
- api_key=api_key,
57
- base_url="https://api.studio.nebius.com/v1/",
58
- )
59
- llm_with_tools = llm_with_tools.bind_tools(tools)
60
-
61
- async def call_llm(state: MessagesState):
62
- response = await llm_with_tools.ainvoke(
63
- state["messages"]
64
- ) # Use await for async invoke
65
- return {"messages": [response]}
66
-
67
- graph = StateGraph(MessagesState)
68
- graph.add_node("llm", functools.partial(call_llm))
69
- graph.add_node("tool", tool_node)
70
- graph.set_entry_point("llm")
71
-
72
- def should_continue(state: MessagesState):
73
- last_message = state["messages"][-1]
74
- return (
75
- "tool"
76
- if hasattr(last_message, "tool_calls") and last_message.tool_calls
77
- else END
78
- )
79
-
80
- graph.add_conditional_edges("llm", should_continue, {"tool": "tool", END: END})
81
- graph.add_edge("tool", "llm")
82
-
83
- self.agent_app = graph.compile(checkpointer=self.memory)
84
- print("LangGraphAgent setup complete.")
85
-
86
- async def generate_responses_for_turn(
87
- self, user_message_text: str, is_first_turn_in_ui: bool
88
- ) -> list[str]:
89
- """
90
- Generates a list of bot utterances for the current turn based on user input.
91
- """
92
- langgraph_input_messages = []
93
- # The SYSTEM_PROMPT is added to the graph input only if it's the first UI interaction *and*
94
- # the checkpointer implies it's the start of a new conversation for the thread_id.
95
- # MemorySaver will handle loading history; system prompt is good for first message of a thread.
96
-
97
- # Check current state in memory to decide if SystemMessage is truly needed
98
- thread_state = await self.memory.aget(self.config) # Use aget for async
99
- is_new_thread_conversation = (
100
- not thread_state
101
- or not thread_state.get("values")
102
- or not thread_state["values"]["messages"]
103
- )
104
-
105
- if is_new_thread_conversation:
106
- print("Adding System Prompt for new conversation thread.")
107
- langgraph_input_messages.append(
108
- SystemMessage(content=SYSTEM_PROMPT.strip())
109
- )
110
-
111
- langgraph_input_messages.append(HumanMessage(content=user_message_text))
112
-
113
- bot_responses_this_turn = []
114
- processed_message_ids_in_stream = set()
115
-
116
- async for result in self.agent_app.astream(
117
- {"messages": langgraph_input_messages},
118
- config=self.config,
119
- stream_mode="values",
120
- ):
121
- if not result or "messages" not in result or not result["messages"]:
122
- continue
123
-
124
- latest_message_in_graph_state = result["messages"][-1]
125
-
126
- if (
127
- isinstance(latest_message_in_graph_state, AIMessage)
128
- and latest_message_in_graph_state.id
129
- not in processed_message_ids_in_stream
130
- ):
131
- current_ai_msg = latest_message_in_graph_state
132
- # Add message ID to set of processed messages to avoid duplication from multiple stream events
133
- # for the same underlying message object.
134
- # However, AIMessages can be broken into chunks if streaming content.
135
- # For now, with stream_mode="values", we get full messages.
136
- # The id check is crucial if the same AIMessage object instance appears multiple times in the stream values.
137
-
138
- newly_generated_content_for_this_step = []
139
-
140
- # 1. Handle AIMessage content
141
- if current_ai_msg.content:
142
- # Add content if it's new or different from the last added piece
143
- if (
144
- not bot_responses_this_turn
145
- or bot_responses_this_turn[-1] != current_ai_msg.content
146
- ):
147
- newly_generated_content_for_this_step.append(
148
- str(current_ai_msg.content)
149
- )
150
-
151
- # 2. Handle tool calls
152
- if hasattr(current_ai_msg, "tool_calls") and current_ai_msg.tool_calls:
153
- for tool_call in current_ai_msg.tool_calls:
154
- # Check if this specific tool call has been processed (e.g., by ID if available, or by content)
155
- # For simplicity, we assume each tool_call in a new AIMessage is distinct for now
156
- call_str = f"**Tool Call:** `{tool_call['name']}`\n*Arguments:* `{tool_call['args']}`"
157
- newly_generated_content_for_this_step.append(call_str)
158
-
159
- if newly_generated_content_for_this_step:
160
- bot_responses_this_turn.extend(
161
- newly_generated_content_for_this_step
162
- )
163
- processed_message_ids_in_stream.add(
164
- current_ai_msg.id
165
- ) # Mark this AIMessage ID as processed
166
-
167
- # Deduplicate consecutive identical messages that might arise from streaming nuances
168
- final_bot_responses = []
169
- if bot_responses_this_turn:
170
- final_bot_responses.append(bot_responses_this_turn[0])
171
- for i in range(1, len(bot_responses_this_turn)):
172
- if bot_responses_this_turn[i] != bot_responses_this_turn[i - 1]:
173
- final_bot_responses.append(bot_responses_this_turn[i])
174
-
175
- return final_bot_responses
176
-
177
-
178
- agent = LangGraphAgent()
179
-
180
  # Apply a theme
181
  theme = gr.themes.Soft(
182
  primary_hue="blue", secondary_hue="sky", neutral_hue="slate"
@@ -191,35 +27,50 @@ theme = gr.themes.Soft(
191
  button_secondary_text_color="white",
192
  )
193
 
194
- with gr.Blocks(theme=theme, title="Trello AI Assistant") as demo:
195
  gr.Markdown(
196
  """
197
- # Trello AI Assistant
198
- Manage your Trello boards and projects with AI assistance.
199
  """
200
  )
201
-
202
- chatbot = gr.Chatbot(
203
- label="Conversation",
204
- bubble_full_width=False,
205
- height=600,
206
- avatar_images=(
207
- None,
208
- "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/Brandon_Sanderson_in_2018.jpg/800px-Brandon_Sanderson_in_2018.jpg?20230101015657",
209
- ),
210
- # (user_avatar, bot_avatar) - replace bot avatar with something better or remove
211
- )
212
-
213
  with gr.Row():
214
- msg = gr.Textbox(
215
- label="Your Message",
216
- placeholder="Type your message here and press Enter to send...",
217
- scale=4, # Make textbox take more space
218
- autofocus=True,
219
- )
220
- # submit_button = gr.Button("Send", variant="primary", scale=1) # Optional: if you want an explicit send button
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
- clear_button = gr.Button("🗑️ Clear Chat & Reset Conversation")
 
 
 
 
 
 
 
 
 
223
 
224
  async def respond(user_message_text, chat_history):
225
  if not user_message_text.strip():
@@ -253,14 +104,45 @@ with gr.Blocks(theme=theme, title="Trello AI Assistant") as demo:
253
 
254
  # Event handlers
255
  msg.submit(respond, [msg, chatbot], [chatbot, msg])
256
-
257
- # if submit_button: # If you add explicit send button
258
- # submit_button.click(respond, [msg, chatbot], [chatbot, msg])
259
 
260
  def clear_chat_and_reset_agent():
261
  agent.reset_thread()
262
  return [], "" # Clears chatbot UI and textbox
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
 
 
 
 
 
 
 
264
  clear_button.click(clear_chat_and_reset_agent, None, [chatbot, msg], queue=False)
265
 
266
  # Load agent setup when the app starts
 
11
  from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
12
  from langgraph.checkpoint.memory import MemorySaver
13
 
 
 
 
 
 
14
 
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  # Apply a theme
17
  theme = gr.themes.Soft(
18
  primary_hue="blue", secondary_hue="sky", neutral_hue="slate"
 
27
  button_secondary_text_color="white",
28
  )
29
 
30
+ with gr.Blocks(theme=theme, title="PMCP - Agentic Project Management") as demo:
31
  gr.Markdown(
32
  """
33
+ # PMCP - Agentic Project Management
34
+ Manage your projects with AI assistance - GitHub, Trello.
35
  """
36
  )
37
+
 
 
 
 
 
 
 
 
 
 
 
38
  with gr.Row():
39
+ # Left column for environment variables
40
+ with gr.Column(scale=1):
41
+ gr.Markdown("### Environment Variables")
42
+ github_repo = gr.Textbox(label="GitHub Repo", placeholder="username/repository")
43
+ github_token = gr.Textbox(label="GitHub Token", placeholder="ghp_xxxxxxxxxxxx", type="password")
44
+ trello_api = gr.Textbox(label="Trello API", placeholder="Your Trello API key")
45
+ trello_token = gr.Textbox(label="Trello Token", placeholder="Your Trello token", type="password")
46
+ hf_token = gr.Textbox(label="HF Token", placeholder="Your Hugging Face token", type="password")
47
+
48
+ set_env_button = gr.Button("Set Environment Variables", variant="primary")
49
+ env_status = gr.Markdown("")
50
+
51
+ # Right column for chat interface
52
+ with gr.Column(scale=2):
53
+ chatbot = gr.Chatbot(
54
+ label="Conversation",
55
+ bubble_full_width=False,
56
+ height=600,
57
+ avatar_images=(
58
+ None,
59
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/Brandon_Sanderson_in_2018.jpg/800px-Brandon_Sanderson_in_2018.jpg?20230101015657",
60
+ ),
61
+ # (user_avatar, bot_avatar) - replace bot avatar with something better or remove
62
+ )
63
 
64
+ with gr.Row():
65
+ msg = gr.Textbox(
66
+ label="Your Message",
67
+ placeholder="Type your message here and press Enter to send...",
68
+ scale=4, # Make textbox take more space
69
+ autofocus=True,
70
+ )
71
+ with gr.Column(scale=1):
72
+ submit_button = gr.Button("Send", variant="primary")
73
+ clear_button = gr.Button("Reset Conversation", variant="secondary")
74
 
75
  async def respond(user_message_text, chat_history):
76
  if not user_message_text.strip():
 
104
 
105
  # Event handlers
106
  msg.submit(respond, [msg, chatbot], [chatbot, msg])
107
+ submit_button.click(respond, [msg, chatbot], [chatbot, msg])
 
 
108
 
109
  def clear_chat_and_reset_agent():
110
  agent.reset_thread()
111
  return [], "" # Clears chatbot UI and textbox
112
+
113
+ def set_environment_variables(github_repo, github_token, trello_api, trello_token, hf_token):
114
+ # Set environment variables
115
+ if github_repo:
116
+ os.environ["GITHUB_REPO"] = github_repo
117
+ if github_token:
118
+ os.environ["GITHUB_TOKEN"] = github_token
119
+ if trello_api:
120
+ os.environ["TRELLO_API_KEY"] = trello_api
121
+ if trello_token:
122
+ os.environ["TRELLO_TOKEN"] = trello_token
123
+ if hf_token:
124
+ os.environ["NEBIUS_API_KEY"] = hf_token
125
+
126
+ # Create a message showing which variables were set
127
+ set_vars = []
128
+ if github_repo: set_vars.append("GITHUB_REPO")
129
+ if github_token: set_vars.append("GITHUB_TOKEN")
130
+ if trello_api: set_vars.append("TRELLO_API_KEY")
131
+ if trello_token: set_vars.append("TRELLO_TOKEN")
132
+ if hf_token: set_vars.append("NEBIUS_API_KEY")
133
+
134
+ if set_vars:
135
+ return f"✅ Set environment variables: {', '.join(set_vars)}"
136
+ else:
137
+ return "⚠️ No environment variables were set"
138
 
139
+ # Connect the set environment variables button
140
+ set_env_button.click(
141
+ set_environment_variables,
142
+ inputs=[github_repo, github_token, trello_api, trello_token, hf_token],
143
+ outputs=[env_status]
144
+ )
145
+
146
  clear_button.click(clear_chat_and_reset_agent, None, [chatbot, msg], queue=False)
147
 
148
  # Load agent setup when the app starts
app_new.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ import os
3
+ import gradio as gr
4
+
5
+ from langchain_core.messages import HumanMessage, AIMessage
6
+
7
+ # Assuming mcpc_graph.py and its setup_graph function are in the same directory.
8
+ from mcpc_graph import setup_graph
9
+
10
+
11
+ async def chat_logic(
12
+ message,
13
+ history,
14
+ session_state,
15
+ github_repo,
16
+ github_token,
17
+ trello_api,
18
+ trello_token,
19
+ hf_token,
20
+ ):
21
+ """
22
+ Handles the main chat logic, including environment setup and streaming responses.
23
+
24
+ Args:
25
+ message (str): The user's input message.
26
+ history (list): The chat history managed by Gradio.
27
+ session_state (dict): A dictionary to maintain state across calls for a session.
28
+ github_repo (str): The GitHub repository (username/repo).
29
+ github_token (str): The GitHub personal access token.
30
+ trello_api (str): The Trello API key.
31
+ trello_token (str): The Trello API token.
32
+ hf_token (str): The Hugging Face API token.
33
+
34
+ Yields:
35
+ str: The bot's streaming response or an interruption message.
36
+ """
37
+ # Retrieve the initialized graph and interrupt handler from the session state.
38
+ app = session_state.get("app")
39
+ human_resume_node = session_state.get("human_resume_node")
40
+
41
+ # If the graph is not initialized, this is the first message of the session.
42
+ # We configure the environment and set up the graph.
43
+ if app is None:
44
+ # Check if all required fields have been filled out.
45
+ if not all([github_repo, github_token, trello_api, trello_token, hf_token]):
46
+ yield "Error: Please provide all API keys and the GitHub repository in the 'API Configuration' section before starting the chat."
47
+ return
48
+
49
+ # Set environment variables for the current process.
50
+ os.environ["GITHUB_REPO"] = github_repo
51
+ os.environ["HUGGINGFACE_API_KEY"] = hf_token
52
+
53
+ # Asynchronously initialize the graph and store it in the session state
54
+ # to reuse it for subsequent messages in the same session.
55
+ app, human_resume_node = await setup_graph(
56
+ github_token=github_token, trello_api=trello_api, trello_token=trello_token
57
+ )
58
+ session_state["app"] = app
59
+ session_state["human_resume_node"] = human_resume_node
60
+
61
+ # Ensure a unique thread_id for the conversation.
62
+ thread_id = session_state.get("thread_id")
63
+ if not thread_id:
64
+ thread_id = str(uuid.uuid4())
65
+ session_state["thread_id"] = thread_id
66
+
67
+ # Check if the current message is a response to a human interruption.
68
+ is_message_command = session_state.get("is_message_command", False)
69
+
70
+ config = {
71
+ "configurable": {"thread_id": thread_id},
72
+ "recursion_limit": 100,
73
+ }
74
+
75
+ if is_message_command:
76
+ # The user is providing feedback to an interruption.
77
+ app_input = human_resume_node.call_human_interrupt_agent(message)
78
+ session_state["is_message_command"] = False
79
+ else:
80
+ # A standard user message.
81
+ app_input = {"messages": [HumanMessage(content=message)]}
82
+
83
+ # Stream the graph's response.
84
+ # This revised logic handles intermediate messages and prevents duplication.
85
+ async for res in app.astream(app_input, config=config, stream_mode="values"):
86
+ if "messages" in res:
87
+ last_message = res["messages"][-1]
88
+ # We only stream content from AIMessages. Any intermediate AIMessages
89
+ # (e.g., "I will now use a tool") will be overwritten by subsequent
90
+ # AIMessages in the UI, so only the final answer is visible.
91
+ if isinstance(last_message, AIMessage):
92
+ yield last_message.content
93
+
94
+ elif "__interrupt__" in res:
95
+ # Handle interruptions where the agent needs human feedback.
96
+ interruption_message = res["__interrupt__"][0]
97
+ session_state["is_message_command"] = True
98
+ yield interruption_message.value
99
+ return # Stop the stream and wait for the user's next message.
100
+
101
+
102
+ def create_gradio_app():
103
+ """Creates and launches the Gradio web application."""
104
+ print("Launching Gradio app...")
105
+
106
+ with gr.Blocks(theme=gr.themes.Soft(), title="LangGraph Multi-Agent Chat") as demo:
107
+ session_state = gr.State({})
108
+
109
+ gr.Markdown(
110
+ """
111
+ # LangGraph Multi-Agent Project Manager
112
+
113
+ Interact with a multi-agent system powered by LangGraph.
114
+ You can assign tasks related to Trello and Github.
115
+ The system can be interrupted for human feedback when it needs to use a tool.
116
+ """
117
+ )
118
+
119
+ chatbot = gr.Chatbot(
120
+ [],
121
+ elem_id="chatbot",
122
+ bubble_full_width=False,
123
+ height=600,
124
+ label="Multi-Agent Chat",
125
+ show_label=False,
126
+ )
127
+
128
+ # --- FIX: Added an accordion for API keys and configuration ---
129
+ with gr.Accordion("API Configuration", open=True):
130
+ gr.Markdown(
131
+ "Please enter your credentials. The agent will be configured when you send your first message."
132
+ )
133
+ github_repo = gr.Textbox(
134
+ label="GitHub Repo",
135
+ placeholder="e.g., username/repository",
136
+ info="The target repository for GitHub operations.",
137
+ )
138
+ github_token = gr.Textbox(
139
+ label="GitHub Token",
140
+ placeholder="ghp_xxxxxxxxxxxx",
141
+ type="password",
142
+ info="A fine-grained personal access token.",
143
+ )
144
+ trello_api = gr.Textbox(
145
+ label="Trello API Key",
146
+ placeholder="Your Trello API key",
147
+ info="Your API key from trello.com/power-ups/admin.",
148
+ )
149
+ trello_token = gr.Textbox(
150
+ label="Trello Token",
151
+ placeholder="Your Trello token",
152
+ type="password",
153
+ info="A token generated from your Trello account.",
154
+ )
155
+ hf_token = gr.Textbox(
156
+ label="Hugging Face Token",
157
+ placeholder="hf_xxxxxxxxxxxx",
158
+ type="password",
159
+ info="Used for tools requiring Hugging Face models.",
160
+ )
161
+
162
+ chat_interface = gr.ChatInterface(
163
+ fn=chat_logic,
164
+ chatbot=chatbot,
165
+ additional_inputs=[
166
+ session_state,
167
+ github_repo,
168
+ github_token,
169
+ trello_api,
170
+ trello_token,
171
+ hf_token,
172
+ ],
173
+ title=None,
174
+ description=None,
175
+ )
176
+
177
+ demo.queue()
178
+ demo.launch(debug=True)
179
+
180
+
181
+ if __name__ == "__main__":
182
+ try:
183
+ # The main function to create the app is now synchronous.
184
+ # Gradio handles the async calls within the chat logic.
185
+ create_gradio_app()
186
+ except KeyboardInterrupt:
187
+ print("\nShutting down Gradio app.")
188
+ except Exception as e:
189
+ print(f"An error occurred: {e}")
main.py CHANGED
@@ -1,6 +1,7 @@
1
  import os
2
  import pprint
3
  import uuid
 
4
 
5
  from langchain_mcp_adapters.client import MultiServerMCPClient
6
  from langchain_openai import ChatOpenAI
@@ -9,13 +10,19 @@ from langgraph.graph import MessagesState, END, StateGraph
9
  from langchain_core.messages import HumanMessage
10
  from langgraph.checkpoint.memory import MemorySaver
11
 
 
12
  from pmcp.agents.executor import ExecutorAgent
13
  from pmcp.agents.trello_agent import TrelloAgent
14
  from pmcp.agents.github_agent import GithubAgent
15
  from pmcp.agents.planner import PlannerAgent
16
 
 
 
 
17
  from pmcp.models.state import PlanningState
18
 
 
 
19
 
20
  async def call_llm(llm_with_tools: ChatOpenAI, state: MessagesState):
21
  response = llm_with_tools.invoke(state["messages"])
@@ -26,17 +33,18 @@ async def main():
26
  mcp_client_trello = MultiServerMCPClient(
27
  {
28
  "trello": {
29
- "url": "http://localhost:8000/sse",
30
- "transport": "sse",
 
31
  }
32
  }
33
  )
34
-
35
  mcp_client_github = MultiServerMCPClient(
36
  {
37
  "github": {
38
- "url": "http://localhost:8002/sse",
39
- "transport": "sse",
 
40
  }
41
  }
42
  )
@@ -45,6 +53,7 @@ async def main():
45
 
46
  trello_tools = await mcp_client_trello.get_tools()
47
  github_tools = await mcp_client_github.get_tools()
 
48
  tool_node = ToolNode(github_tools + trello_tools)
49
 
50
  llm = ChatOpenAI(
@@ -66,23 +75,30 @@ async def main():
66
  )
67
  executor_agent = ExecutorAgent(llm=llm)
68
 
 
 
 
 
 
69
  graph = StateGraph(MessagesState)
70
  graph.add_node(planner_agent.agent.agent_name, planner_agent.acall_planner_agent)
71
  graph.add_node(trello_agent.agent.agent_name, trello_agent.acall_trello_agent)
72
  graph.add_node(github_agent.agent.agent_name, github_agent.acall_github_agent)
73
  graph.add_node(executor_agent.agent.agent_name, executor_agent.acall_executor_agent)
74
  graph.add_node("tool", tool_node)
 
75
  graph.set_entry_point(planner_agent.agent.agent_name)
76
 
77
  def should_continue(state: PlanningState):
78
  last_message = state.messages[-1]
79
  if last_message.tool_calls:
80
- return "tool"
81
  return executor_agent.agent.agent_name
82
 
83
  def execute_agent(state: PlanningState):
84
  if state.current_step:
85
  return state.current_step.agent
 
86
  return END
87
 
88
  graph.add_conditional_edges(trello_agent.agent.agent_name, should_continue)
@@ -94,22 +110,38 @@ async def main():
94
  graph.add_edge(planner_agent.agent.agent_name, executor_agent.agent.agent_name)
95
 
96
  app = graph.compile(checkpointer=memory)
97
- app.get_graph(xray=True).draw_mermaid_png()
98
 
99
  user_input = input("user >")
100
- config = {"configurable": {"thread_id": f"{str(uuid.uuid4())}"}}
 
 
 
101
 
 
102
  while user_input.lower() != "q":
103
- async for res in app.astream(
104
- {
 
 
 
 
 
 
105
  "messages": [
106
  HumanMessage(content=user_input),
107
  ]
108
- },
 
 
109
  config=config,
110
  stream_mode="values",
111
  ):
112
- pprint.pprint(res["messages"][-1])
 
 
 
 
113
  pprint.pprint("-------------------------------------")
114
  user_input = input("user >")
115
 
 
1
  import os
2
  import pprint
3
  import uuid
4
+ from dotenv import load_dotenv
5
 
6
  from langchain_mcp_adapters.client import MultiServerMCPClient
7
  from langchain_openai import ChatOpenAI
 
10
  from langchain_core.messages import HumanMessage
11
  from langgraph.checkpoint.memory import MemorySaver
12
 
13
+
14
  from pmcp.agents.executor import ExecutorAgent
15
  from pmcp.agents.trello_agent import TrelloAgent
16
  from pmcp.agents.github_agent import GithubAgent
17
  from pmcp.agents.planner import PlannerAgent
18
 
19
+ from pmcp.nodes.human_interrupt_node import HumanInterruptNode
20
+ from pmcp.nodes.human_resume_node import HumanResumeNode
21
+
22
  from pmcp.models.state import PlanningState
23
 
24
+ load_dotenv()
25
+
26
 
27
  async def call_llm(llm_with_tools: ChatOpenAI, state: MessagesState):
28
  response = llm_with_tools.invoke(state["messages"])
 
33
  mcp_client_trello = MultiServerMCPClient(
34
  {
35
  "trello": {
36
+ "command": "python",
37
+ "args": ["pmcp/mcp_server/trello_server/mcp_trello_main.py"],
38
+ "transport": "stdio",
39
  }
40
  }
41
  )
 
42
  mcp_client_github = MultiServerMCPClient(
43
  {
44
  "github": {
45
+ "command": "python",
46
+ "args": ["pmcp/mcp_server/github_server/mcp_github_main.py"],
47
+ "transport": "stdio",
48
  }
49
  }
50
  )
 
53
 
54
  trello_tools = await mcp_client_trello.get_tools()
55
  github_tools = await mcp_client_github.get_tools()
56
+
57
  tool_node = ToolNode(github_tools + trello_tools)
58
 
59
  llm = ChatOpenAI(
 
75
  )
76
  executor_agent = ExecutorAgent(llm=llm)
77
 
78
+ human_interrupt_node = HumanInterruptNode(
79
+ llm=llm,
80
+ )
81
+ human_resume_node = HumanResumeNode(llm=llm)
82
+
83
  graph = StateGraph(MessagesState)
84
  graph.add_node(planner_agent.agent.agent_name, planner_agent.acall_planner_agent)
85
  graph.add_node(trello_agent.agent.agent_name, trello_agent.acall_trello_agent)
86
  graph.add_node(github_agent.agent.agent_name, github_agent.acall_github_agent)
87
  graph.add_node(executor_agent.agent.agent_name, executor_agent.acall_executor_agent)
88
  graph.add_node("tool", tool_node)
89
+ graph.add_node("human_interrupt", human_interrupt_node.call_human_interrupt_agent)
90
  graph.set_entry_point(planner_agent.agent.agent_name)
91
 
92
  def should_continue(state: PlanningState):
93
  last_message = state.messages[-1]
94
  if last_message.tool_calls:
95
+ return "human_interrupt"
96
  return executor_agent.agent.agent_name
97
 
98
  def execute_agent(state: PlanningState):
99
  if state.current_step:
100
  return state.current_step.agent
101
+
102
  return END
103
 
104
  graph.add_conditional_edges(trello_agent.agent.agent_name, should_continue)
 
110
  graph.add_edge(planner_agent.agent.agent_name, executor_agent.agent.agent_name)
111
 
112
  app = graph.compile(checkpointer=memory)
113
+ app.get_graph(xray=True).draw_mermaid()
114
 
115
  user_input = input("user >")
116
+ config = {
117
+ "configurable": {"thread_id": f"{str(uuid.uuid4())}"},
118
+ "recursion_limit": 100,
119
+ }
120
 
121
+ is_message_command = False
122
  while user_input.lower() != "q":
123
+
124
+ if is_message_command:
125
+ app_input = human_resume_node.call_human_interrupt_agent(
126
+ user_input
127
+ )
128
+ is_message_command = False
129
+ else:
130
+ app_input = {
131
  "messages": [
132
  HumanMessage(content=user_input),
133
  ]
134
+ }
135
+ async for res in app.astream(
136
+ app_input,
137
  config=config,
138
  stream_mode="values",
139
  ):
140
+ if "messages" in res:
141
+ pprint.pprint(res["messages"][-1])
142
+ else:
143
+ pprint.pprint(res["__interrupt__"][0])
144
+ is_message_command = True
145
  pprint.pprint("-------------------------------------")
146
  user_input = input("user >")
147
 
mcpc_graph.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from langchain_mcp_adapters.client import MultiServerMCPClient
4
+ from langchain_openai import ChatOpenAI
5
+ from langgraph.prebuilt import ToolNode
6
+ from langgraph.graph import MessagesState, END, StateGraph
7
+ from langgraph.checkpoint.memory import MemorySaver
8
+
9
+
10
+ from pmcp.agents.executor import ExecutorAgent
11
+ from pmcp.agents.trello_agent import TrelloAgent
12
+ from pmcp.agents.github_agent import GithubAgent
13
+ from pmcp.agents.planner import PlannerAgent
14
+
15
+ from pmcp.nodes.human_interrupt_node import HumanInterruptNode
16
+ from pmcp.nodes.human_resume_node import HumanResumeNode
17
+
18
+ from pmcp.models.state import PlanningState
19
+
20
+
21
+ async def setup_graph(github_token: str, trello_api:str, trello_token:str):
22
+
23
+ mcp_client_github = MultiServerMCPClient(
24
+ {
25
+ "github": {
26
+ "command": "python",
27
+ "args": ["pmcp/mcp_server/github_server/mcp_github_main.py", "--api-key", github_token],
28
+ "transport": "stdio",
29
+ }
30
+ }
31
+ )
32
+
33
+
34
+ mcp_client_trello = MultiServerMCPClient(
35
+ {
36
+ "trello": {
37
+ "command": "python",
38
+ "args": ["pmcp/mcp_server/trello_server/mcp_trello_main.py", "--api-key", trello_api, "--token", trello_token],
39
+ "transport": "stdio",
40
+ }
41
+ }
42
+ )
43
+
44
+
45
+
46
+ memory = MemorySaver()
47
+
48
+ trello_tools = await mcp_client_trello.get_tools()
49
+ github_tools = await mcp_client_github.get_tools()
50
+
51
+ tool_node = ToolNode(github_tools + trello_tools)
52
+
53
+ llm = ChatOpenAI(
54
+ model="Qwen/Qwen2.5-32B-Instruct",
55
+ temperature=0.0,
56
+ api_key=os.getenv("NEBIUS_API_KEY"),
57
+ base_url="https://api.studio.nebius.com/v1/",
58
+ )
59
+
60
+
61
+ trello_agent = TrelloAgent(
62
+ tools=trello_tools,
63
+ llm=llm,
64
+ )
65
+
66
+ github_agent = GithubAgent(llm=llm, tools=github_tools)
67
+
68
+ planner_agent = PlannerAgent(
69
+ llm=llm,
70
+ )
71
+ executor_agent = ExecutorAgent(llm=llm)
72
+
73
+ human_interrupt_node = HumanInterruptNode(
74
+ llm=llm,
75
+ )
76
+ human_resume_node = HumanResumeNode(llm=llm)
77
+
78
+ graph = StateGraph(MessagesState)
79
+ graph.add_node(planner_agent.agent.agent_name, planner_agent.acall_planner_agent)
80
+ graph.add_node(trello_agent.agent.agent_name, trello_agent.acall_trello_agent)
81
+ graph.add_node(github_agent.agent.agent_name, github_agent.acall_github_agent)
82
+ graph.add_node(executor_agent.agent.agent_name, executor_agent.acall_executor_agent)
83
+ graph.add_node("tool", tool_node)
84
+ graph.add_node("human_interrupt", human_interrupt_node.call_human_interrupt_agent)
85
+ graph.set_entry_point(planner_agent.agent.agent_name)
86
+
87
+ def should_continue(state: PlanningState):
88
+ last_message = state.messages[-1]
89
+ if last_message.tool_calls:
90
+ return "human_interrupt"
91
+ return executor_agent.agent.agent_name
92
+
93
+ def execute_agent(state: PlanningState):
94
+ if state.current_step:
95
+ return state.current_step.agent
96
+
97
+ return END
98
+
99
+ graph.add_conditional_edges(trello_agent.agent.agent_name, should_continue)
100
+ graph.add_conditional_edges(github_agent.agent.agent_name, should_continue)
101
+ graph.add_conditional_edges(executor_agent.agent.agent_name, execute_agent)
102
+
103
+ graph.add_edge("tool", trello_agent.agent.agent_name)
104
+ graph.add_edge("tool", github_agent.agent.agent_name)
105
+ graph.add_edge(planner_agent.agent.agent_name, executor_agent.agent.agent_name)
106
+
107
+ app = graph.compile(checkpointer=memory)
108
+ app.get_graph(xray=True).draw_mermaid()
109
+
110
+
111
+
112
+ return app, human_resume_node
pmcp/agents/executor.py CHANGED
@@ -31,18 +31,17 @@ class ExecutorAgent:
31
  }
32
 
33
  async def acall_executor_agent(self, state: PlanningState):
34
- current_step = state.plan_step
35
-
36
- plan_step = None
37
- if len(state.plan.steps) > current_step:
38
- plan_step = state.plan.steps[current_step]
39
-
 
 
 
40
  return {
41
- "plan_step": current_step + 1,
42
- "messages": [
43
- HumanMessage(
44
- content=f"The {plan_step.agent} agent should perform the following action:\n{plan_step.description}"
45
- )
46
- ],
47
- "current_step": plan_step,
48
  }
 
31
  }
32
 
33
  async def acall_executor_agent(self, state: PlanningState):
34
+ plan_step_index = state.plan_step
35
+ current_step = None
36
+ messages = []
37
+ if len(state.plan.steps) > plan_step_index:
38
+ current_step = state.plan.steps[plan_step_index]
39
+ messages = [HumanMessage(
40
+ content=f"The {current_step.agent} agent should perform the following action:\n{current_step.description}"
41
+ )]
42
+
43
  return {
44
+ "plan_step": plan_step_index + 1,
45
+ "messages": messages,
46
+ "current_step": current_step,
 
 
 
 
47
  }
pmcp/agents/github_agent.py CHANGED
@@ -9,8 +9,7 @@ from pmcp.models.state import PlanningState
9
 
10
  SYSTEM_PROMPT = """
11
  You are an assistant that can manage Trello boards and projects.
12
- 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.
13
- If the user's answer is negative, then you have to abort everything and end the conversation.
14
  """
15
 
16
 
 
9
 
10
  SYSTEM_PROMPT = """
11
  You are an assistant that can manage Trello boards and projects.
12
+ You will be given a set of tools to work with.
 
13
  """
14
 
15
 
pmcp/agents/planner.py CHANGED
@@ -10,8 +10,8 @@ from pmcp.models.state import PlanningState
10
 
11
  SYSTEM_PROMPT = """
12
  You are a Planner Agent responsible for breaking down high-level project goals into clear, actionable steps. You do not execute tasks yourself — instead, you delegate them to two specialized agents:
13
- - Trello Agent – Handles all operations related to Trello (boards (only read), lists, cards, assignments, due dates, etc.).
14
- - GitHub Agent – Handles all operations related to GitHub (issues, can see in textual form the repository).
15
  Your job is to:
16
  - Analyze the user’s request or project goal.
17
  - Decompose it into a step-by-step plan with granular, unambiguous tasks.
@@ -42,11 +42,11 @@ class PlannerAgent:
42
  messages=[SystemMessage(content=self.agent.system_prompt)] + state.messages,
43
  clazz=Plan,
44
  )
45
- return {"plan": response}
46
 
47
  async def acall_planner_agent(self, state: PlanningState):
48
  response = await self.agent.acall_agent_structured(
49
  messages=[SystemMessage(content=self.agent.system_prompt)] + state.messages,
50
  clazz=Plan,
51
  )
52
- return {"plan": response}
 
10
 
11
  SYSTEM_PROMPT = """
12
  You are a Planner Agent responsible for breaking down high-level project goals into clear, actionable steps. You do not execute tasks yourself — instead, you delegate them to two specialized agents:
13
+ - TRELLO_AGENT – Handles all operations related to Trello (boards (only read), lists, cards, assignments, due dates, etc.).
14
+ - GITHUB_AGENT – Handles all operations related to GitHub (issues, can see in textual form the repository).
15
  Your job is to:
16
  - Analyze the user’s request or project goal.
17
  - Decompose it into a step-by-step plan with granular, unambiguous tasks.
 
42
  messages=[SystemMessage(content=self.agent.system_prompt)] + state.messages,
43
  clazz=Plan,
44
  )
45
+ return {"plan": response, "plan_step": 0, "current_step": None}
46
 
47
  async def acall_planner_agent(self, state: PlanningState):
48
  response = await self.agent.acall_agent_structured(
49
  messages=[SystemMessage(content=self.agent.system_prompt)] + state.messages,
50
  clazz=Plan,
51
  )
52
+ return {"plan": response, "plan_step": 0, "current_step": None}
pmcp/agents/trello_agent.py CHANGED
@@ -9,8 +9,7 @@ from pmcp.models.state import PlanningState
9
 
10
  SYSTEM_PROMPT = """
11
  You are an assistant that can manage Trello boards and projects.
12
- 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.
13
- If the user's answer is negative, then you have to abort everything and end the conversation.
14
  """
15
 
16
 
 
9
 
10
  SYSTEM_PROMPT = """
11
  You are an assistant that can manage Trello boards and projects.
12
+ You will be given a set of tools to work with.
 
13
  """
14
 
15
 
pmcp/mcp_server/__init__.py ADDED
File without changes
pmcp/mcp_server/github_server/__init__.py ADDED
File without changes
pmcp/mcp_server/github_server/connection_test.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # client_with_stdio.py
2
+ import asyncio
3
+ from fastmcp import Client
4
+ from fastmcp.client.transports import PythonStdioTransport
5
+
6
+ async def main():
7
+
8
+ transport = PythonStdioTransport(
9
+ python_cmd="python",
10
+ script_path="mcp_github_main.py",
11
+ args=["--api-key", "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"]
12
+ )
13
+
14
+
15
+ async with Client(transport) as client:
16
+ tools = await client.list_tools()
17
+ print("Available tools:", tools)
18
+
19
+ result = await client.call_tool(
20
+ "get_issues",
21
+ {"owner": "jlowin", "repo": "fastmcp"}
22
+ )
23
+ print("Result:", result)
24
+
25
+ if __name__ == "__main__":
26
+ asyncio.run(main())
27
+
28
+
pmcp/mcp_server/github_server/github.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pmcp.mcp_server.github_server.utils.github_api import GithubClient
2
+
3
+
4
+
5
+ github_client = None
6
+
7
+ def initialize_github_client(api_key: str = None) -> None:
8
+ try:
9
+ if not api_key:
10
+ raise ValueError(
11
+ "api_key required"
12
+ )
13
+ global github_client
14
+ github_client = GithubClient(api_key=api_key)
15
+ except Exception as e:
16
+ raise
17
+
18
+
19
+ # Add a prompt for common Github operations
20
+ def github_help() -> str:
21
+ """Provides help information about available Github operations."""
22
+ return """
23
+ Available Github Operations:
24
+ 1. get issues
25
+ 2. create issue
26
+ 3. comment issue
27
+ 4. close issue
28
+ 5. get pull requests
29
+ 6. create pull request
30
+ 7. get repo stats
31
+ 8. list branches
32
+ 9. get recent commits
33
+ 10. get file contents
34
+ """
pmcp/mcp_server/github_server/mcp_github_main.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ import argparse
4
+ from mcp.server.fastmcp import FastMCP
5
+
6
+ from pmcp.mcp_server.github_server.github import initialize_github_client
7
+
8
+
9
+ # Configure logging
10
+ logging.basicConfig(
11
+ level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
12
+ )
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def parse_args():
17
+ parser = argparse.ArgumentParser(description="Avvia il Github MCP Server")
18
+ parser.add_argument("--api-key", type=str, required=True, help="API key per GitHub")
19
+ return parser.parse_args()
20
+
21
+ def main():
22
+ args = parse_args()
23
+
24
+ initialize_github_client(args.api_key)
25
+
26
+ mcp = FastMCP("Github MCP Server")
27
+ #import here because the client is not initialized yet
28
+ from pmcp.mcp_server.github_server.tools.tools import register_tools
29
+ register_tools(mcp)
30
+
31
+ try:
32
+ logger.info("Starting Github MCP Server in Stdio...")
33
+ mcp.run()
34
+ logger.info("Github MCP Server started successfully")
35
+ except KeyboardInterrupt:
36
+ logger.info("Shutting down server...")
37
+ except Exception as e:
38
+ logger.error(f"Server error: {str(e)}")
39
+ raise
40
+
41
+ if __name__ == "__main__":
42
+ main()
pmcp/mcp_server/github_server/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_server/services/__init__.py ADDED
File without changes
pmcp/mcp_server/github_server/services/branches.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pmcp.mcp_server.github_server.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_server/services/contents.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pmcp.mcp_server.github_server.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_server/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_server.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_server/services/pull_requests.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pmcp.mcp_server.github_server.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_server/services/repo.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pmcp.mcp_server.github_server.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_server/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_server.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_server/tools/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
pmcp/mcp_server/github_server/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_server.services.branches import BranchService
7
+ from pmcp.mcp_server.github_server.github import github_client
8
+
9
+ service = BranchService(github_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_server/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_server.services.contents import ContentService
8
+ from pmcp.mcp_server.github_server.github import github_client
9
+
10
+ service = ContentService(github_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_server/tools/issues.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_server.services.issues import IssueService
8
+ from pmcp.mcp_server.github_server.github import github_client
9
+
10
+
11
+ service = IssueService(github_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 title and number 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": [{"issue_number": 123, "issue_title": "Issue Title"}, ...]}
28
+ """
29
+ try:
30
+ issues = await service.get_issues(owner, repo)
31
+ # Extract both title and number, skipping pull requests
32
+ issue_info = [{"issue_number": i["number"], "issue_title": i["title"]} for i in issues if "pull_request" not in i]
33
+ return {"issues": issue_info}
34
+ except Exception as exc:
35
+ error_msg = f"Failed to get issues: {str(exc)}"
36
+ await ctx.error(str(exc))
37
+ raise
38
+
39
+
40
+ # ───────────────────────────────────────────────────────────────────────── #
41
+ # WRITE
42
+
43
+ async def create_issue(
44
+ ctx: Context, owner: str, repo: str, title: str, body: str | None = None
45
+ ) -> Dict[str, Any]:
46
+ """
47
+ Opens a new issue.
48
+
49
+ Args:
50
+ ctx: FastMCP request context (handles errors).
51
+ owner (str): Repository owner.
52
+ repo (str): Repository name.
53
+ title (str): Issue title.
54
+ body (str): Body of the issue.
55
+
56
+ Returns:
57
+ {"url": "...", "number": 123}
58
+ """
59
+ try:
60
+ result = await service.create_issue(owner, repo, title, body)
61
+ return {"url": result["html_url"], "number": result["number"]}
62
+ except Exception as exc:
63
+ error_msg = f"Failed to create issue {str(exc)}"
64
+ await ctx.error(str(exc))
65
+ raise
66
+
67
+
68
+ async def comment_issue(
69
+ ctx: Context, owner: str, repo: str, issue_number: int, body: str
70
+ ) -> Dict[str, str]:
71
+ """
72
+ Adds a comment on an existing issue.
73
+
74
+ Args:
75
+ ctx: FastMCP request context (handles errors).
76
+ owner (str): Repository owner.
77
+ repo (str): Repository name.
78
+ issue_number (int): Issue number.
79
+ body (str): Body of the issue.
80
+ """
81
+ try:
82
+ result = await service.comment_issue(owner, repo, issue_number, body)
83
+ return {"url": result["html_url"]}
84
+ except Exception as exc:
85
+ error_msg = f"Failed to add the comment {str(exc)}"
86
+ await ctx.error(str(exc))
87
+ raise
88
+
89
+
90
+ async def close_issue(
91
+ ctx: Context, owner: str, repo: str, issue_number: int
92
+ ) -> Dict[str, str]:
93
+ """
94
+ Closes an issue.
95
+
96
+ Args:
97
+ ctx: FastMCP request context (handles errors).
98
+ owner (str): Repository owner.
99
+ repo (str): Repository name.
100
+ issue_number (int): Issue number.
101
+
102
+ Returns:
103
+ Dict
104
+ """
105
+ try:
106
+ await service.close_issue(owner, repo, issue_number)
107
+ return {"status": "closed"}
108
+ except Exception as exc:
109
+ error_msg = f"Failed to close the issue number {issue_number} in repo {repo}"
110
+ await ctx.error(str(exc))
111
+ raise
pmcp/mcp_server/github_server/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_server.services.pull_requests import PullRequestService
7
+ from pmcp.mcp_server.github_server.github import github_client
8
+
9
+ service = PullRequestService(github_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_server/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_server.services.repo import RepoService
6
+ from pmcp.mcp_server.github_server.github import github_client
7
+
8
+
9
+ service = RepoService(github_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_server/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_server.services.repo_to_text import RepoToTextService
6
+ from pmcp.mcp_server.github_server.github import github_client
7
+
8
+ from mcp.server.fastmcp import Context
9
+
10
+
11
+ service = RepoToTextService(github_client)
12
+
13
+ async def get_repo_to_text(ctx: Context, repo: str) -> str:
14
+ """Retrieves the repo structure and the repo content 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_server/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_server.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_server/utils/__init__.py ADDED
File without changes
pmcp/mcp_server/github_server/utils/github_api.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import httpx
3
+ from pmcp.mcp_server.github_server.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_server/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_server/__init__.py ADDED
File without changes
pmcp/mcp_server/trello_server/connection_test.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastmcp import Client
2
+
3
+ async def main():
4
+ # Connect via stdio to a local script
5
+ async with Client("mcp_trello_main.py") as client:
6
+ tools = await client.list_tools()
7
+ print(f"Available tools: {tools}")
8
+ result = await client.call_tool("get_boards")
9
+ print(f"Result: {result}")
10
+
11
+
12
+ if __name__ == "__main__":
13
+ import asyncio
14
+ asyncio.run(main())
pmcp/mcp_server/trello_server/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_server/mcp_trello_main.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import argparse
3
+ from mcp.server.fastmcp import FastMCP
4
+
5
+ from pmcp.mcp_server.trello_server.trello import initialize_trello_client
6
+
7
+ # Configure logging
8
+ logging.basicConfig(
9
+ level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
10
+ )
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def parse_args():
15
+ parser = argparse.ArgumentParser(description="Avvia il Trello MCP Server")
16
+ parser.add_argument("--api-key", type=str, required=True, help="API key per Trello")
17
+ parser.add_argument("--token", type=str, required=True, help="Token per Trello")
18
+ return parser.parse_args()
19
+
20
+
21
+ def main():
22
+ args = parse_args()
23
+
24
+ initialize_trello_client(args.api_key, args.token)
25
+
26
+ mcp = FastMCP("Trello MCP Server")
27
+ # import here because the client is not initialized yet
28
+ from pmcp.mcp_server.trello_server.tools.tools import register_tools
29
+
30
+ register_tools(mcp)
31
+
32
+ try:
33
+ logger.info("Starting Trello MCP Server in Stdio...")
34
+ mcp.run()
35
+ logger.info("Trello MCP Server started successfully")
36
+ except KeyboardInterrupt:
37
+ logger.info("Shutting down server...")
38
+ except Exception as e:
39
+ logger.error(f"Server error: {str(e)}")
40
+ raise
41
+
42
+
43
+ if __name__ == "__main__":
44
+ main()
pmcp/mcp_server/trello_server/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_server/services/__init__.py ADDED
File without changes
pmcp/mcp_server/trello_server/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_server.models import TrelloBoard, TrelloLabel
8
+ from pmcp.mcp_server.trello_server.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_server/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_server.models import TrelloCard
8
+ from pmcp.mcp_server.trello_server.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_server/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_server.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_server/services/list.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+
3
+ from pmcp.mcp_server.trello_server.models import TrelloList
4
+ from pmcp.mcp_server.trello_server.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_server/tools/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
pmcp/mcp_server/trello_server/tools/board.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_server.models import TrelloBoard, TrelloLabel
11
+ from pmcp.mcp_server.trello_server.services.board import BoardService
12
+ from pmcp.mcp_server.trello_server.trello import trello_client
13
+
14
+
15
+
16
+ service = BoardService(trello_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
+ Use this method when the user specify only the Board Name and you have to retrieve the Board ID.
40
+
41
+ Returns:
42
+ List[TrelloBoard]: A list of board objects.
43
+ """
44
+ try:
45
+ result = await service.get_boards()
46
+ return result
47
+ except Exception as e:
48
+ error_msg = f"Failed to get boards: {str(e)}"
49
+ await ctx.error(error_msg)
50
+ raise
51
+
52
+
53
+ async def get_board_labels(ctx: Context, board_id: str) -> List[TrelloLabel]:
54
+ """Retrieves all labels for a specific board.
55
+
56
+ Args:
57
+ board_id (str): The ID of the board whose labels to retrieve.
58
+
59
+ Returns:
60
+ List[TrelloLabel]: A list of label objects for the board.
61
+ """
62
+ try:
63
+ result = await service.get_board_labels(board_id)
64
+ return result
65
+ except Exception as e:
66
+ error_msg = f"Failed to get board labels: {str(e)}"
67
+ await ctx.error(error_msg)
68
+ raise
pmcp/mcp_server/trello_server/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_server.models import TrelloCard
11
+ from pmcp.mcp_server.trello_server.services.card import CardService
12
+ from pmcp.mcp_server.trello_server.trello import trello_client
13
+ from pmcp.mcp_server.trello_server.dtos.update_card import UpdateCardPayload
14
+
15
+
16
+ service = CardService(trello_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_server/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_server.services.checklist import ChecklistService
9
+ from pmcp.mcp_server.trello_server.trello import trello_client
10
+
11
+ service = ChecklistService(trello_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)