fenix-mcp 1.1.0__tar.gz → 1.2.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.
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/PKG-INFO +1 -1
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/__init__.py +1 -1
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/application/tools/intelligence.py +25 -11
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/application/tools/knowledge.py +97 -32
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/application/tools/productivity.py +46 -13
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/domain/knowledge.py +17 -10
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/domain/productivity.py +16 -13
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/infrastructure/fenix_api/client.py +22 -3
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp.egg-info/PKG-INFO +1 -1
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/README.md +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/application/presenters.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/application/tool_base.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/application/tool_registry.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/application/tools/__init__.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/application/tools/health.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/application/tools/initialize.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/application/tools/user_config.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/domain/initialization.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/domain/intelligence.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/domain/user_config.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/infrastructure/config.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/infrastructure/context.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/infrastructure/http_client.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/infrastructure/logging.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/interface/mcp_server.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/interface/transports.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp/main.py +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp.egg-info/SOURCES.txt +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp.egg-info/dependency_links.txt +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp.egg-info/entry_points.txt +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp.egg-info/requires.txt +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/fenix_mcp.egg-info/top_level.txt +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/pyproject.toml +0 -0
- {fenix_mcp-1.1.0 → fenix_mcp-1.2.0}/setup.cfg +0 -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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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 = (
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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 = (
|
|
124
|
-
|
|
125
|
-
|
|
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(
|
|
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
|
-
|
|
859
|
-
|
|
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(
|
|
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)
|
|
@@ -1063,6 +1091,13 @@ def _format_work(item: Dict[str, Any], *, header: Optional[str] = None) -> str:
|
|
|
1063
1091
|
tags = item.get("tags", [])
|
|
1064
1092
|
if tags:
|
|
1065
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
|
+
|
|
1066
1101
|
return "\n".join(lines)
|
|
1067
1102
|
|
|
1068
1103
|
|
|
@@ -1106,7 +1141,12 @@ def _format_sprint(sprint: Dict[str, Any], header: Optional[str] = None) -> str:
|
|
|
1106
1141
|
return "\n".join(lines)
|
|
1107
1142
|
|
|
1108
1143
|
|
|
1109
|
-
def _format_mode(
|
|
1144
|
+
def _format_mode(
|
|
1145
|
+
mode: Dict[str, Any],
|
|
1146
|
+
*,
|
|
1147
|
+
header: Optional[str] = None,
|
|
1148
|
+
show_content: bool = False,
|
|
1149
|
+
) -> str:
|
|
1110
1150
|
lines: List[str] = []
|
|
1111
1151
|
if header:
|
|
1112
1152
|
lines.append(header)
|
|
@@ -1120,10 +1160,19 @@ def _format_mode(mode: Dict[str, Any], header: Optional[str] = None) -> str:
|
|
|
1120
1160
|
)
|
|
1121
1161
|
if mode.get("description"):
|
|
1122
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"))
|
|
1123
1167
|
return "\n".join(lines)
|
|
1124
1168
|
|
|
1125
1169
|
|
|
1126
|
-
def _format_rule(
|
|
1170
|
+
def _format_rule(
|
|
1171
|
+
rule: Dict[str, Any],
|
|
1172
|
+
*,
|
|
1173
|
+
header: Optional[str] = None,
|
|
1174
|
+
show_content: bool = False,
|
|
1175
|
+
) -> str:
|
|
1127
1176
|
lines: List[str] = []
|
|
1128
1177
|
if header:
|
|
1129
1178
|
lines.append(header)
|
|
@@ -1137,10 +1186,19 @@ def _format_rule(rule: Dict[str, Any], header: Optional[str] = None) -> str:
|
|
|
1137
1186
|
)
|
|
1138
1187
|
if rule.get("description"):
|
|
1139
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"))
|
|
1140
1193
|
return "\n".join(lines)
|
|
1141
1194
|
|
|
1142
1195
|
|
|
1143
|
-
def _format_doc(
|
|
1196
|
+
def _format_doc(
|
|
1197
|
+
doc: Dict[str, Any],
|
|
1198
|
+
*,
|
|
1199
|
+
header: Optional[str] = None,
|
|
1200
|
+
show_content: bool = False,
|
|
1201
|
+
) -> str:
|
|
1144
1202
|
lines: List[str] = []
|
|
1145
1203
|
if header:
|
|
1146
1204
|
lines.append(header)
|
|
@@ -1157,6 +1215,13 @@ def _format_doc(doc: Dict[str, Any], header: Optional[str] = None) -> str:
|
|
|
1157
1215
|
lines.append(
|
|
1158
1216
|
f"Updated at: {_format_date(doc.get('updated_at') or doc.get('updatedAt'))}"
|
|
1159
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
|
+
|
|
1160
1225
|
return "\n".join(lines)
|
|
1161
1226
|
|
|
1162
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 = (
|
|
35
|
-
|
|
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 (
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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(
|
|
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(
|
|
253
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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 "
|
|
183
|
+
return "not set"
|
|
181
184
|
try:
|
|
182
185
|
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
183
|
-
return dt.strftime("%d
|
|
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(
|
|
505
|
-
|
|
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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|