backlog-mcp 1.0.3__py3-none-any.whl → 1.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
app/main.py CHANGED
@@ -11,6 +11,7 @@ app = FastMCP(
11
11
  # Add tool for Backlog MCP
12
12
  app.add_tool(get_issue_details)
13
13
  app.add_tool(get_user_issue_list)
14
+ app.add_tool(update_issue_description)
14
15
 
15
16
 
16
17
  def main() -> int:
app/tools/__init__.py CHANGED
@@ -1,2 +1,3 @@
1
1
  from .get_issue_details import *
2
2
  from .get_user_issue_list import *
3
+ from .update_issue_description import *
@@ -4,13 +4,15 @@ from app.utils.ultils import get_issue_detail_handler
4
4
 
5
5
  async def get_issue_details(
6
6
  issue_key: str,
7
- timezone: str = "UTC"
7
+ include_comments: bool,
8
+ timezone: str = "UTC",
8
9
  ):
9
10
  """
10
11
  Get details of a Backlog issue by its key.
11
12
 
12
13
  Args:
13
14
  issue_key (str): The key of the Backlog issue to retrieve.
15
+ include_comments (bool): Whether to include comments in the response.
14
16
  timezone (str, optional): The timezone to format datetime fields. Defaults to "UTC".
15
17
  """
16
18
  try:
@@ -22,6 +24,7 @@ async def get_issue_details(
22
24
  api_key=ctx.api_key,
23
25
  issue_key=issue_key,
24
26
  timezone=timezone,
27
+ include_comments=include_comments,
25
28
  )
26
29
  return result
27
30
  except Exception as e:
@@ -0,0 +1,31 @@
1
+ from app.utils.di import create_backlog_context
2
+ from app.utils.ultils import update_issue_description_handler
3
+
4
+
5
+ async def update_issue_description(
6
+ issue_key: str,
7
+ description: str,
8
+ ):
9
+ """
10
+ Update the description of a Backlog issue.
11
+
12
+ Args:
13
+ issue_key (str): The key or ID of the Backlog issue to update.
14
+ description (str): The new description content for the issue.
15
+ """
16
+ try:
17
+ if not issue_key:
18
+ raise ValueError("Please provide an issue key.")
19
+ if not description:
20
+ raise ValueError("Please provide a description.")
21
+
22
+ ctx = create_backlog_context()
23
+ result = await update_issue_description_handler(
24
+ backlog_domain=ctx.backlog_domain,
25
+ api_key=ctx.api_key,
26
+ issue_key=issue_key,
27
+ description=description,
28
+ )
29
+ return result
30
+ except Exception as e:
31
+ raise e
app/utils/ultils.py CHANGED
@@ -46,28 +46,29 @@ def time_in_range(time: str, start_range: str, end_range: str):
46
46
  return start_range_time <= time_to_be_compared <= end_range_time
47
47
 
48
48
 
49
- def process_issue_detail(issue_detail, timezone, issue_key):
49
+ def process_issue_detail(issue_detail, timezone, issue_key, include_comments: bool = True):
50
50
  processed_issue = {
51
51
  "issue_key": issue_key,
52
52
  "summary": issue_detail["summary"],
53
53
  "description": issue_detail["description"]
54
54
  }
55
55
 
56
- comments = issue_detail.get("comments", [])
57
- if comments:
58
- # Sort comments by created_at (created field)
59
- sorted_comments = sorted(comments, key=lambda c: convert_to_timezone(timezone, c["created"]))
60
- # Create list of {content, created_by} where created_by is just the name
61
- processed_comments = [
62
- {
63
- "content": c["content"],
64
- "created_by": c["createdUser"]["name"] if c.get("createdUser") else None
65
- }
66
- for c in sorted_comments if c.get("content")
67
- ]
68
- # Filter out None created_by if any (though should not happen)
69
- processed_comments = [c for c in processed_comments if c["created_by"]]
70
- processed_issue["comments"] = processed_comments
56
+ if include_comments:
57
+ comments = issue_detail.get("comments", [])
58
+ if comments:
59
+ # Sort comments by created_at (created field)
60
+ sorted_comments = sorted(comments, key=lambda c: convert_to_timezone(timezone, c["created"]))
61
+ # Create list of {content, created_by} where created_by is just the name
62
+ processed_comments = [
63
+ {
64
+ "content": c["content"],
65
+ "created_by": c["createdUser"]["name"] if c.get("createdUser") else None
66
+ }
67
+ for c in sorted_comments if c.get("content")
68
+ ]
69
+ # Filter out None created_by if any (though should not happen)
70
+ processed_comments = [c for c in processed_comments if c["created_by"]]
71
+ processed_issue["comments"] = processed_comments
71
72
 
72
73
  return processed_issue
73
74
 
@@ -77,6 +78,7 @@ async def get_issue_detail_handler(
77
78
  api_key: str,
78
79
  issue_key: str,
79
80
  timezone: str,
81
+ include_comments: bool = True,
80
82
  ):
81
83
  issue_comments_url = f"{backlog_domain}api/v2/issues/{issue_key}/comments"
82
84
  issue_detail_url = f"{backlog_domain}api/v2/issues/{issue_key}"
@@ -85,31 +87,38 @@ async def get_issue_detail_handler(
85
87
  async with httpx.AsyncClient() as client:
86
88
  try:
87
89
  issue_detail_response = client.get(issue_detail_url, params=params)
88
- comments_response = client.get(issue_comments_url, params=params)
89
-
90
- results = await asyncio.gather(issue_detail_response, comments_response)
91
-
92
- issue_detail = results[0].json()
93
- issue_comment = results[1].json()
90
+
91
+ if include_comments:
92
+ comments_response = client.get(issue_comments_url, params=params)
93
+ results = await asyncio.gather(issue_detail_response, comments_response)
94
+ issue_detail_result = results[0]
95
+ comments_result = results[1]
96
+ issue_detail = issue_detail_result.json()
97
+ issue_comment = comments_result.json()
98
+ else:
99
+ issue_detail_result = await issue_detail_response
100
+ issue_detail = issue_detail_result.json()
101
+ issue_comment = []
94
102
 
95
- if not results[0].is_success:
103
+ if not issue_detail_result.is_success:
96
104
  error_code = issue_detail["errors"][0]["code"]
97
105
  return {
98
106
  "error_msg": BacklogApiError.get_description_by_code(error_code),
99
107
  }
100
108
 
101
- if not results[1].is_success:
109
+ if include_comments and not comments_result.is_success:
102
110
  error_code = issue_comment["errors"][0]["code"]
103
111
  return {
104
112
  "error_msg": BacklogApiError.get_description_by_code(error_code),
105
113
  }
106
114
 
107
- comments_in_time_range = []
108
- for comment in issue_comment:
109
- comments_in_time_range.append(comment)
115
+ if include_comments:
116
+ comments_in_time_range = []
117
+ for comment in issue_comment:
118
+ comments_in_time_range.append(comment)
119
+ issue_detail.update({"comments": comments_in_time_range})
110
120
 
111
- issue_detail.update({"comments": comments_in_time_range})
112
- processed_detail = process_issue_detail(issue_detail, timezone, issue_key)
121
+ processed_detail = process_issue_detail(issue_detail, timezone, issue_key, include_comments)
113
122
  return processed_detail
114
123
 
115
124
  except Exception as e:
@@ -291,3 +300,56 @@ async def get_current_user(backlog_domain: str, api_key: str) -> dict:
291
300
  raise ValueError(f"Failed to get current user: {e}") from e
292
301
  except Exception as e:
293
302
  raise ValueError(f"Unexpected error while getting current user: {e}") from e
303
+
304
+
305
+ async def update_issue_description_handler(
306
+ backlog_domain: str,
307
+ api_key: str,
308
+ issue_key: str,
309
+ description: str,
310
+ ):
311
+ """Update issue description via Backlog API.
312
+
313
+ Args:
314
+ backlog_domain (str): Backlog domain URL
315
+ api_key (str): API key for authentication
316
+ issue_key (str): Issue key or ID
317
+ description (str): New description content
318
+
319
+ Returns:
320
+ dict: Updated issue information
321
+ """
322
+ try:
323
+ url = f"{backlog_domain}api/v2/issues/{issue_key}"
324
+ params = {"apiKey": api_key}
325
+ data = {"description": description}
326
+
327
+ async with httpx.AsyncClient() as client:
328
+ response = await client.patch(
329
+ url,
330
+ params=params,
331
+ data=data,
332
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
333
+ timeout=10.0
334
+ )
335
+ response.raise_for_status()
336
+ result = response.json()
337
+
338
+ result = {
339
+ "issueKey": result["issueKey"],
340
+ "summary": result["summary"],
341
+ "description": result["description"]
342
+ }
343
+
344
+ return result
345
+ except httpx.HTTPStatusError as e:
346
+ try:
347
+ body = e.response.json()
348
+ error_code = None
349
+ if "errors" in body and body["errors"]:
350
+ error_code = body["errors"][0].get("code")
351
+ raise ValueError(f"API error: {BacklogApiError.get_description_by_code(error_code)}") from e
352
+ except Exception:
353
+ raise ValueError("Failed to parse error response") from e
354
+ except Exception as e:
355
+ raise ValueError(f"Request failed: {str(e)}") from e
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backlog-mcp
3
- Version: 1.0.3
3
+ Version: 1.0.5
4
4
  Summary: A Model Context Protocol (MCP) server for Backlog project management integration
5
5
  Author-email: BaoNguyen <baonguyen@teqnological.asia>
6
6
  Requires-Python: <3.14,>=3.13
@@ -1,19 +1,20 @@
1
1
  app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  app/logging_config.py,sha256=1tSV1HXExfIi_mxOpugvGL7ywd43SMmsKsjARWXTLnU,999
3
- app/main.py,sha256=IZ-oDlynIZN-knCZqbUlnTMZ1YDJ-mHpUdAgv-Q-Lp4,488
3
+ app/main.py,sha256=iCyuJoQlueVQjZWmAlmb1WuMBDO2arTHSjkjLoXCZqQ,527
4
4
  app/server_settings.py,sha256=mI-G1DKJ2IBg9dKuW4Da5MDhFLeYSQwLOGGNEb_kQwM,434
5
5
  app/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  app/constants/constants.py,sha256=DV8HJnvCceHqUmA5csk9CcmcX9Zr0upl56qXVa9tF88,1664
7
7
  app/core/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
8
8
  app/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  app/models/models.py,sha256=y3JhziNew3_2bM4Lw3aVjWcTvhmaEbzz0AaCHsdAkbk,4169
10
- app/tools/__init__.py,sha256=LxxyJiqccPQCNjei_YvtNJgEMs4U7IzKEsAaNKToaKg,68
11
- app/tools/get_issue_details.py,sha256=GMuC6lf33z0YTREq8VQ9oWwRe59XyHCnfpja0BMXjCw,819
10
+ app/tools/__init__.py,sha256=v37to4C68ZJswTDeECaZZT3IPbRIe876CzJ_0XmZYCw,108
11
+ app/tools/get_issue_details.py,sha256=efixu906oloeIZdf-geTlLxt9nLe-tPDqUlDd9HL6-g,973
12
12
  app/tools/get_user_issue_list.py,sha256=soNX0OfvfT1hIyzeDMPaHpAjHeGqavtZdTl0CemCpyo,865
13
+ app/tools/update_issue_description.py,sha256=yS0yAETvDWeQYyeT-h7gTTihuszxyTJtizEyT6p4SX8,916
13
14
  app/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
15
  app/utils/di.py,sha256=5MmL6efLAX3uzp5WSMi4E_BDCgapzlr_OERZoN5NGnM,408
15
- app/utils/ultils.py,sha256=LuTHKOZGhjU9LHGLNNzQJeck-UJYu49L4MK9ZfUoRtg,10798
16
- backlog_mcp-1.0.3.dist-info/METADATA,sha256=tLveO9390Py9GBN-YLzcvIEQYfW4EjR_8Cs5L45JaMA,3883
17
- backlog_mcp-1.0.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
18
- backlog_mcp-1.0.3.dist-info/entry_points.txt,sha256=h5JeHYUp_uoaya4FO3QelcsgYOu-VRmh5qWRhPrwjtk,46
19
- backlog_mcp-1.0.3.dist-info/RECORD,,
16
+ app/utils/ultils.py,sha256=_JGcpS-EMgp4gw_A3c4dTdPl5UEvVCM1x61n5VrQt70,13099
17
+ backlog_mcp-1.0.5.dist-info/METADATA,sha256=t774bjIDzAc1nbJBF6ahSq3j7rb4oSFFstufxSdzAQg,3883
18
+ backlog_mcp-1.0.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
19
+ backlog_mcp-1.0.5.dist-info/entry_points.txt,sha256=h5JeHYUp_uoaya4FO3QelcsgYOu-VRmh5qWRhPrwjtk,46
20
+ backlog_mcp-1.0.5.dist-info/RECORD,,