fenix-mcp 1.5.0__py3-none-any.whl → 1.6.0__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.
fenix_mcp/__init__.py CHANGED
@@ -8,4 +8,4 @@ Fênix Cloud MCP Server (Python edition).
8
8
  __all__ = ["__version__"]
9
9
 
10
10
 
11
- __version__ = "1.5.0"
11
+ __version__ = "1.6.0"
@@ -39,18 +39,20 @@ class KnowledgeAction(str, Enum):
39
39
  # Work items
40
40
  WORK_CREATE = (
41
41
  "work_create",
42
- "Creates a work item with title, status and optional links.",
42
+ "Creates a work item with title, status and optional links. Use parent_key (e.g., DVPT-0001) to set parent.",
43
43
  )
44
44
  WORK_LIST = (
45
45
  "work_list",
46
46
  "Lists work items with status, priority and context filters.",
47
47
  )
48
- WORK_GET = ("work_get", "Gets full details of a work item by ID.")
48
+ WORK_GET = (
49
+ "work_get",
50
+ "Gets full details of a work item by ID or key (e.g., DVPT-0001).",
51
+ )
49
52
  WORK_UPDATE = (
50
53
  "work_update",
51
- "Updates specific fields of an existing work item.",
54
+ "Updates allowed fields of an existing work item (title, description, priority, story_points, tags, due_date).",
52
55
  )
53
- WORK_DELETE = ("work_delete", "Permanently removes a work item.")
54
56
  WORK_BACKLOG = ("work_backlog", "Lists backlog items for a team.")
55
57
  WORK_SEARCH = (
56
58
  "work_search",
@@ -60,7 +62,10 @@ class KnowledgeAction(str, Enum):
60
62
  WORK_BY_BOARD = ("work_by_board", "Lists work items associated with a board.")
61
63
  WORK_BY_SPRINT = ("work_by_sprint", "Lists work items associated with a sprint.")
62
64
  WORK_BY_EPIC = ("work_by_epic", "Lists work items associated with an epic.")
63
- WORK_CHILDREN = ("work_children", "Lists child work items of a parent item.")
65
+ WORK_CHILDREN = (
66
+ "work_children",
67
+ "Lists child work items of a parent item by ID or key (e.g., DVPT-0001).",
68
+ )
64
69
  WORK_STATUS_UPDATE = (
65
70
  "work_status_update",
66
71
  "Updates only the status of a work item.",
@@ -69,6 +74,10 @@ class KnowledgeAction(str, Enum):
69
74
  "work_assign_sprint",
70
75
  "Assigns work items to a sprint.",
71
76
  )
77
+ WORK_ASSIGN_TO_ME = (
78
+ "work_assign_to_me",
79
+ "Assigns a work item to the current user.",
80
+ )
72
81
 
73
82
  # Boards
74
83
  BOARD_LIST = ("board_list", "Lists available boards with optional filters.")
@@ -203,6 +212,10 @@ class KnowledgeRequest(ToolRequest):
203
212
  )
204
213
 
205
214
  # Work item fields
215
+ work_key: Optional[str] = Field(
216
+ default=None,
217
+ description="Work item key (e.g., DVPT-0001). Use this instead of id for work_get and work_children.",
218
+ )
206
219
  work_title: Optional[TitleStr] = Field(default=None, description="Work item title.")
207
220
  work_description: Optional[MarkdownStr] = Field(
208
221
  default=None, description="Work item description (Markdown)."
@@ -213,7 +226,7 @@ class KnowledgeRequest(ToolRequest):
213
226
  )
214
227
  work_status: Optional[str] = Field(
215
228
  default=None,
216
- description="Work item status (backlog, todo, in_progress, review, testing, done, cancelled).",
229
+ description="Work item status ID (UUID of TeamStatus).",
217
230
  )
218
231
  work_priority: Optional[str] = Field(
219
232
  default=None,
@@ -230,7 +243,12 @@ class KnowledgeRequest(ToolRequest):
230
243
  default=None, description="Assignee ID (UUID)."
231
244
  )
232
245
  parent_id: Optional[UUIDStr] = Field(
233
- default=None, description="Parent item ID (UUID)."
246
+ default=None,
247
+ description="Parent item ID (UUID). Prefer using parent_key instead.",
248
+ )
249
+ parent_key: Optional[str] = Field(
250
+ default=None,
251
+ description="Parent item key (e.g., DVPT-0001). Use this to set the parent of a work item.",
234
252
  )
235
253
  work_due_date: Optional[DateTimeStr] = Field(
236
254
  default=None, description="Work item due date (ISO 8601)."
@@ -372,19 +390,25 @@ class KnowledgeTool(Tool):
372
390
  return text(
373
391
  "❌ Provide work_category to create the item. Values: backend, frontend, mobile, fullstack, devops, infra, platform, sre, database, security, data, analytics, ai_ml, qa, automation, design, research, product, project, agile, support, operations, documentation, training, architecture, planning, development."
374
392
  )
393
+
394
+ # Resolve parent_key to parent_id if provided
395
+ parent_id = payload.parent_id
396
+ if payload.parent_key and not parent_id:
397
+ parent_work = await self._service.work_get_by_key(payload.parent_key)
398
+ parent_id = parent_work.get("id")
399
+
375
400
  work = await self._service.work_create(
376
401
  {
377
402
  "title": payload.work_title,
378
403
  "description": payload.work_description,
379
404
  "item_type": payload.work_type,
380
- "status": payload.work_status,
405
+ "status_id": payload.work_status,
381
406
  "priority": payload.work_priority,
382
407
  "work_category": payload.work_category,
383
408
  "story_points": payload.story_points,
384
409
  "assignee_id": payload.assignee_id,
385
410
  "sprint_id": payload.sprint_id,
386
- "board_id": payload.board_id,
387
- "parent_id": payload.parent_id,
411
+ "parent_id": parent_id,
388
412
  "due_date": payload.work_due_date,
389
413
  "tags": payload.work_tags,
390
414
  }
@@ -399,7 +423,6 @@ class KnowledgeTool(Tool):
399
423
  type=payload.work_type,
400
424
  assignee=payload.assignee_id,
401
425
  sprint=payload.sprint_id,
402
- board=payload.board_id,
403
426
  )
404
427
  if not items:
405
428
  return text("🎯 No work items found.")
@@ -407,41 +430,51 @@ class KnowledgeTool(Tool):
407
430
  return text(f"🎯 **Work items ({len(items)}):**\n\n{body}")
408
431
 
409
432
  if action is KnowledgeAction.WORK_GET:
410
- if not payload.id:
411
- return text("❌ Provide the work item ID.")
412
- work = await self._service.work_get(payload.id)
433
+ # Support both id and work_key
434
+ if payload.work_key:
435
+ work = await self._service.work_get_by_key(payload.work_key)
436
+ elif payload.id:
437
+ work = await self._service.work_get(payload.id)
438
+ else:
439
+ return text("❌ Provide id or work_key to get the work item.")
413
440
  return text(
414
441
  _format_work(work, header="🎯 Work item details", show_description=True)
415
442
  )
416
443
 
417
444
  if action is KnowledgeAction.WORK_UPDATE:
418
- if not payload.id:
419
- return text("❌ Provide the work item ID.")
445
+ # Resolve work_key to id if needed
446
+ work_id = payload.id
447
+ if payload.work_key and not work_id:
448
+ work_item = await self._service.work_get_by_key(payload.work_key)
449
+ work_id = work_item.get("id")
450
+ if not work_id:
451
+ return text("❌ Provide id or work_key to update the work item.")
452
+
453
+ # Only allow safe fields to be updated via MCP
454
+ # Excluded: item_type, status_id, assignee_id, sprint_id, parent_id, work_category
420
455
  work = await self._service.work_update(
421
- payload.id,
456
+ work_id,
422
457
  {
423
458
  "title": payload.work_title,
424
459
  "description": payload.work_description,
425
- "item_type": payload.work_type,
426
- "status": payload.work_status,
427
460
  "priority": payload.work_priority,
428
- "work_category": payload.work_category,
429
461
  "story_points": payload.story_points,
430
- "assignee_id": payload.assignee_id,
431
- "sprint_id": payload.sprint_id,
432
- "board_id": payload.board_id,
433
- "parent_id": payload.parent_id,
434
462
  "due_date": payload.work_due_date,
435
463
  "tags": payload.work_tags,
436
464
  },
437
465
  )
438
466
  return text(_format_work(work, header="✅ Work item updated"))
439
467
 
440
- if action is KnowledgeAction.WORK_DELETE:
441
- if not payload.id:
442
- return text("❌ Provide the work item ID.")
443
- await self._service.work_delete(payload.id)
444
- return text(f"🗑️ Work item {payload.id} removed.")
468
+ if action is KnowledgeAction.WORK_ASSIGN_TO_ME:
469
+ # Resolve work_key to id if needed
470
+ work_id = payload.id
471
+ if payload.work_key and not work_id:
472
+ work_item = await self._service.work_get_by_key(payload.work_key)
473
+ work_id = work_item.get("id")
474
+ if not work_id:
475
+ return text("❌ Provide id or work_key to assign the work item.")
476
+ work = await self._service.work_assign_to_me(work_id)
477
+ return text(_format_work(work, header="✅ Work item assigned to you"))
445
478
 
446
479
  if action is KnowledgeAction.WORK_BACKLOG:
447
480
  items = await self._service.work_backlog()
@@ -503,22 +536,32 @@ class KnowledgeTool(Tool):
503
536
  return text(f"📦 **Epic work items ({len(items)}):**\n\n{body}")
504
537
 
505
538
  if action is KnowledgeAction.WORK_CHILDREN:
506
- if not payload.id:
507
- return text("❌ Provide the parent work item ID.")
508
- items = await self._service.work_children(payload.id)
539
+ # Support both id and work_key
540
+ work_id = payload.id
541
+ if payload.work_key and not work_id:
542
+ work_item = await self._service.work_get_by_key(payload.work_key)
543
+ work_id = work_item.get("id")
544
+ if not work_id:
545
+ return text("❌ Provide id or work_key to list children.")
546
+ items = await self._service.work_children(work_id)
509
547
  if not items:
510
548
  return text("👶 No child items found.")
511
549
  body = "\n\n".join(_format_work(item) for item in items)
512
550
  return text(f"👶 **Child work items ({len(items)}):**\n\n{body}")
513
551
 
514
552
  if action is KnowledgeAction.WORK_STATUS_UPDATE:
515
- if not payload.id:
516
- return text("❌ Provide the work item ID.")
553
+ # Resolve work_key to id if needed
554
+ work_id = payload.id
555
+ if payload.work_key and not work_id:
556
+ work_item = await self._service.work_get_by_key(payload.work_key)
557
+ work_id = work_item.get("id")
558
+ if not work_id:
559
+ return text("❌ Provide id or work_key to update status.")
517
560
  if not payload.work_status:
518
- return text("❌ Provide work_status to update.")
561
+ return text("❌ Provide work_status (status_id) to update.")
519
562
  work = await self._service.work_update_status(
520
- payload.id,
521
- {"status": payload.work_status},
563
+ work_id,
564
+ {"status_id": payload.work_status},
522
565
  )
523
566
  return text(_format_work(work, header="✅ Status updated"))
524
567
 
@@ -1084,14 +1127,21 @@ def _format_work(
1084
1127
  lines.append(header)
1085
1128
  lines.append("")
1086
1129
 
1130
+ # Extract key (e.g., DVPT-0001)
1131
+ key = item.get("key", "")
1132
+
1087
1133
  # Extract title
1088
1134
  title = item.get("title") or item.get("name") or "Untitled"
1089
1135
 
1090
1136
  # Extract type
1091
1137
  item_type = item.get("item_type") or item.get("type") or "unknown"
1092
1138
 
1093
- # Extract status
1094
- status = item.get("status") or item.get("state") or "unknown"
1139
+ # Extract status - prefer status object with name, fallback to status_id
1140
+ status_obj = item.get("status")
1141
+ if isinstance(status_obj, dict):
1142
+ status = status_obj.get("name", "unknown")
1143
+ else:
1144
+ status = item.get("status_id") or status_obj or "unknown"
1095
1145
 
1096
1146
  # Extract priority
1097
1147
  priority = item.get("priority") or item.get("priority_level") or "undefined"
@@ -1102,15 +1152,19 @@ def _format_work(
1102
1152
  # Extract assignee - check both assignee_id and assignee object
1103
1153
  assignee = item.get("assignee_id")
1104
1154
  if not assignee and item.get("assignee"):
1105
- assignee = item.get("assignee", {}).get("name") or item.get("assignee", {}).get(
1106
- "id"
1107
- )
1155
+ assignee_obj = item.get("assignee", {})
1156
+ assignee = assignee_obj.get("name") or assignee_obj.get("id")
1108
1157
  if not assignee:
1109
1158
  assignee = "N/A"
1110
1159
 
1160
+ # Format title line with key if available
1161
+ if key:
1162
+ lines.append(f"🎯 **[{key}] {title}**")
1163
+ else:
1164
+ lines.append(f"🎯 **{title}**")
1165
+
1111
1166
  lines.extend(
1112
1167
  [
1113
- f"🎯 **{title}**",
1114
1168
  f"ID: {item_id}",
1115
1169
  f"Type: {item_type}",
1116
1170
  f"Status: {status}",
@@ -1118,6 +1172,11 @@ def _format_work(
1118
1172
  f"Assignee: {assignee}",
1119
1173
  ]
1120
1174
  )
1175
+
1176
+ # Add key as separate line if present (for easy reference)
1177
+ if key:
1178
+ lines.append(f"Key: {key}")
1179
+
1121
1180
  if item.get("due_date") or item.get("dueDate"):
1122
1181
  lines.append(
1123
1182
  f"Due date: {_format_date(item.get('due_date') or item.get('dueDate'))}"
@@ -72,6 +72,12 @@ class KnowledgeService:
72
72
  async def work_get(self, work_id: str) -> Dict[str, Any]:
73
73
  return await self._call_dict(self.api.get_work_item, work_id)
74
74
 
75
+ async def work_get_by_key(self, key: str) -> Dict[str, Any]:
76
+ return await self._call_dict(self.api.get_work_item_by_key, key)
77
+
78
+ async def work_assign_to_me(self, work_id: str) -> Dict[str, Any]:
79
+ return await self._call_dict(self.api.assign_work_item_to_me, work_id)
80
+
75
81
  async def work_update(
76
82
  self, work_id: str, payload: Dict[str, Any]
77
83
  ) -> Dict[str, Any]:
@@ -492,6 +492,12 @@ class FenixApiClient:
492
492
  def get_work_item(self, item_id: str) -> Any:
493
493
  return self._request("GET", f"/api/work-items/{item_id}")
494
494
 
495
+ def get_work_item_by_key(self, key: str) -> Any:
496
+ return self._request("GET", f"/api/work-items/by-key/{key}")
497
+
498
+ def assign_work_item_to_me(self, item_id: str) -> Any:
499
+ return self._request("POST", f"/api/work-items/{item_id}/assign-to-me")
500
+
495
501
  def update_work_item(self, item_id: str, payload: Mapping[str, Any]) -> Any:
496
502
  return self._request("PATCH", f"/api/work-items/{item_id}", json=payload)
497
503
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fenix-mcp
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Fênix Cloud MCP server implemented in Python
5
5
  Author: Fenix Inc
6
6
  Requires-Python: >=3.10
@@ -1,4 +1,4 @@
1
- fenix_mcp/__init__.py,sha256=Kfh5kzR-z6NJepWTkqAOawt-M0YPenLm7NKRJzcsVpA,180
1
+ fenix_mcp/__init__.py,sha256=0hD_tbK6SIxOmfllb0TMUOmfIeKzQvRt5FZWd9Uhcgw,180
2
2
  fenix_mcp/main.py,sha256=iJV-9btNMDJMObvcn7wBQdbLLKjkYCQ1ANGEwHGHlMU,2857
3
3
  fenix_mcp/application/presenters.py,sha256=fGME54PdCDhTBhXO-JUB9yLdBHiE1aeXLTC2fCuxnxM,689
4
4
  fenix_mcp/application/tool_base.py,sha256=YJk7aSVGjXEvAkXrOHOuUjCFhYni9NPKFyPKiZqkrCc,4235
@@ -7,23 +7,23 @@ fenix_mcp/application/tools/__init__.py,sha256=Gi1YvYh-KdL9HD8gLVrknHrxiKKEOhHBE
7
7
  fenix_mcp/application/tools/health.py,sha256=m5DxhoRbdwl6INzd6PISxv1NAv-ljCrezsr773VB0wE,834
8
8
  fenix_mcp/application/tools/initialize.py,sha256=YfsE3fVYiqGEwvaI_jg5-0K7pGURXxpB3WNwETmGBPc,5499
9
9
  fenix_mcp/application/tools/intelligence.py,sha256=fXfjBwAQmZCn3Zc8BqFnQFAJkpd9JsfOPa_uXJj-bMU,15778
10
- fenix_mcp/application/tools/knowledge.py,sha256=AJkzw9V5vFkcNzJaufw1HCpZG9rbIACFG9DbC6YA1lQ,52263
10
+ fenix_mcp/application/tools/knowledge.py,sha256=mQL3PmXCQIBpSMhpxCaWmF7silj__s1pc5dMJSciXUw,54650
11
11
  fenix_mcp/application/tools/productivity.py,sha256=wyJ7-2VqgI2cdrliBD_ejwNvQhN1DecpXSQVrCxcUpQ,11231
12
12
  fenix_mcp/application/tools/user_config.py,sha256=O5AVg7IUKL9uIoUoBSFovBDHl9jofhKWzhFK7CnKi4s,6470
13
13
  fenix_mcp/domain/initialization.py,sha256=AZhdSNITQ7O3clELBuqGvjJc-c8pFKc7zQz-XR2xXPc,6933
14
14
  fenix_mcp/domain/intelligence.py,sha256=j1kkxT-pjuzLQeAGDd2H8gd3O1aeUIRgHFnMGvNwQYg,8636
15
- fenix_mcp/domain/knowledge.py,sha256=FmE3mGgu9jxr4fDKmBGdUBJ6KWiaVzqtmVu4ZXseXlE,20436
15
+ fenix_mcp/domain/knowledge.py,sha256=X2terO-05fMzCpDTEmdIoAAzooEqx2odF90zIOXyf6I,20726
16
16
  fenix_mcp/domain/productivity.py,sha256=PzY664eRPuBCfZGUY_Uv1GNeyMWsw6xqC54C-nobQns,6799
17
17
  fenix_mcp/domain/user_config.py,sha256=8rzhJCNqIArfaCoKxxQXFoemCU7qww3hq0RDanIf_2Y,2028
18
18
  fenix_mcp/infrastructure/config.py,sha256=zhJ3hhsP-bRfICcdq8rIDh5NGDe_u7AGpcgjcc2U1nY,1908
19
19
  fenix_mcp/infrastructure/context.py,sha256=kiDiamiPbHZpTGyZMylcQwtLhfaDXrxAkWSst_DWQNw,470
20
20
  fenix_mcp/infrastructure/http_client.py,sha256=QLIPhGYR_cBQGsbIO4RTR6ksyvkQt-OKHQU1JhPyap8,2470
21
21
  fenix_mcp/infrastructure/logging.py,sha256=bHrWlSi_0HshRe3--BK_5nzUszW-gh37q6jsd0ShS2Y,1371
22
- fenix_mcp/infrastructure/fenix_api/client.py,sha256=z5S6cwBxerpaDXj4Y4LWZEd7ZuGcVmgcaXQv3tTFPBs,28038
22
+ fenix_mcp/infrastructure/fenix_api/client.py,sha256=5ewLAdZcNO59zgCkFLke0sTLU8dalo1W0nXKATt3F4Q,28301
23
23
  fenix_mcp/interface/mcp_server.py,sha256=5UM2NJuNbwHkmCEprIFataJ5nFZiO8efTtP_oW3_iX0,2331
24
24
  fenix_mcp/interface/transports.py,sha256=PxdhfjH8UMl03f7nuCLc-M6tMx6-Y-btVz_mSqXKrSI,8138
25
- fenix_mcp-1.5.0.dist-info/METADATA,sha256=iF-2vrXuVW1dKcOh0r0_QpTwRxcysmlLsGAcNuW8MEg,7260
26
- fenix_mcp-1.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- fenix_mcp-1.5.0.dist-info/entry_points.txt,sha256=o52x_YHBupEd-1Z1GSfUjv3gJrx5_I-EkHhCgt1WBaE,49
28
- fenix_mcp-1.5.0.dist-info/top_level.txt,sha256=2G1UtKpwjaIGQyE7sRoHecxaGLeuexfjrOUjv9DDKh4,10
29
- fenix_mcp-1.5.0.dist-info/RECORD,,
25
+ fenix_mcp-1.6.0.dist-info/METADATA,sha256=ZIVb_fHfqXzT-S6-vxJqVplXjwGxNTIYYpPAH4AHirE,7260
26
+ fenix_mcp-1.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ fenix_mcp-1.6.0.dist-info/entry_points.txt,sha256=o52x_YHBupEd-1Z1GSfUjv3gJrx5_I-EkHhCgt1WBaE,49
28
+ fenix_mcp-1.6.0.dist-info/top_level.txt,sha256=2G1UtKpwjaIGQyE7sRoHecxaGLeuexfjrOUjv9DDKh4,10
29
+ fenix_mcp-1.6.0.dist-info/RECORD,,