fenix-mcp 1.4.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.4.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,12 +226,16 @@ 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,
220
233
  description="Work item priority (critical, high, medium, low).",
221
234
  )
235
+ work_category: Optional[str] = Field(
236
+ default=None,
237
+ description="Work item category/discipline. REQUIRED for work_create. 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.",
238
+ )
222
239
  story_points: Optional[int] = Field(
223
240
  default=None, ge=0, le=100, description="Story points (0-100)."
224
241
  )
@@ -226,7 +243,12 @@ class KnowledgeRequest(ToolRequest):
226
243
  default=None, description="Assignee ID (UUID)."
227
244
  )
228
245
  parent_id: Optional[UUIDStr] = Field(
229
- 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.",
230
252
  )
231
253
  work_due_date: Optional[DateTimeStr] = Field(
232
254
  default=None, description="Work item due date (ISO 8601)."
@@ -364,18 +386,29 @@ class KnowledgeTool(Tool):
364
386
  if action is KnowledgeAction.WORK_CREATE:
365
387
  if not payload.work_title:
366
388
  return text("❌ Provide work_title to create the item.")
389
+ if not payload.work_category:
390
+ return text(
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."
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
+
367
400
  work = await self._service.work_create(
368
401
  {
369
402
  "title": payload.work_title,
370
403
  "description": payload.work_description,
371
404
  "item_type": payload.work_type,
372
- "status": payload.work_status,
405
+ "status_id": payload.work_status,
373
406
  "priority": payload.work_priority,
407
+ "work_category": payload.work_category,
374
408
  "story_points": payload.story_points,
375
409
  "assignee_id": payload.assignee_id,
376
410
  "sprint_id": payload.sprint_id,
377
- "board_id": payload.board_id,
378
- "parent_id": payload.parent_id,
411
+ "parent_id": parent_id,
379
412
  "due_date": payload.work_due_date,
380
413
  "tags": payload.work_tags,
381
414
  }
@@ -390,7 +423,6 @@ class KnowledgeTool(Tool):
390
423
  type=payload.work_type,
391
424
  assignee=payload.assignee_id,
392
425
  sprint=payload.sprint_id,
393
- board=payload.board_id,
394
426
  )
395
427
  if not items:
396
428
  return text("🎯 No work items found.")
@@ -398,40 +430,51 @@ class KnowledgeTool(Tool):
398
430
  return text(f"🎯 **Work items ({len(items)}):**\n\n{body}")
399
431
 
400
432
  if action is KnowledgeAction.WORK_GET:
401
- if not payload.id:
402
- return text("❌ Provide the work item ID.")
403
- 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.")
404
440
  return text(
405
441
  _format_work(work, header="🎯 Work item details", show_description=True)
406
442
  )
407
443
 
408
444
  if action is KnowledgeAction.WORK_UPDATE:
409
- if not payload.id:
410
- 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
411
455
  work = await self._service.work_update(
412
- payload.id,
456
+ work_id,
413
457
  {
414
458
  "title": payload.work_title,
415
459
  "description": payload.work_description,
416
- "item_type": payload.work_type,
417
- "status": payload.work_status,
418
460
  "priority": payload.work_priority,
419
461
  "story_points": payload.story_points,
420
- "assignee_id": payload.assignee_id,
421
- "sprint_id": payload.sprint_id,
422
- "board_id": payload.board_id,
423
- "parent_id": payload.parent_id,
424
462
  "due_date": payload.work_due_date,
425
463
  "tags": payload.work_tags,
426
464
  },
427
465
  )
428
466
  return text(_format_work(work, header="✅ Work item updated"))
429
467
 
430
- if action is KnowledgeAction.WORK_DELETE:
431
- if not payload.id:
432
- return text("❌ Provide the work item ID.")
433
- await self._service.work_delete(payload.id)
434
- 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"))
435
478
 
436
479
  if action is KnowledgeAction.WORK_BACKLOG:
437
480
  items = await self._service.work_backlog()
@@ -493,22 +536,32 @@ class KnowledgeTool(Tool):
493
536
  return text(f"📦 **Epic work items ({len(items)}):**\n\n{body}")
494
537
 
495
538
  if action is KnowledgeAction.WORK_CHILDREN:
496
- if not payload.id:
497
- return text("❌ Provide the parent work item ID.")
498
- 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)
499
547
  if not items:
500
548
  return text("👶 No child items found.")
501
549
  body = "\n\n".join(_format_work(item) for item in items)
502
550
  return text(f"👶 **Child work items ({len(items)}):**\n\n{body}")
503
551
 
504
552
  if action is KnowledgeAction.WORK_STATUS_UPDATE:
505
- if not payload.id:
506
- 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.")
507
560
  if not payload.work_status:
508
- return text("❌ Provide work_status to update.")
561
+ return text("❌ Provide work_status (status_id) to update.")
509
562
  work = await self._service.work_update_status(
510
- payload.id,
511
- {"status": payload.work_status},
563
+ work_id,
564
+ {"status_id": payload.work_status},
512
565
  )
513
566
  return text(_format_work(work, header="✅ Status updated"))
514
567
 
@@ -1074,14 +1127,21 @@ def _format_work(
1074
1127
  lines.append(header)
1075
1128
  lines.append("")
1076
1129
 
1130
+ # Extract key (e.g., DVPT-0001)
1131
+ key = item.get("key", "")
1132
+
1077
1133
  # Extract title
1078
1134
  title = item.get("title") or item.get("name") or "Untitled"
1079
1135
 
1080
1136
  # Extract type
1081
1137
  item_type = item.get("item_type") or item.get("type") or "unknown"
1082
1138
 
1083
- # Extract status
1084
- 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"
1085
1145
 
1086
1146
  # Extract priority
1087
1147
  priority = item.get("priority") or item.get("priority_level") or "undefined"
@@ -1092,15 +1152,19 @@ def _format_work(
1092
1152
  # Extract assignee - check both assignee_id and assignee object
1093
1153
  assignee = item.get("assignee_id")
1094
1154
  if not assignee and item.get("assignee"):
1095
- assignee = item.get("assignee", {}).get("name") or item.get("assignee", {}).get(
1096
- "id"
1097
- )
1155
+ assignee_obj = item.get("assignee", {})
1156
+ assignee = assignee_obj.get("name") or assignee_obj.get("id")
1098
1157
  if not assignee:
1099
1158
  assignee = "N/A"
1100
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
+
1101
1166
  lines.extend(
1102
1167
  [
1103
- f"🎯 **{title}**",
1104
1168
  f"ID: {item_id}",
1105
1169
  f"Type: {item_type}",
1106
1170
  f"Status: {status}",
@@ -1108,6 +1172,11 @@ def _format_work(
1108
1172
  f"Assignee: {assignee}",
1109
1173
  ]
1110
1174
  )
1175
+
1176
+ # Add key as separate line if present (for easy reference)
1177
+ if key:
1178
+ lines.append(f"Key: {key}")
1179
+
1111
1180
  if item.get("due_date") or item.get("dueDate"):
1112
1181
  lines.append(
1113
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.4.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=c39bdiud7BvOvRegYtF7vm5AIO3SEp8fgNQu1KJGSDM,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=W8YFAPy3s1l7amdWLSEdG3xOXbPVyORCfM8Lza_M3Qs,51312
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.4.0.dist-info/METADATA,sha256=A0YktRHXhAMgbmzjdbsm0oMDV7V079flOVhTs3QJxqw,7260
26
- fenix_mcp-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- fenix_mcp-1.4.0.dist-info/entry_points.txt,sha256=o52x_YHBupEd-1Z1GSfUjv3gJrx5_I-EkHhCgt1WBaE,49
28
- fenix_mcp-1.4.0.dist-info/top_level.txt,sha256=2G1UtKpwjaIGQyE7sRoHecxaGLeuexfjrOUjv9DDKh4,10
29
- fenix_mcp-1.4.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,,