fenix-mcp 0.1.0__py3-none-any.whl → 0.2.2__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 +4 -1
- fenix_mcp/application/tool_base.py +3 -2
- fenix_mcp/application/tool_registry.py +3 -1
- fenix_mcp/application/tools/health.py +1 -3
- fenix_mcp/application/tools/initialize.py +20 -7
- fenix_mcp/application/tools/intelligence.py +47 -15
- fenix_mcp/application/tools/knowledge.py +162 -50
- fenix_mcp/application/tools/productivity.py +26 -9
- fenix_mcp/application/tools/user_config.py +18 -9
- fenix_mcp/domain/initialization.py +22 -22
- fenix_mcp/domain/intelligence.py +27 -15
- fenix_mcp/domain/knowledge.py +161 -61
- fenix_mcp/domain/productivity.py +3 -2
- fenix_mcp/domain/user_config.py +16 -7
- fenix_mcp/infrastructure/config.py +6 -2
- fenix_mcp/infrastructure/context.py +0 -1
- fenix_mcp/infrastructure/fenix_api/client.py +118 -38
- fenix_mcp/infrastructure/http_client.py +1 -1
- fenix_mcp/interface/mcp_server.py +0 -3
- fenix_mcp/interface/transports.py +19 -10
- fenix_mcp/main.py +4 -1
- {fenix_mcp-0.1.0.dist-info → fenix_mcp-0.2.2.dist-info}/METADATA +56 -6
- fenix_mcp-0.2.2.dist-info/RECORD +29 -0
- fenix_mcp-0.1.0.dist-info/RECORD +0 -29
- {fenix_mcp-0.1.0.dist-info → fenix_mcp-0.2.2.dist-info}/WHEEL +0 -0
- {fenix_mcp-0.1.0.dist-info → fenix_mcp-0.2.2.dist-info}/entry_points.txt +0 -0
- {fenix_mcp-0.1.0.dist-info → fenix_mcp-0.2.2.dist-info}/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import Any, Dict,
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
8
|
|
|
9
9
|
from pydantic import Field
|
|
10
10
|
|
|
@@ -51,24 +51,36 @@ class TodoAction(str, Enum):
|
|
|
51
51
|
|
|
52
52
|
ACTION_FIELD_DESCRIPTION = (
|
|
53
53
|
"Ação de produtividade (TODO). Escolha um dos valores: "
|
|
54
|
-
+ ", ".join(
|
|
54
|
+
+ ", ".join(
|
|
55
|
+
f"`{member.value}` ({member.description.rstrip('.')})." for member in TodoAction
|
|
56
|
+
)
|
|
55
57
|
)
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
class ProductivityRequest(ToolRequest):
|
|
59
61
|
action: TodoAction = Field(description=ACTION_FIELD_DESCRIPTION)
|
|
60
62
|
id: Optional[str] = Field(default=None, description="Identificador do item TODO.")
|
|
61
|
-
title: Optional[str] = Field(
|
|
62
|
-
|
|
63
|
+
title: Optional[str] = Field(
|
|
64
|
+
default=None, description="Título do TODO (obrigatório em create)."
|
|
65
|
+
)
|
|
66
|
+
content: Optional[str] = Field(
|
|
67
|
+
default=None, description="Conteúdo em Markdown (obrigatório em create)."
|
|
68
|
+
)
|
|
63
69
|
status: Optional[str] = Field(default="pending", description="Status do TODO.")
|
|
64
70
|
priority: Optional[str] = Field(default="medium", description="Prioridade do TODO.")
|
|
65
71
|
category: Optional[str] = Field(default=None, description="Categoria opcional.")
|
|
66
72
|
tags: Optional[List[str]] = Field(default=None, description="Lista de tags.")
|
|
67
|
-
due_date: Optional[str] = Field(
|
|
68
|
-
|
|
73
|
+
due_date: Optional[str] = Field(
|
|
74
|
+
default=None, description="Data de vencimento do TODO (ISO)."
|
|
75
|
+
)
|
|
76
|
+
limit: int = Field(
|
|
77
|
+
default=20, ge=1, le=100, description="Limite de resultados em list/search."
|
|
78
|
+
)
|
|
69
79
|
offset: int = Field(default=0, ge=0, description="Offset de paginação.")
|
|
70
80
|
query: Optional[str] = Field(default=None, description="Termo de busca.")
|
|
71
|
-
days: Optional[int] = Field(
|
|
81
|
+
days: Optional[int] = Field(
|
|
82
|
+
default=None, ge=1, le=30, description="Janela de dias para upcoming."
|
|
83
|
+
)
|
|
72
84
|
|
|
73
85
|
|
|
74
86
|
class ProductivityTool(Tool):
|
|
@@ -175,7 +187,9 @@ class ProductivityTool(Tool):
|
|
|
175
187
|
async def _handle_search(self, payload: ProductivityRequest):
|
|
176
188
|
if not payload.query:
|
|
177
189
|
return text("❌ Informe um termo de busca (query).")
|
|
178
|
-
todos = await self._service.search(
|
|
190
|
+
todos = await self._service.search(
|
|
191
|
+
payload.query, limit=payload.limit, offset=payload.offset
|
|
192
|
+
)
|
|
179
193
|
if not todos:
|
|
180
194
|
return text("🔍 Nenhum TODO encontrado para a busca.")
|
|
181
195
|
body = "\n\n".join(ProductivityService.format_todo(todo) for todo in todos)
|
|
@@ -213,7 +227,10 @@ class ProductivityTool(Tool):
|
|
|
213
227
|
return text(f"🔖 **Tags utilizadas:**\n{body}")
|
|
214
228
|
|
|
215
229
|
async def _handle_help(self):
|
|
216
|
-
return text(
|
|
230
|
+
return text(
|
|
231
|
+
"📚 **Ações disponíveis para productivity**\n\n"
|
|
232
|
+
+ TodoAction.formatted_help()
|
|
233
|
+
)
|
|
217
234
|
|
|
218
235
|
@staticmethod
|
|
219
236
|
def _format_single(todo: Dict[str, Any], *, header: str) -> str:
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import Dict, List, Optional
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
8
|
|
|
9
9
|
from pydantic import Field
|
|
10
10
|
|
|
@@ -43,9 +43,9 @@ class UserConfigAction(str, Enum):
|
|
|
43
43
|
return "\n".join(lines)
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
ACTION_FIELD_DESCRIPTION = (
|
|
47
|
-
"
|
|
48
|
-
|
|
46
|
+
ACTION_FIELD_DESCRIPTION = "Ação a executar. Escolha um dos valores: " + ", ".join(
|
|
47
|
+
f"`{member.value}` ({member.description.rstrip('.')})."
|
|
48
|
+
for member in UserConfigAction
|
|
49
49
|
)
|
|
50
50
|
|
|
51
51
|
|
|
@@ -53,12 +53,18 @@ class UserConfigRequest(ToolRequest):
|
|
|
53
53
|
action: UserConfigAction = Field(description=ACTION_FIELD_DESCRIPTION)
|
|
54
54
|
id: Optional[str] = Field(default=None, description="ID do documento.")
|
|
55
55
|
name: Optional[str] = Field(default=None, description="Nome do documento.")
|
|
56
|
-
content: Optional[str] = Field(
|
|
56
|
+
content: Optional[str] = Field(
|
|
57
|
+
default=None, description="Conteúdo em Markdown/JSON."
|
|
58
|
+
)
|
|
57
59
|
mode_id: Optional[str] = Field(default=None, description="ID do modo associado.")
|
|
58
|
-
is_default: Optional[bool] = Field(
|
|
60
|
+
is_default: Optional[bool] = Field(
|
|
61
|
+
default=None, description="Marca o documento como padrão."
|
|
62
|
+
)
|
|
59
63
|
limit: int = Field(default=20, ge=1, le=100, description="Limite para listagem.")
|
|
60
64
|
offset: int = Field(default=0, ge=0, description="Offset para listagem.")
|
|
61
|
-
return_content: Optional[bool] = Field(
|
|
65
|
+
return_content: Optional[bool] = Field(
|
|
66
|
+
default=None, description="Retorna conteúdo completo."
|
|
67
|
+
)
|
|
62
68
|
|
|
63
69
|
|
|
64
70
|
class UserConfigTool(Tool):
|
|
@@ -125,7 +131,7 @@ class UserConfigTool(Tool):
|
|
|
125
131
|
|
|
126
132
|
if action is UserConfigAction.DELETE:
|
|
127
133
|
if not payload.id:
|
|
128
|
-
return text("❌ Informe o ID do documento."
|
|
134
|
+
return text("❌ Informe o ID do documento.")
|
|
129
135
|
await self._service.delete(payload.id)
|
|
130
136
|
return text(f"🗑️ Documento {payload.id} removido.")
|
|
131
137
|
|
|
@@ -135,7 +141,10 @@ class UserConfigTool(Tool):
|
|
|
135
141
|
)
|
|
136
142
|
|
|
137
143
|
async def _handle_help(self):
|
|
138
|
-
return text(
|
|
144
|
+
return text(
|
|
145
|
+
"📚 **Ações disponíveis para user_config**\n\n"
|
|
146
|
+
+ UserConfigAction.formatted_help()
|
|
147
|
+
)
|
|
139
148
|
|
|
140
149
|
|
|
141
150
|
def _format_doc(doc: Dict[str, Any], header: Optional[str] = None) -> str:
|
|
@@ -25,7 +25,9 @@ class InitializationService:
|
|
|
25
25
|
self._api = api_client
|
|
26
26
|
self._logger = logger
|
|
27
27
|
|
|
28
|
-
async def gather_data(
|
|
28
|
+
async def gather_data(
|
|
29
|
+
self, *, include_user_docs: bool, limit: int
|
|
30
|
+
) -> InitializationData:
|
|
29
31
|
profile = await self._safe_call(self._api.get_profile)
|
|
30
32
|
core_docs = await self._safe_call(
|
|
31
33
|
self._api.list_core_documents,
|
|
@@ -35,29 +37,20 @@ class InitializationService:
|
|
|
35
37
|
self._logger.debug("Core docs response", extra={"core_docs": core_docs})
|
|
36
38
|
user_docs: List[Dict[str, Any]] = []
|
|
37
39
|
if include_user_docs:
|
|
38
|
-
user_docs =
|
|
39
|
-
self.
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
user_docs = (
|
|
41
|
+
await self._safe_call(
|
|
42
|
+
self._api.list_user_core_documents,
|
|
43
|
+
return_content=True,
|
|
44
|
+
)
|
|
45
|
+
or []
|
|
46
|
+
)
|
|
42
47
|
if self._logger:
|
|
43
48
|
self._logger.debug("User docs response", extra={"user_docs": user_docs})
|
|
44
|
-
memories = await self._safe_call(
|
|
45
|
-
self._api.list_memories,
|
|
46
|
-
include_content=True,
|
|
47
|
-
include_metadata=False,
|
|
48
|
-
limit=3,
|
|
49
|
-
offset=0,
|
|
50
|
-
sortBy="created_at",
|
|
51
|
-
sortOrder="desc",
|
|
52
|
-
)
|
|
53
|
-
if self._logger:
|
|
54
|
-
self._logger.debug("Memories response", extra={"memories": memories})
|
|
55
|
-
|
|
56
49
|
return InitializationData(
|
|
57
50
|
profile=profile,
|
|
58
51
|
core_documents=self._extract_items(core_docs, "coreDocuments"),
|
|
59
52
|
user_documents=self._extract_items(user_docs, "userCoreDocuments"),
|
|
60
|
-
recent_memories=
|
|
53
|
+
recent_memories=[],
|
|
61
54
|
)
|
|
62
55
|
|
|
63
56
|
async def _safe_call(self, func, *args, **kwargs):
|
|
@@ -80,7 +73,9 @@ class InitializationService:
|
|
|
80
73
|
return []
|
|
81
74
|
|
|
82
75
|
@staticmethod
|
|
83
|
-
def build_existing_user_summary(
|
|
76
|
+
def build_existing_user_summary(
|
|
77
|
+
data: InitializationData, include_user_docs: bool
|
|
78
|
+
) -> str:
|
|
84
79
|
profile = data.profile or {}
|
|
85
80
|
user_info = profile.get("user") or {}
|
|
86
81
|
tenant_info = profile.get("tenant") or {}
|
|
@@ -109,16 +104,21 @@ class InitializationService:
|
|
|
109
104
|
|
|
110
105
|
if core_count:
|
|
111
106
|
preview = ", ".join(
|
|
112
|
-
doc.get("name", doc.get("title", "sem título"))
|
|
107
|
+
doc.get("name", doc.get("title", "sem título"))
|
|
108
|
+
for doc in data.core_documents[:5]
|
|
113
109
|
)
|
|
114
110
|
lines.append(f"- Exemplos de documentos principais: {preview}")
|
|
115
111
|
|
|
116
112
|
if include_user_docs and user_count:
|
|
117
|
-
preview = ", ".join(
|
|
113
|
+
preview = ", ".join(
|
|
114
|
+
doc.get("name", "sem título") for doc in data.user_documents[:5]
|
|
115
|
+
)
|
|
118
116
|
lines.append(f"- Exemplos de documentos pessoais: {preview}")
|
|
119
117
|
|
|
120
118
|
if memories_count:
|
|
121
|
-
preview = ", ".join(
|
|
119
|
+
preview = ", ".join(
|
|
120
|
+
mem.get("title", "sem título") for mem in data.recent_memories[:3]
|
|
121
|
+
)
|
|
122
122
|
lines.append(f"- Memórias recentes: {preview}")
|
|
123
123
|
|
|
124
124
|
lines.append("")
|
fenix_mcp/domain/intelligence.py
CHANGED
|
@@ -44,12 +44,15 @@ class IntelligenceService:
|
|
|
44
44
|
params = _strip_none(filters)
|
|
45
45
|
include_content = bool(params.pop("content", True))
|
|
46
46
|
include_metadata = bool(params.pop("metadata", False))
|
|
47
|
-
return
|
|
48
|
-
self.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
return (
|
|
48
|
+
await self._call(
|
|
49
|
+
self.api.list_memories,
|
|
50
|
+
include_content=include_content,
|
|
51
|
+
include_metadata=include_metadata,
|
|
52
|
+
**params,
|
|
53
|
+
)
|
|
54
|
+
or []
|
|
55
|
+
)
|
|
53
56
|
|
|
54
57
|
async def similar_memories(
|
|
55
58
|
self, *, content: str, threshold: float, max_results: int
|
|
@@ -58,7 +61,9 @@ class IntelligenceService:
|
|
|
58
61
|
"content": content,
|
|
59
62
|
"threshold": threshold,
|
|
60
63
|
}
|
|
61
|
-
result =
|
|
64
|
+
result = (
|
|
65
|
+
await self._call(self.api.find_similar_memories, _strip_none(payload)) or []
|
|
66
|
+
)
|
|
62
67
|
if isinstance(result, list) and max_results:
|
|
63
68
|
return result[:max_results]
|
|
64
69
|
return result
|
|
@@ -78,15 +83,20 @@ class IntelligenceService:
|
|
|
78
83
|
"sortBy": "priority_score",
|
|
79
84
|
"sortOrder": "desc",
|
|
80
85
|
}
|
|
81
|
-
return
|
|
82
|
-
self.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
return (
|
|
87
|
+
await self._call(
|
|
88
|
+
self.api.list_memories,
|
|
89
|
+
include_content=False,
|
|
90
|
+
include_metadata=False,
|
|
91
|
+
**params,
|
|
92
|
+
)
|
|
93
|
+
or []
|
|
94
|
+
)
|
|
87
95
|
|
|
88
96
|
async def analytics(self, *, time_range: str, group_by: str) -> Dict[str, Any]:
|
|
89
|
-
memories = await self.query_memories(
|
|
97
|
+
memories = await self.query_memories(
|
|
98
|
+
limit=200, timeRange=time_range, groupBy=group_by
|
|
99
|
+
)
|
|
90
100
|
summary: Dict[str, Any] = {
|
|
91
101
|
"total_memories": len(memories),
|
|
92
102
|
"by_group": {},
|
|
@@ -100,7 +110,9 @@ class IntelligenceService:
|
|
|
100
110
|
async def update_memory(self, memory_id: str, **fields: Any) -> Dict[str, Any]:
|
|
101
111
|
payload = _strip_none(fields)
|
|
102
112
|
if "importance" in payload:
|
|
103
|
-
payload["priority_score"] = _importance_to_priority(
|
|
113
|
+
payload["priority_score"] = _importance_to_priority(
|
|
114
|
+
payload.pop("importance")
|
|
115
|
+
)
|
|
104
116
|
mapping = {
|
|
105
117
|
"documentation_item_id": "documentationItemId",
|
|
106
118
|
"mode_id": "modeId",
|