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 +1 -1
- fenix_mcp/application/tools/intelligence.py +25 -11
- fenix_mcp/application/tools/knowledge.py +111 -34
- fenix_mcp/application/tools/productivity.py +46 -13
- fenix_mcp/domain/knowledge.py +17 -10
- fenix_mcp/domain/productivity.py +16 -13
- fenix_mcp/infrastructure/fenix_api/client.py +22 -3
- {fenix_mcp-1.0.0.dist-info → fenix_mcp-1.2.0.dist-info}/METADATA +1 -1
- {fenix_mcp-1.0.0.dist-info → fenix_mcp-1.2.0.dist-info}/RECORD +12 -12
- {fenix_mcp-1.0.0.dist-info → fenix_mcp-1.2.0.dist-info}/WHEEL +0 -0
- {fenix_mcp-1.0.0.dist-info → fenix_mcp-1.2.0.dist-info}/entry_points.txt +0 -0
- {fenix_mcp-1.0.0.dist-info → fenix_mcp-1.2.0.dist-info}/top_level.txt +0 -0
fenix_mcp/__init__.py
CHANGED
|
@@ -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,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 = (
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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 = (
|
|
121
|
-
|
|
122
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
847
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 = (
|
|
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
|
+
)
|
fenix_mcp/domain/knowledge.py
CHANGED
|
@@ -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 {}
|
fenix_mcp/domain/productivity.py
CHANGED
|
@@ -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:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
fenix_mcp/__init__.py,sha256=
|
|
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=
|
|
10
|
-
fenix_mcp/application/tools/knowledge.py,sha256=
|
|
11
|
-
fenix_mcp/application/tools/productivity.py,sha256=
|
|
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=
|
|
16
|
-
fenix_mcp/domain/productivity.py,sha256=
|
|
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=
|
|
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.
|
|
26
|
-
fenix_mcp-1.
|
|
27
|
-
fenix_mcp-1.
|
|
28
|
-
fenix_mcp-1.
|
|
29
|
-
fenix_mcp-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|