fenix-mcp 1.0.0__py3-none-any.whl → 1.2.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.0.0"
11
+ __version__ = "1.2.0"
@@ -35,6 +35,7 @@ class IntelligenceAction(str, Enum):
35
35
  "Creates intelligent memories with similarity analysis.",
36
36
  )
37
37
  QUERY = ("memory_query", "Lists memories applying filters and text search.")
38
+ GET = ("memory_get", "Gets memory details and content by ID.")
38
39
  SIMILARITY = ("memory_similarity", "Finds memories similar to a base content.")
39
40
  CONSOLIDATE = (
40
41
  "memory_consolidate",
@@ -185,6 +186,8 @@ class IntelligenceTool(Tool):
185
186
  return await self._handle_smart_create(payload)
186
187
  if action is IntelligenceAction.QUERY:
187
188
  return await self._handle_query(payload)
189
+ if action is IntelligenceAction.GET:
190
+ return await self._handle_get(payload)
188
191
  if action is IntelligenceAction.SIMILARITY:
189
192
  return await self._handle_similarity(payload)
190
193
  if action is IntelligenceAction.CONSOLIDATE:
@@ -258,6 +261,14 @@ class IntelligenceTool(Tool):
258
261
  body = "\n\n".join(_format_memory(mem) for mem in memories)
259
262
  return text(f"🧠 **Memories ({len(memories)}):**\n\n{body}")
260
263
 
264
+ async def _handle_get(self, payload: IntelligenceRequest):
265
+ if not payload.id:
266
+ return text("❌ Provide the memory ID to get details.")
267
+ memory = await self._service.get_memory(
268
+ payload.id, include_content=True, include_metadata=True
269
+ )
270
+ return text(_format_memory(memory, show_content=True))
271
+
261
272
  async def _handle_similarity(self, payload: IntelligenceRequest):
262
273
  if not payload.content:
263
274
  return text("❌ Provide the base content to compare similarity.")
@@ -343,17 +354,20 @@ class IntelligenceTool(Tool):
343
354
  )
344
355
 
345
356
 
346
- def _format_memory(memory: Dict[str, Any]) -> str:
347
- return "\n".join(
348
- [
349
- f"🧠 **{memory.get('title', 'Untitled')}**",
350
- f"ID: {memory.get('id', memory.get('memoryId', 'N/A'))}",
351
- f"Category: {memory.get('category', 'N/A')}",
352
- f"Tags: {', '.join(memory.get('tags', [])) or 'None'}",
353
- f"Importance: {memory.get('importance', 'N/A')}",
354
- f"Accesses: {memory.get('access_count', 'N/A')}",
355
- ]
356
- )
357
+ def _format_memory(memory: Dict[str, Any], *, show_content: bool = False) -> str:
358
+ lines = [
359
+ f"🧠 **{memory.get('title', 'Untitled')}**",
360
+ f"ID: {memory.get('id', memory.get('memoryId', 'N/A'))}",
361
+ f"Category: {memory.get('category', 'N/A')}",
362
+ f"Tags: {', '.join(memory.get('tags', [])) or 'None'}",
363
+ f"Importance: {memory.get('importance', 'N/A')}",
364
+ f"Accesses: {memory.get('access_count', 'N/A')}",
365
+ ]
366
+ if show_content and memory.get("content"):
367
+ lines.append("")
368
+ lines.append("**Content:**")
369
+ lines.append(memory.get("content"))
370
+ return "\n".join(lines)
357
371
 
358
372
 
359
373
  def format_percentage(value: Optional[float]) -> str:
@@ -107,19 +107,36 @@ class KnowledgeAction(str, Enum):
107
107
  RULE_UPDATE = ("rule_update", "Updates an existing rule.")
108
108
  RULE_DELETE = ("rule_delete", "Removes a rule.")
109
109
 
110
- # Documentation
111
- DOC_CREATE = ("doc_create", "Creates a documentation item.")
112
- DOC_LIST = ("doc_list", "Lists documentation items with filters.")
113
- DOC_GET = ("doc_get", "Gets documentation item details.")
110
+ # Documentation - Navigation workflow: doc_full_tree -> doc_children -> doc_get
111
+ DOC_CREATE = (
112
+ "doc_create",
113
+ "Creates a documentation item. Requires doc_emoji for page, api_doc, and guide types.",
114
+ )
115
+ DOC_LIST = (
116
+ "doc_list",
117
+ "Lists documentation items. Use doc_full_tree for hierarchical view.",
118
+ )
119
+ DOC_GET = (
120
+ "doc_get",
121
+ "Reads document content by ID. Get the ID from doc_full_tree or doc_children first.",
122
+ )
114
123
  DOC_UPDATE = ("doc_update", "Updates a documentation item.")
115
124
  DOC_DELETE = ("doc_delete", "Removes a documentation item.")
116
- DOC_SEARCH = ("doc_search", "Searches documentation items by text.")
117
- DOC_ROOTS = ("doc_roots", "Lists available root documents.")
125
+ DOC_ROOTS = (
126
+ "doc_roots",
127
+ "Lists root folders. Use doc_children to navigate inside.",
128
+ )
118
129
  DOC_RECENT = ("doc_recent", "Lists recently accessed documents.")
119
130
  DOC_ANALYTICS = ("doc_analytics", "Returns document analytics.")
120
- DOC_CHILDREN = ("doc_children", "Lists child documents of an item.")
121
- DOC_TREE = ("doc_tree", "Retrieves the direct tree of a document.")
122
- DOC_FULL_TREE = ("doc_full_tree", "Retrieves the full documentation tree.")
131
+ DOC_CHILDREN = (
132
+ "doc_children",
133
+ "Lists child documents of a folder by ID. Use this to navigate into folders.",
134
+ )
135
+ DOC_TREE = ("doc_tree", "Retrieves tree starting from a specific document.")
136
+ DOC_FULL_TREE = (
137
+ "doc_full_tree",
138
+ "Retrieves complete documentation tree. Start here to find documents.",
139
+ )
123
140
  DOC_MOVE = ("doc_move", "Moves a document to another parent.")
124
141
  DOC_PUBLISH = ("doc_publish", "Changes publication status of a document.")
125
142
  DOC_VERSION = ("doc_version", "Generates or retrieves a document version.")
@@ -295,7 +312,8 @@ class KnowledgeRequest(ToolRequest):
295
312
  default=None, ge=0, description="Desired position when moving documents."
296
313
  )
297
314
  doc_emoji: Optional[EmojiStr] = Field(
298
- default=None, description="Emoji displayed with the document."
315
+ default=None,
316
+ description="Emoji displayed with the document. REQUIRED for page, api_doc, and guide types (not required for folder).",
299
317
  )
300
318
  doc_emote: Optional[EmojiStr] = Field(
301
319
  default=None, description="Alias for emoji, kept for compatibility."
@@ -383,7 +401,9 @@ class KnowledgeTool(Tool):
383
401
  if not payload.id:
384
402
  return text("❌ Provide the work item ID.")
385
403
  work = await self._service.work_get(payload.id)
386
- return text(_format_work(work, header="🎯 Work item details"))
404
+ return text(
405
+ _format_work(work, header="🎯 Work item details", show_description=True)
406
+ )
387
407
 
388
408
  if action is KnowledgeAction.WORK_UPDATE:
389
409
  if not payload.id:
@@ -427,6 +447,11 @@ class KnowledgeTool(Tool):
427
447
  items = await self._service.work_search(
428
448
  query=query,
429
449
  limit=payload.limit,
450
+ item_type=payload.work_type,
451
+ status=payload.work_status,
452
+ priority=payload.work_priority,
453
+ assignee_id=payload.assignee_id,
454
+ tags=payload.work_tags,
430
455
  )
431
456
  if not items:
432
457
  return text("🔍 No work items found.")
@@ -654,7 +679,7 @@ class KnowledgeTool(Tool):
654
679
  return_description=payload.return_description,
655
680
  return_metadata=payload.return_metadata,
656
681
  )
657
- return text(_format_mode(mode, header="🎭 Mode details"))
682
+ return text(_format_mode(mode, header="🎭 Mode details", show_content=True))
658
683
 
659
684
  if action is KnowledgeAction.MODE_UPDATE:
660
685
  if not payload.mode_id:
@@ -762,7 +787,7 @@ class KnowledgeTool(Tool):
762
787
  return_metadata=payload.return_metadata,
763
788
  return_modes=payload.return_metadata,
764
789
  )
765
- return text(_format_rule(rule, header="📋 Rule details"))
790
+ return text(_format_rule(rule, header="📋 Rule details", show_content=True))
766
791
 
767
792
  if action is KnowledgeAction.RULE_UPDATE:
768
793
  if not payload.rule_id:
@@ -807,6 +832,14 @@ class KnowledgeTool(Tool):
807
832
  return text(
808
833
  "❌ Invalid doc_type. Use one of the supported values: " + allowed
809
834
  )
835
+ # Emoji is required for page, api_doc, guide (not for folder)
836
+ doc_type = sanitize_null(payload.doc_type) or "page"
837
+ emoji = sanitize_null(payload.doc_emoji or payload.doc_emote)
838
+ if doc_type != "folder" and not emoji:
839
+ return text(
840
+ "❌ Provide doc_emoji to create documentation. "
841
+ "Emoji is required for page, api_doc, and guide types."
842
+ )
810
843
  doc = await self._service.doc_create(
811
844
  {
812
845
  "title": payload.doc_title,
@@ -842,11 +875,10 @@ class KnowledgeTool(Tool):
842
875
  if action is KnowledgeAction.DOC_GET:
843
876
  if not payload.id:
844
877
  return text("❌ Provide the documentation ID.")
845
- doc = await self._service.doc_get(
846
- payload.id,
847
- returnContent=payload.return_content,
878
+ doc = await self._service.doc_get(payload.id)
879
+ return text(
880
+ _format_doc(doc, header="📄 Documentation details", show_content=True)
848
881
  )
849
- return text(_format_doc(doc, header="📄 Documentation details"))
850
882
 
851
883
  if action is KnowledgeAction.DOC_UPDATE:
852
884
  if not payload.id:
@@ -884,19 +916,6 @@ class KnowledgeTool(Tool):
884
916
  await self._service.doc_delete(payload.id)
885
917
  return text(f"🗑️ Documentation {payload.id} removed.")
886
918
 
887
- if action is KnowledgeAction.DOC_SEARCH:
888
- query = sanitize_null(payload.query)
889
- if not query:
890
- return text("❌ Provide query to search documentation.")
891
- docs = await self._service.doc_search(
892
- query=query,
893
- limit=payload.limit,
894
- )
895
- if not docs:
896
- return text("🔍 No documents found for the specified filters.")
897
- body = "\n\n".join(_format_doc(doc) for doc in docs)
898
- return text(f"🔍 **Results ({len(docs)}):**\n\n{body}")
899
-
900
919
  if action is KnowledgeAction.DOC_ROOTS:
901
920
  docs = await self._service.doc_roots()
902
921
  if not docs:
@@ -1001,13 +1020,34 @@ class KnowledgeTool(Tool):
1001
1020
  )
1002
1021
 
1003
1022
  async def _handle_help(self):
1023
+ workflow_guide = """
1024
+ ## 📖 How to find and read a document
1025
+
1026
+ 1. **doc_full_tree** → See complete folder structure with IDs
1027
+ 2. **doc_children(id)** → List contents of a specific folder
1028
+ 3. **doc_get(id)** → Read the document content
1029
+
1030
+ Example: To find "Overview" inside "Architecture" folder:
1031
+ 1. Call `doc_full_tree` to see all folders and documents
1032
+ 2. Find the folder "Architecture" and note its ID
1033
+ 3. Call `doc_children` with that ID to list its contents
1034
+ 4. Find "Overview" document and note its ID
1035
+ 5. Call `doc_get` with that ID to read the content
1036
+
1037
+ """
1004
1038
  return text(
1005
1039
  "📚 **Available actions for knowledge**\n\n"
1040
+ + workflow_guide
1006
1041
  + KnowledgeAction.formatted_help()
1007
1042
  )
1008
1043
 
1009
1044
 
1010
- def _format_work(item: Dict[str, Any], *, header: Optional[str] = None) -> str:
1045
+ def _format_work(
1046
+ item: Dict[str, Any],
1047
+ *,
1048
+ header: Optional[str] = None,
1049
+ show_description: bool = False,
1050
+ ) -> str:
1011
1051
  lines: List[str] = []
1012
1052
  if header:
1013
1053
  lines.append(header)
@@ -1051,6 +1091,13 @@ def _format_work(item: Dict[str, Any], *, header: Optional[str] = None) -> str:
1051
1091
  tags = item.get("tags", [])
1052
1092
  if tags:
1053
1093
  lines.append(f"Tags: {', '.join(tags)}")
1094
+
1095
+ # Show description only when explicitly requested (e.g., work_get)
1096
+ if show_description and item.get("description"):
1097
+ lines.append("")
1098
+ lines.append("**Description:**")
1099
+ lines.append(item.get("description"))
1100
+
1054
1101
  return "\n".join(lines)
1055
1102
 
1056
1103
 
@@ -1094,7 +1141,12 @@ def _format_sprint(sprint: Dict[str, Any], header: Optional[str] = None) -> str:
1094
1141
  return "\n".join(lines)
1095
1142
 
1096
1143
 
1097
- def _format_mode(mode: Dict[str, Any], header: Optional[str] = None) -> str:
1144
+ def _format_mode(
1145
+ mode: Dict[str, Any],
1146
+ *,
1147
+ header: Optional[str] = None,
1148
+ show_content: bool = False,
1149
+ ) -> str:
1098
1150
  lines: List[str] = []
1099
1151
  if header:
1100
1152
  lines.append(header)
@@ -1108,10 +1160,19 @@ def _format_mode(mode: Dict[str, Any], header: Optional[str] = None) -> str:
1108
1160
  )
1109
1161
  if mode.get("description"):
1110
1162
  lines.append(f"Description: {mode['description']}")
1163
+ if show_content and mode.get("content"):
1164
+ lines.append("")
1165
+ lines.append("**Content:**")
1166
+ lines.append(mode.get("content"))
1111
1167
  return "\n".join(lines)
1112
1168
 
1113
1169
 
1114
- def _format_rule(rule: Dict[str, Any], header: Optional[str] = None) -> str:
1170
+ def _format_rule(
1171
+ rule: Dict[str, Any],
1172
+ *,
1173
+ header: Optional[str] = None,
1174
+ show_content: bool = False,
1175
+ ) -> str:
1115
1176
  lines: List[str] = []
1116
1177
  if header:
1117
1178
  lines.append(header)
@@ -1125,10 +1186,19 @@ def _format_rule(rule: Dict[str, Any], header: Optional[str] = None) -> str:
1125
1186
  )
1126
1187
  if rule.get("description"):
1127
1188
  lines.append(f"Description: {rule['description']}")
1189
+ if show_content and rule.get("content"):
1190
+ lines.append("")
1191
+ lines.append("**Content:**")
1192
+ lines.append(rule.get("content"))
1128
1193
  return "\n".join(lines)
1129
1194
 
1130
1195
 
1131
- def _format_doc(doc: Dict[str, Any], header: Optional[str] = None) -> str:
1196
+ def _format_doc(
1197
+ doc: Dict[str, Any],
1198
+ *,
1199
+ header: Optional[str] = None,
1200
+ show_content: bool = False,
1201
+ ) -> str:
1132
1202
  lines: List[str] = []
1133
1203
  if header:
1134
1204
  lines.append(header)
@@ -1145,6 +1215,13 @@ def _format_doc(doc: Dict[str, Any], header: Optional[str] = None) -> str:
1145
1215
  lines.append(
1146
1216
  f"Updated at: {_format_date(doc.get('updated_at') or doc.get('updatedAt'))}"
1147
1217
  )
1218
+
1219
+ # Show content only when explicitly requested (e.g., doc_get)
1220
+ if show_content and doc.get("content"):
1221
+ lines.append("")
1222
+ lines.append("**Content:**")
1223
+ lines.append(doc.get("content"))
1224
+
1148
1225
  return "\n".join(lines)
1149
1226
 
1150
1227
 
@@ -31,8 +31,12 @@ class TodoAction(str, Enum):
31
31
  return obj
32
32
 
33
33
  CREATE = ("todo_create", "Creates a new TODO.")
34
- LIST = ("todo_list", "Lists TODOs with optional filters.")
35
- GET = ("todo_get", "Gets TODO details by ID.")
34
+ LIST = (
35
+ "todo_list",
36
+ "Lists TODOs. By default returns only pending and in_progress. "
37
+ "To filter by status, use status parameter with values: pending (backlog), in_progress, completed, cancelled.",
38
+ )
39
+ GET = ("todo_get", "Gets TODO details and content by ID.")
36
40
  UPDATE = ("todo_update", "Updates fields of an existing TODO.")
37
41
  DELETE = ("todo_delete", "Removes a TODO by ID.")
38
42
  STATS = ("todo_stats", "Returns aggregated TODO statistics.")
@@ -77,7 +81,7 @@ class ProductivityRequest(ToolRequest):
77
81
  )
78
82
  status: Optional[str] = Field(
79
83
  default=None,
80
- description="TODO status (pending, in_progress, completed, cancelled).",
84
+ description="TODO status. Values: pending (shown as 'backlog' in UI), in_progress, completed, cancelled.",
81
85
  )
82
86
  priority: Optional[str] = Field(
83
87
  default=None, description="TODO priority (low, medium, high, urgent)."
@@ -154,13 +158,32 @@ class ProductivityTool(Tool):
154
158
  return text(self._format_single(todo, header="✅ TODO created successfully!"))
155
159
 
156
160
  async def _handle_list(self, payload: ProductivityRequest):
157
- todos = await self._service.list_todos(
158
- limit=payload.limit,
159
- offset=payload.offset,
160
- status=payload.status,
161
- priority=payload.priority,
162
- category=payload.category,
163
- )
161
+ # If no status filter provided, fetch pending and in_progress only
162
+ if payload.status:
163
+ todos = await self._service.list_todos(
164
+ limit=payload.limit,
165
+ offset=payload.offset,
166
+ status=payload.status,
167
+ priority=payload.priority,
168
+ category=payload.category,
169
+ )
170
+ else:
171
+ # Fetch pending and in_progress separately and merge
172
+ pending = await self._service.list_todos(
173
+ limit=payload.limit,
174
+ offset=payload.offset,
175
+ status="pending",
176
+ priority=payload.priority,
177
+ category=payload.category,
178
+ )
179
+ in_progress = await self._service.list_todos(
180
+ limit=payload.limit,
181
+ offset=payload.offset,
182
+ status="in_progress",
183
+ priority=payload.priority,
184
+ category=payload.category,
185
+ )
186
+ todos = pending + in_progress
164
187
  if not todos:
165
188
  return text("📋 No TODOs found.")
166
189
  body = "\n\n".join(ProductivityService.format_todo(todo) for todo in todos)
@@ -170,7 +193,9 @@ class ProductivityTool(Tool):
170
193
  if not payload.id:
171
194
  return text("❌ Provide the ID to get a TODO.")
172
195
  todo = await self._service.get_todo(payload.id)
173
- return text(self._format_single(todo, header="📋 TODO found"))
196
+ return text(
197
+ self._format_single(todo, header="📋 TODO found", show_content=True)
198
+ )
174
199
 
175
200
  async def _handle_update(self, payload: ProductivityRequest):
176
201
  if not payload.id:
@@ -249,5 +274,13 @@ class ProductivityTool(Tool):
249
274
  )
250
275
 
251
276
  @staticmethod
252
- def _format_single(todo: Dict[str, Any], *, header: str) -> str:
253
- return "\n".join([header, "", ProductivityService.format_todo(todo)])
277
+ def _format_single(
278
+ todo: Dict[str, Any], *, header: str, show_content: bool = False
279
+ ) -> str:
280
+ return "\n".join(
281
+ [
282
+ header,
283
+ "",
284
+ ProductivityService.format_todo(todo, show_content=show_content),
285
+ ]
286
+ )
@@ -85,11 +85,26 @@ class KnowledgeService:
85
85
  async def work_backlog(self) -> List[Dict[str, Any]]:
86
86
  return await self._call_list(self.api.list_work_items_backlog)
87
87
 
88
- async def work_search(self, *, query: str, limit: int) -> List[Dict[str, Any]]:
88
+ async def work_search(
89
+ self,
90
+ *,
91
+ query: str,
92
+ limit: int,
93
+ item_type: Optional[str] = None,
94
+ status: Optional[str] = None,
95
+ priority: Optional[str] = None,
96
+ assignee_id: Optional[str] = None,
97
+ tags: Optional[List[str]] = None,
98
+ ) -> List[Dict[str, Any]]:
89
99
  return await self._call_list(
90
100
  self.api.search_work_items,
91
101
  query=query,
92
102
  limit=limit,
103
+ item_type=item_type,
104
+ status=status,
105
+ priority=priority,
106
+ assignee_id=assignee_id,
107
+ tags=tags,
93
108
  )
94
109
 
95
110
  async def work_analytics(self) -> Dict[str, Any]:
@@ -472,14 +487,6 @@ class KnowledgeService:
472
487
  async def doc_delete(self, doc_id: str) -> None:
473
488
  await self._call(self.api.delete_documentation_item, doc_id)
474
489
 
475
- async def doc_search(self, *, query: str, limit: int) -> List[Dict[str, Any]]:
476
- result = await self._call(
477
- self.api.search_documentation_items,
478
- query=query,
479
- limit=limit,
480
- )
481
- return _ensure_list(result)
482
-
483
490
  async def doc_roots(self) -> List[Dict[str, Any]]:
484
491
  result = await self._call(self.api.list_documentation_roots)
485
492
  return _ensure_list(result)
@@ -496,7 +503,7 @@ class KnowledgeService:
496
503
  return _ensure_dict(result)
497
504
 
498
505
  async def doc_children(self, doc_id: str) -> List[Dict[str, Any]]:
499
- return await self._call(self.api.get_documentation_children, doc_id) or []
506
+ return await self._call_list(self.api.get_documentation_children, doc_id)
500
507
 
501
508
  async def doc_tree(self, doc_id: str) -> Dict[str, Any]:
502
509
  return await self._call(self.api.get_documentation_tree, doc_id) or {}
@@ -148,17 +148,20 @@ class ProductivityService:
148
148
  return []
149
149
 
150
150
  @staticmethod
151
- def format_todo(item: Dict[str, Any]) -> str:
152
- return "\n".join(
153
- [
154
- f"📋 **{item.get('title', 'Sem título')}**",
155
- f"ID: {item.get('id', 'N/A')}",
156
- f"Status: {item.get('status', 'desconhecido')}",
157
- f"Prioridade: {item.get('priority', 'desconhecida')}",
158
- f"Categoria: {item.get('category') or 'não definida'}",
159
- f"Vencimento: {format_date(item.get('dueDate') or item.get('due_date'))}",
160
- ]
161
- )
151
+ def format_todo(item: Dict[str, Any], *, show_content: bool = False) -> str:
152
+ lines = [
153
+ f"📋 **{item.get('title', 'No title')}**",
154
+ f"ID: {item.get('id', 'N/A')}",
155
+ f"Status: {item.get('status', 'unknown')}",
156
+ f"Priority: {item.get('priority', 'unknown')}",
157
+ f"Category: {item.get('category') or 'not set'}",
158
+ f"Due date: {format_date(item.get('dueDate') or item.get('due_date'))}",
159
+ ]
160
+ if show_content and item.get("content"):
161
+ lines.append("")
162
+ lines.append("**Content:**")
163
+ lines.append(item.get("content"))
164
+ return "\n".join(lines)
162
165
 
163
166
 
164
167
  def _strip_none(data: Dict[str, Any]) -> Dict[str, Any]:
@@ -177,9 +180,9 @@ def _coerce_str_list(value: Any, *, fallback_key: str) -> List[str]:
177
180
 
178
181
  def format_date(value: Optional[str]) -> str:
179
182
  if not value:
180
- return "não definido"
183
+ return "not set"
181
184
  try:
182
185
  dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
183
- return dt.strftime("%d/%m/%Y")
186
+ return dt.strftime("%Y-%m-%d")
184
187
  except ValueError:
185
188
  return value
@@ -4,7 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  from dataclasses import dataclass, field
7
- from typing import Any, Dict, Mapping, Optional
7
+ from typing import Any, Dict, List, Mapping, Optional
8
8
 
9
9
  from fenix_mcp.infrastructure.http_client import HttpClient
10
10
 
@@ -501,8 +501,27 @@ class FenixApiClient:
501
501
  def list_work_items_backlog(self) -> Any:
502
502
  return self._request("GET", "/api/work-items/backlog")
503
503
 
504
- def search_work_items(self, *, query: str, limit: int) -> Any:
505
- params = self._build_params(required={"q": query, "limit": limit})
504
+ def search_work_items(
505
+ self,
506
+ *,
507
+ query: str,
508
+ limit: int,
509
+ item_type: Optional[str] = None,
510
+ status: Optional[str] = None,
511
+ priority: Optional[str] = None,
512
+ assignee_id: Optional[str] = None,
513
+ tags: Optional[List[str]] = None,
514
+ ) -> Any:
515
+ params = self._build_params(
516
+ required={"q": query, "limit": limit},
517
+ optional={
518
+ "item_type": item_type,
519
+ "status": status,
520
+ "priority": priority,
521
+ "assignee_id": assignee_id,
522
+ "tags": ",".join(tags) if tags else None,
523
+ },
524
+ )
506
525
  return self._request("GET", "/api/work-items/search", params=params)
507
526
 
508
527
  def get_work_items_analytics(self) -> Any:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fenix-mcp
3
- Version: 1.0.0
3
+ Version: 1.2.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=8n7G23wlV18PQNQP_Q37ubfEXQW3IBAttHpw5NECZus,180
1
+ fenix_mcp/__init__.py,sha256=O5PVWpKntZHHyfKRMCPhfNtYk_njwoiRCa3nwTtaoQ8,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
@@ -6,24 +6,24 @@ fenix_mcp/application/tool_registry.py,sha256=bPT5g8GfxG_qu28R1WaDOZHvtmG6TPDvZi
6
6
  fenix_mcp/application/tools/__init__.py,sha256=Gi1YvYh-KdL9HD8gLVrknHrxiKKEOhHBEZ02KBXJaKQ,796
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
- fenix_mcp/application/tools/intelligence.py,sha256=3HRPMQEx5cyKBCjkIpN48W1YibduxQ2h07lQtERQ0aY,15105
10
- fenix_mcp/application/tools/knowledge.py,sha256=nfM99_cE6jK4p50_2BDr5Xxaybs0IcQFifUtrrhwRYc,47901
11
- fenix_mcp/application/tools/productivity.py,sha256=bC6HqBHlvjzHvNbfcRp7W6-Z6HHwDS5B8_rTBde0dyo,10033
9
+ fenix_mcp/application/tools/intelligence.py,sha256=fXfjBwAQmZCn3Zc8BqFnQFAJkpd9JsfOPa_uXJj-bMU,15778
10
+ fenix_mcp/application/tools/knowledge.py,sha256=hTtf5ijVUg47tFYVN4Y-Exzt8J55h6fOV5xJrK3ydOk,50065
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=PG8GinNNgM-UatnzPbNcs5UGOLLvr_UzU9ZThNoDh3w,20307
16
- fenix_mcp/domain/productivity.py,sha256=nmHRuVJGRRu1s4eMoAv8vXHKsSauCPl-FvFx3I_yCTE,6661
15
+ fenix_mcp/domain/knowledge.py,sha256=FmE3mGgu9jxr4fDKmBGdUBJ6KWiaVzqtmVu4ZXseXlE,20436
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=E6TW6hvF8xPGM0HGIumnv72TmCqqkoCoDIjnYwqGFnQ,27514
22
+ fenix_mcp/infrastructure/fenix_api/client.py,sha256=z5S6cwBxerpaDXj4Y4LWZEd7ZuGcVmgcaXQv3tTFPBs,28038
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.0.0.dist-info/METADATA,sha256=ALncgS5QZiHFpvsnArmA1mXYeU0fn9uiQ9-MNGWPmF0,7260
26
- fenix_mcp-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- fenix_mcp-1.0.0.dist-info/entry_points.txt,sha256=o52x_YHBupEd-1Z1GSfUjv3gJrx5_I-EkHhCgt1WBaE,49
28
- fenix_mcp-1.0.0.dist-info/top_level.txt,sha256=2G1UtKpwjaIGQyE7sRoHecxaGLeuexfjrOUjv9DDKh4,10
29
- fenix_mcp-1.0.0.dist-info/RECORD,,
25
+ fenix_mcp-1.2.0.dist-info/METADATA,sha256=1nFk9q9nLtzlw-qx6m-pwhL7XaO4StWM8GQykmzRiFw,7260
26
+ fenix_mcp-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ fenix_mcp-1.2.0.dist-info/entry_points.txt,sha256=o52x_YHBupEd-1Z1GSfUjv3gJrx5_I-EkHhCgt1WBaE,49
28
+ fenix_mcp-1.2.0.dist-info/top_level.txt,sha256=2G1UtKpwjaIGQyE7sRoHecxaGLeuexfjrOUjv9DDKh4,10
29
+ fenix_mcp-1.2.0.dist-info/RECORD,,