Spaces:
Runtime error
Runtime error
Merge pull request #4
Browse filesGradio app, planner with interrupt and CI
This view is limited to 50 files because it contains too many changes. See raw diff
- .env.example +4 -0
- .github/workflows/sync_to_hf.yml +20 -0
- .gitignore +1 -0
- README.md +16 -2
- app.py +72 -190
- app_new.py +189 -0
- main.py +44 -12
- mcpc_graph.py +112 -0
- pmcp/agents/executor.py +12 -13
- pmcp/agents/github_agent.py +1 -2
- pmcp/agents/planner.py +4 -4
- pmcp/agents/trello_agent.py +1 -2
- pmcp/mcp_server/__init__.py +0 -0
- pmcp/mcp_server/github_server/__init__.py +0 -0
- pmcp/mcp_server/github_server/connection_test.py +28 -0
- pmcp/mcp_server/github_server/github.py +34 -0
- pmcp/mcp_server/github_server/mcp_github_main.py +42 -0
- pmcp/mcp_server/github_server/models.py +20 -0
- pmcp/mcp_server/github_server/services/__init__.py +0 -0
- pmcp/mcp_server/github_server/services/branches.py +21 -0
- pmcp/mcp_server/github_server/services/contents.py +44 -0
- pmcp/mcp_server/github_server/services/issues.py +86 -0
- pmcp/mcp_server/github_server/services/pull_requests.py +58 -0
- pmcp/mcp_server/github_server/services/repo.py +21 -0
- pmcp/mcp_server/github_server/services/repo_to_text.py +26 -0
- pmcp/mcp_server/github_server/tools/__init__.py +1 -0
- pmcp/mcp_server/github_server/tools/branches.py +31 -0
- pmcp/mcp_server/github_server/tools/contents.py +62 -0
- pmcp/mcp_server/github_server/tools/issues.py +111 -0
- pmcp/mcp_server/github_server/tools/pull_requests.py +68 -0
- pmcp/mcp_server/github_server/tools/repo.py +35 -0
- pmcp/mcp_server/github_server/tools/repo_to_text.py +28 -0
- pmcp/mcp_server/github_server/tools/tools.py +31 -0
- pmcp/mcp_server/github_server/utils/__init__.py +0 -0
- pmcp/mcp_server/github_server/utils/github_api.py +67 -0
- pmcp/mcp_server/github_server/utils/repo_to_text_utils.py +83 -0
- pmcp/mcp_server/trello_server/__init__.py +0 -0
- pmcp/mcp_server/trello_server/connection_test.py +14 -0
- pmcp/mcp_server/trello_server/dtos/update_card.py +22 -0
- pmcp/mcp_server/trello_server/mcp_trello_main.py +44 -0
- pmcp/mcp_server/trello_server/models.py +47 -0
- pmcp/mcp_server/trello_server/services/__init__.py +0 -0
- pmcp/mcp_server/trello_server/services/board.py +53 -0
- pmcp/mcp_server/trello_server/services/card.py +84 -0
- pmcp/mcp_server/trello_server/services/checklist.py +162 -0
- pmcp/mcp_server/trello_server/services/list.py +82 -0
- pmcp/mcp_server/trello_server/tools/__init__.py +1 -0
- pmcp/mcp_server/trello_server/tools/board.py +68 -0
- pmcp/mcp_server/trello_server/tools/card.py +115 -0
- 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 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 195 |
gr.Markdown(
|
| 196 |
"""
|
| 197 |
-
#
|
| 198 |
-
Manage your
|
| 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 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 30 |
-
"
|
|
|
|
| 31 |
}
|
| 32 |
}
|
| 33 |
)
|
| 34 |
-
|
| 35 |
mcp_client_github = MultiServerMCPClient(
|
| 36 |
{
|
| 37 |
"github": {
|
| 38 |
-
"
|
| 39 |
-
"
|
|
|
|
| 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 "
|
| 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).
|
| 98 |
|
| 99 |
user_input = input("user >")
|
| 100 |
-
config = {
|
|
|
|
|
|
|
|
|
|
| 101 |
|
|
|
|
| 102 |
while user_input.lower() != "q":
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
"messages": [
|
| 106 |
HumanMessage(content=user_input),
|
| 107 |
]
|
| 108 |
-
}
|
|
|
|
|
|
|
| 109 |
config=config,
|
| 110 |
stream_mode="values",
|
| 111 |
):
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
if len(state.plan.steps) >
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
| 40 |
return {
|
| 41 |
-
"plan_step":
|
| 42 |
-
"messages":
|
| 43 |
-
|
| 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.
|
| 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 |
-
-
|
| 14 |
-
-
|
| 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.
|
| 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)
|