fenix-mcp 1.1.0__tar.gz → 1.3.0__tar.gz

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.
Files changed (34) hide show
  1. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/PKG-INFO +1 -1
  2. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/__init__.py +1 -1
  3. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/application/tools/intelligence.py +25 -11
  4. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/application/tools/knowledge.py +101 -32
  5. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/application/tools/productivity.py +46 -13
  6. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/domain/knowledge.py +17 -10
  7. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/domain/productivity.py +16 -13
  8. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/infrastructure/fenix_api/client.py +22 -3
  9. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp.egg-info/PKG-INFO +1 -1
  10. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/README.md +0 -0
  11. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/application/presenters.py +0 -0
  12. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/application/tool_base.py +0 -0
  13. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/application/tool_registry.py +0 -0
  14. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/application/tools/__init__.py +0 -0
  15. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/application/tools/health.py +0 -0
  16. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/application/tools/initialize.py +0 -0
  17. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/application/tools/user_config.py +0 -0
  18. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/domain/initialization.py +0 -0
  19. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/domain/intelligence.py +0 -0
  20. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/domain/user_config.py +0 -0
  21. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/infrastructure/config.py +0 -0
  22. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/infrastructure/context.py +0 -0
  23. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/infrastructure/http_client.py +0 -0
  24. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/infrastructure/logging.py +0 -0
  25. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/interface/mcp_server.py +0 -0
  26. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/interface/transports.py +0 -0
  27. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp/main.py +0 -0
  28. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp.egg-info/SOURCES.txt +0 -0
  29. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp.egg-info/dependency_links.txt +0 -0
  30. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp.egg-info/entry_points.txt +0 -0
  31. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp.egg-info/requires.txt +0 -0
  32. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/fenix_mcp.egg-info/top_level.txt +0 -0
  33. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/pyproject.toml +0 -0
  34. {fenix_mcp-1.1.0 → fenix_mcp-1.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fenix-mcp
3
- Version: 1.1.0
3
+ Version: 1.3.0
4
4
  Summary: Fênix Cloud MCP server implemented in Python
5
5
  Author: Fenix Inc
6
6
  Requires-Python: >=3.10
@@ -8,4 +8,4 @@ Fênix Cloud MCP Server (Python edition).
8
8
  __all__ = ["__version__"]
9
9
 
10
10
 
11
- __version__ = "1.1.0"
11
+ __version__ = "1.3.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,22 +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
110
+ # Documentation - Navigation workflow: doc_full_tree -> doc_children -> doc_get
111
111
  DOC_CREATE = (
112
112
  "doc_create",
113
113
  "Creates a documentation item. Requires doc_emoji for page, api_doc, and guide types.",
114
114
  )
115
- DOC_LIST = ("doc_list", "Lists documentation items with filters.")
116
- DOC_GET = ("doc_get", "Gets documentation item details.")
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
+ )
117
123
  DOC_UPDATE = ("doc_update", "Updates a documentation item.")
118
124
  DOC_DELETE = ("doc_delete", "Removes a documentation item.")
119
- DOC_SEARCH = ("doc_search", "Searches documentation items by text.")
120
- 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
+ )
121
129
  DOC_RECENT = ("doc_recent", "Lists recently accessed documents.")
122
130
  DOC_ANALYTICS = ("doc_analytics", "Returns document analytics.")
123
- DOC_CHILDREN = ("doc_children", "Lists child documents of an item.")
124
- DOC_TREE = ("doc_tree", "Retrieves the direct tree of a document.")
125
- 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
+ )
126
140
  DOC_MOVE = ("doc_move", "Moves a document to another parent.")
127
141
  DOC_PUBLISH = ("doc_publish", "Changes publication status of a document.")
128
142
  DOC_VERSION = ("doc_version", "Generates or retrieves a document version.")
@@ -387,7 +401,9 @@ class KnowledgeTool(Tool):
387
401
  if not payload.id:
388
402
  return text("❌ Provide the work item ID.")
389
403
  work = await self._service.work_get(payload.id)
390
- return text(_format_work(work, header="🎯 Work item details"))
404
+ return text(
405
+ _format_work(work, header="🎯 Work item details", show_description=True)
406
+ )
391
407
 
392
408
  if action is KnowledgeAction.WORK_UPDATE:
393
409
  if not payload.id:
@@ -431,6 +447,11 @@ class KnowledgeTool(Tool):
431
447
  items = await self._service.work_search(
432
448
  query=query,
433
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,
434
455
  )
435
456
  if not items:
436
457
  return text("🔍 No work items found.")
@@ -658,7 +679,7 @@ class KnowledgeTool(Tool):
658
679
  return_description=payload.return_description,
659
680
  return_metadata=payload.return_metadata,
660
681
  )
661
- return text(_format_mode(mode, header="🎭 Mode details"))
682
+ return text(_format_mode(mode, header="🎭 Mode details", show_content=True))
662
683
 
663
684
  if action is KnowledgeAction.MODE_UPDATE:
664
685
  if not payload.mode_id:
@@ -766,7 +787,7 @@ class KnowledgeTool(Tool):
766
787
  return_metadata=payload.return_metadata,
767
788
  return_modes=payload.return_metadata,
768
789
  )
769
- return text(_format_rule(rule, header="📋 Rule details"))
790
+ return text(_format_rule(rule, header="📋 Rule details", show_content=True))
770
791
 
771
792
  if action is KnowledgeAction.RULE_UPDATE:
772
793
  if not payload.rule_id:
@@ -854,11 +875,10 @@ class KnowledgeTool(Tool):
854
875
  if action is KnowledgeAction.DOC_GET:
855
876
  if not payload.id:
856
877
  return text("❌ Provide the documentation ID.")
857
- doc = await self._service.doc_get(
858
- payload.id,
859
- 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)
860
881
  )
861
- return text(_format_doc(doc, header="📄 Documentation details"))
862
882
 
863
883
  if action is KnowledgeAction.DOC_UPDATE:
864
884
  if not payload.id:
@@ -896,19 +916,6 @@ class KnowledgeTool(Tool):
896
916
  await self._service.doc_delete(payload.id)
897
917
  return text(f"🗑️ Documentation {payload.id} removed.")
898
918
 
899
- if action is KnowledgeAction.DOC_SEARCH:
900
- query = sanitize_null(payload.query)
901
- if not query:
902
- return text("❌ Provide query to search documentation.")
903
- docs = await self._service.doc_search(
904
- query=query,
905
- limit=payload.limit,
906
- )
907
- if not docs:
908
- return text("🔍 No documents found for the specified filters.")
909
- body = "\n\n".join(_format_doc(doc) for doc in docs)
910
- return text(f"🔍 **Results ({len(docs)}):**\n\n{body}")
911
-
912
919
  if action is KnowledgeAction.DOC_ROOTS:
913
920
  docs = await self._service.doc_roots()
914
921
  if not docs:
@@ -1013,13 +1020,34 @@ class KnowledgeTool(Tool):
1013
1020
  )
1014
1021
 
1015
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
+ """
1016
1038
  return text(
1017
1039
  "📚 **Available actions for knowledge**\n\n"
1040
+ + workflow_guide
1018
1041
  + KnowledgeAction.formatted_help()
1019
1042
  )
1020
1043
 
1021
1044
 
1022
- 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:
1023
1051
  lines: List[str] = []
1024
1052
  if header:
1025
1053
  lines.append(header)
@@ -1028,6 +1056,9 @@ def _format_work(item: Dict[str, Any], *, header: Optional[str] = None) -> str:
1028
1056
  # Extract title
1029
1057
  title = item.get("title") or item.get("name") or "Untitled"
1030
1058
 
1059
+ # Extract type
1060
+ item_type = item.get("item_type") or item.get("type") or "unknown"
1061
+
1031
1062
  # Extract status
1032
1063
  status = item.get("status") or item.get("state") or "unknown"
1033
1064
 
@@ -1050,6 +1081,7 @@ def _format_work(item: Dict[str, Any], *, header: Optional[str] = None) -> str:
1050
1081
  [
1051
1082
  f"🎯 **{title}**",
1052
1083
  f"ID: {item_id}",
1084
+ f"Type: {item_type}",
1053
1085
  f"Status: {status}",
1054
1086
  f"Priority: {priority}",
1055
1087
  f"Assignee: {assignee}",
@@ -1063,6 +1095,13 @@ def _format_work(item: Dict[str, Any], *, header: Optional[str] = None) -> str:
1063
1095
  tags = item.get("tags", [])
1064
1096
  if tags:
1065
1097
  lines.append(f"Tags: {', '.join(tags)}")
1098
+
1099
+ # Show description only when explicitly requested (e.g., work_get)
1100
+ if show_description and item.get("description"):
1101
+ lines.append("")
1102
+ lines.append("**Description:**")
1103
+ lines.append(item.get("description"))
1104
+
1066
1105
  return "\n".join(lines)
1067
1106
 
1068
1107
 
@@ -1106,7 +1145,12 @@ def _format_sprint(sprint: Dict[str, Any], header: Optional[str] = None) -> str:
1106
1145
  return "\n".join(lines)
1107
1146
 
1108
1147
 
1109
- def _format_mode(mode: Dict[str, Any], header: Optional[str] = None) -> str:
1148
+ def _format_mode(
1149
+ mode: Dict[str, Any],
1150
+ *,
1151
+ header: Optional[str] = None,
1152
+ show_content: bool = False,
1153
+ ) -> str:
1110
1154
  lines: List[str] = []
1111
1155
  if header:
1112
1156
  lines.append(header)
@@ -1120,10 +1164,19 @@ def _format_mode(mode: Dict[str, Any], header: Optional[str] = None) -> str:
1120
1164
  )
1121
1165
  if mode.get("description"):
1122
1166
  lines.append(f"Description: {mode['description']}")
1167
+ if show_content and mode.get("content"):
1168
+ lines.append("")
1169
+ lines.append("**Content:**")
1170
+ lines.append(mode.get("content"))
1123
1171
  return "\n".join(lines)
1124
1172
 
1125
1173
 
1126
- def _format_rule(rule: Dict[str, Any], header: Optional[str] = None) -> str:
1174
+ def _format_rule(
1175
+ rule: Dict[str, Any],
1176
+ *,
1177
+ header: Optional[str] = None,
1178
+ show_content: bool = False,
1179
+ ) -> str:
1127
1180
  lines: List[str] = []
1128
1181
  if header:
1129
1182
  lines.append(header)
@@ -1137,10 +1190,19 @@ def _format_rule(rule: Dict[str, Any], header: Optional[str] = None) -> str:
1137
1190
  )
1138
1191
  if rule.get("description"):
1139
1192
  lines.append(f"Description: {rule['description']}")
1193
+ if show_content and rule.get("content"):
1194
+ lines.append("")
1195
+ lines.append("**Content:**")
1196
+ lines.append(rule.get("content"))
1140
1197
  return "\n".join(lines)
1141
1198
 
1142
1199
 
1143
- def _format_doc(doc: Dict[str, Any], header: Optional[str] = None) -> str:
1200
+ def _format_doc(
1201
+ doc: Dict[str, Any],
1202
+ *,
1203
+ header: Optional[str] = None,
1204
+ show_content: bool = False,
1205
+ ) -> str:
1144
1206
  lines: List[str] = []
1145
1207
  if header:
1146
1208
  lines.append(header)
@@ -1157,6 +1219,13 @@ def _format_doc(doc: Dict[str, Any], header: Optional[str] = None) -> str:
1157
1219
  lines.append(
1158
1220
  f"Updated at: {_format_date(doc.get('updated_at') or doc.get('updatedAt'))}"
1159
1221
  )
1222
+
1223
+ # Show content only when explicitly requested (e.g., doc_get)
1224
+ if show_content and doc.get("content"):
1225
+ lines.append("")
1226
+ lines.append("**Content:**")
1227
+ lines.append(doc.get("content"))
1228
+
1160
1229
  return "\n".join(lines)
1161
1230
 
1162
1231
 
@@ -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.1.0
3
+ Version: 1.3.0
4
4
  Summary: Fênix Cloud MCP server implemented in Python
5
5
  Author: Fenix Inc
6
6
  Requires-Python: >=3.10
File without changes
File without changes
File without changes
File without changes