fenix-mcp 0.5.12__tar.gz → 0.6.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 (35) hide show
  1. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/PKG-INFO +1 -1
  2. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/__init__.py +1 -1
  3. fenix_mcp-0.6.0/fenix_mcp/application/tool_base.py +140 -0
  4. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/intelligence.py +50 -52
  5. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/knowledge.py +200 -47
  6. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/productivity.py +26 -10
  7. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/user_config.py +18 -5
  8. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/domain/intelligence.py +3 -30
  9. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp.egg-info/PKG-INFO +1 -1
  10. fenix_mcp-0.5.12/fenix_mcp/application/tool_base.py +0 -47
  11. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/README.md +0 -0
  12. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/presenters.py +0 -0
  13. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tool_registry.py +0 -0
  14. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/__init__.py +0 -0
  15. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/health.py +0 -0
  16. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/initialize.py +0 -0
  17. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/domain/initialization.py +0 -0
  18. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/domain/knowledge.py +0 -0
  19. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/domain/productivity.py +0 -0
  20. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/domain/user_config.py +0 -0
  21. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/infrastructure/config.py +0 -0
  22. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/infrastructure/context.py +0 -0
  23. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/infrastructure/fenix_api/client.py +0 -0
  24. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/infrastructure/http_client.py +0 -0
  25. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/infrastructure/logging.py +0 -0
  26. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/interface/mcp_server.py +0 -0
  27. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/interface/transports.py +0 -0
  28. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/main.py +0 -0
  29. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp.egg-info/SOURCES.txt +0 -0
  30. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp.egg-info/dependency_links.txt +0 -0
  31. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp.egg-info/entry_points.txt +0 -0
  32. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp.egg-info/requires.txt +0 -0
  33. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp.egg-info/top_level.txt +0 -0
  34. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/pyproject.toml +0 -0
  35. {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fenix-mcp
3
- Version: 0.5.12
3
+ Version: 0.6.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__ = "0.5.12"
11
+ __version__ = "0.6.0"
@@ -0,0 +1,140 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Base abstractions for MCP tools."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import Annotated, Any, Dict, Type
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, StringConstraints
10
+
11
+ from fenix_mcp.infrastructure.context import AppContext
12
+
13
+ # =============================================================================
14
+ # Type aliases for common field types - generates proper JSON schema
15
+ # =============================================================================
16
+
17
+ # UUID string - format: uuid, with regex pattern validation
18
+ UUIDStr = Annotated[
19
+ str,
20
+ Field(
21
+ pattern=r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
22
+ json_schema_extra={"format": "uuid"},
23
+ ),
24
+ ]
25
+
26
+ # ISO 8601 date string (YYYY-MM-DD)
27
+ DateStr = Annotated[
28
+ str,
29
+ Field(
30
+ pattern=r"^\d{4}-\d{2}-\d{2}$",
31
+ json_schema_extra={"format": "date"},
32
+ examples=["2025-01-15"],
33
+ ),
34
+ ]
35
+
36
+ # ISO 8601 datetime string (YYYY-MM-DDTHH:MM:SSZ or YYYY-MM-DD)
37
+ DateTimeStr = Annotated[
38
+ str,
39
+ Field(
40
+ pattern=r"^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$",
41
+ json_schema_extra={"format": "date-time"},
42
+ examples=["2025-01-15T10:30:00Z", "2025-01-15"],
43
+ ),
44
+ ]
45
+
46
+ # Markdown content - indicates rich text
47
+ MarkdownStr = Annotated[
48
+ str,
49
+ Field(
50
+ json_schema_extra={"format": "markdown", "contentMediaType": "text/markdown"},
51
+ ),
52
+ ]
53
+
54
+ # Short title/name strings (1-300 chars)
55
+ TitleStr = Annotated[
56
+ str,
57
+ StringConstraints(min_length=1, max_length=300),
58
+ ]
59
+
60
+ # Description strings (up to 1000 chars)
61
+ DescriptionStr = Annotated[
62
+ str,
63
+ StringConstraints(max_length=1000),
64
+ ]
65
+
66
+ # Tag string (lowercase, alphanumeric with hyphens/underscores)
67
+ TagStr = Annotated[
68
+ str,
69
+ Field(
70
+ pattern=r"^[a-zA-Z0-9_-]+$",
71
+ examples=["bug", "feature", "high-priority"],
72
+ ),
73
+ ]
74
+
75
+ # Category strings
76
+ CategoryStr = Annotated[
77
+ str,
78
+ StringConstraints(max_length=100),
79
+ ]
80
+
81
+ # Language code (ISO 639-1)
82
+ LanguageStr = Annotated[
83
+ str,
84
+ Field(
85
+ pattern=r"^[a-z]{2}(-[A-Z]{2})?$",
86
+ examples=["pt", "en", "pt-BR", "en-US"],
87
+ ),
88
+ ]
89
+
90
+ # Version string (semver-like)
91
+ VersionStr = Annotated[
92
+ str,
93
+ Field(
94
+ pattern=r"^[0-9]+(\.[0-9]+)*(-[a-zA-Z0-9]+)?$",
95
+ max_length=20,
96
+ examples=["1.0", "2.1.3", "1.0.0-beta"],
97
+ ),
98
+ ]
99
+
100
+ # Emoji string (single emoji or short code)
101
+ EmojiStr = Annotated[
102
+ str,
103
+ StringConstraints(max_length=10),
104
+ ]
105
+
106
+
107
+ class ToolRequest(BaseModel):
108
+ """Base request payload."""
109
+
110
+ model_config = ConfigDict(extra="forbid")
111
+
112
+
113
+ ToolResponse = Dict[str, Any]
114
+
115
+
116
+ class Tool(ABC):
117
+ """Interface implemented by all tools."""
118
+
119
+ name: str
120
+ description: str
121
+ request_model: Type[ToolRequest] = ToolRequest
122
+
123
+ def schema(self) -> Dict[str, Any]:
124
+ """Return JSON schema describing the tool arguments."""
125
+ return {
126
+ "name": self.name,
127
+ "description": self.description,
128
+ "inputSchema": self.request_model.model_json_schema(),
129
+ }
130
+
131
+ async def execute(
132
+ self, raw_arguments: Dict[str, Any], context: AppContext
133
+ ) -> ToolResponse:
134
+ """Validate raw arguments and run the tool."""
135
+ payload = self.request_model.model_validate(raw_arguments or {})
136
+ return await self.run(payload, context)
137
+
138
+ @abstractmethod
139
+ async def run(self, payload: ToolRequest, context: AppContext) -> ToolResponse:
140
+ """Execute business logic and return a MCP-formatted response."""
@@ -9,7 +9,16 @@ from typing import Any, Dict, List, Optional
9
9
  from pydantic import Field, field_validator
10
10
 
11
11
  from fenix_mcp.application.presenters import text
12
- from fenix_mcp.application.tool_base import Tool, ToolRequest
12
+ from fenix_mcp.application.tool_base import (
13
+ CategoryStr,
14
+ DateTimeStr,
15
+ MarkdownStr,
16
+ TagStr,
17
+ TitleStr,
18
+ Tool,
19
+ ToolRequest,
20
+ UUIDStr,
21
+ )
13
22
  from fenix_mcp.domain.intelligence import IntelligenceService, build_metadata
14
23
  from fenix_mcp.infrastructure.context import AppContext
15
24
 
@@ -31,9 +40,8 @@ class IntelligenceAction(str, Enum):
31
40
  "memory_consolidate",
32
41
  "Consolida múltiplas memórias em uma principal.",
33
42
  )
34
- PRIORITY = ("memory_priority", "Retorna memórias ordenadas por prioridade.")
35
- ANALYTICS = ("memory_analytics", "Calcula métricas e analytics das memórias.")
36
43
  UPDATE = ("memory_update", "Atualiza campos de uma memória existente.")
44
+ DELETE = ("memory_delete", "Remove uma memória pelo ID.")
37
45
  HELP = ("memory_help", "Mostra as ações suportadas e seus usos.")
38
46
 
39
47
  @classmethod
@@ -62,18 +70,19 @@ ACTION_FIELD_DESCRIPTION = (
62
70
 
63
71
  class IntelligenceRequest(ToolRequest):
64
72
  action: IntelligenceAction = Field(description=ACTION_FIELD_DESCRIPTION)
65
- title: Optional[str] = Field(default=None, description="Título da memória.")
66
- content: Optional[str] = Field(
67
- default=None, description="Conteúdo/texto da memória."
73
+ title: Optional[TitleStr] = Field(default=None, description="Título da memória.")
74
+ content: Optional[MarkdownStr] = Field(
75
+ default=None, description="Conteúdo/texto da memória (Markdown)."
68
76
  )
69
- metadata: Optional[str] = Field(
77
+ metadata: Optional[MarkdownStr] = Field(
70
78
  default=None,
71
79
  description="Metadata estruturada da memória (formato pipe, toml compacto, etc.).",
72
80
  )
73
81
  context: Optional[str] = Field(default=None, description="Contexto adicional.")
74
82
  source: Optional[str] = Field(default=None, description="Fonte da memória.")
75
83
  importance: Optional[str] = Field(
76
- default=None, description="Nível de importância da memória."
84
+ default=None,
85
+ description="Nível de importância da memória (low, medium, high, critical).",
77
86
  )
78
87
  include_content: bool = Field(
79
88
  default=False,
@@ -83,7 +92,7 @@ class IntelligenceRequest(ToolRequest):
83
92
  default=False,
84
93
  description="Retornar metadata completa das memórias? Defina true para incluir o campo bruto.",
85
94
  )
86
- tags: Optional[List[str]] = Field(
95
+ tags: Optional[List[TagStr]] = Field(
87
96
  default=None,
88
97
  description="Tags da memória como array JSON de strings.",
89
98
  json_schema_extra={"example": ["tag1", "tag2", "tag3"]},
@@ -120,34 +129,44 @@ class IntelligenceRequest(ToolRequest):
120
129
  limit: int = Field(default=20, ge=1, le=100, description="Limite de resultados.")
121
130
  offset: int = Field(default=0, ge=0, description="Offset para paginação.")
122
131
  query: Optional[str] = Field(default=None, description="Termo de busca.")
123
- category: Optional[str] = Field(default=None, description="Categoria para filtro.")
124
- date_from: Optional[str] = Field(default=None, description="Filtro inicial (ISO).")
125
- date_to: Optional[str] = Field(default=None, description="Filtro final (ISO).")
132
+ category: Optional[CategoryStr] = Field(
133
+ default=None, description="Categoria para filtro."
134
+ )
135
+ date_from: Optional[DateTimeStr] = Field(
136
+ default=None, description="Filtro inicial (ISO 8601)."
137
+ )
138
+ date_to: Optional[DateTimeStr] = Field(
139
+ default=None, description="Filtro final (ISO 8601)."
140
+ )
126
141
  threshold: float = Field(
127
142
  default=0.8, ge=0, le=1, description="Limite mínimo de similaridade."
128
143
  )
129
144
  max_results: int = Field(
130
145
  default=5, ge=1, le=20, description="Máximo de memórias similares."
131
146
  )
132
- memory_ids: Optional[List[str]] = Field(
133
- default=None, description="IDs para consolidação."
147
+ memory_ids: Optional[List[UUIDStr]] = Field(
148
+ default=None, description="IDs das memórias para consolidação (UUIDs)."
134
149
  )
135
150
  strategy: str = Field(default="merge", description="Estratégia de consolidação.")
136
151
  time_range: str = Field(
137
152
  default="month", description="Janela de tempo para analytics."
138
153
  )
139
154
  group_by: str = Field(default="category", description="Agrupamento para analytics.")
140
- id: Optional[str] = Field(default=None, description="ID da memória para update.")
141
- documentation_item_id: Optional[str] = Field(
142
- default=None, description="ID de documentação relacionada."
155
+ id: Optional[UUIDStr] = Field(default=None, description="ID da memória (UUID).")
156
+ documentation_item_id: Optional[UUIDStr] = Field(
157
+ default=None, description="ID de documentação relacionada (UUID)."
143
158
  )
144
- mode_id: Optional[str] = Field(default=None, description="ID do modo relacionado.")
145
- rule_id: Optional[str] = Field(default=None, description="ID da regra relacionada.")
146
- work_item_id: Optional[str] = Field(
147
- default=None, description="ID do work item relacionado."
159
+ mode_id: Optional[UUIDStr] = Field(
160
+ default=None, description="ID do modo relacionado (UUID)."
148
161
  )
149
- sprint_id: Optional[str] = Field(
150
- default=None, description="ID do sprint relacionado."
162
+ rule_id: Optional[UUIDStr] = Field(
163
+ default=None, description="ID da regra relacionada (UUID)."
164
+ )
165
+ work_item_id: Optional[UUIDStr] = Field(
166
+ default=None, description="ID do work item relacionado (UUID)."
167
+ )
168
+ sprint_id: Optional[UUIDStr] = Field(
169
+ default=None, description="ID do sprint relacionado (UUID)."
151
170
  )
152
171
 
153
172
 
@@ -174,12 +193,10 @@ class IntelligenceTool(Tool):
174
193
  return await self._handle_similarity(payload)
175
194
  if action is IntelligenceAction.CONSOLIDATE:
176
195
  return await self._handle_consolidate(payload)
177
- if action is IntelligenceAction.PRIORITY:
178
- return await self._handle_priority(payload)
179
- if action is IntelligenceAction.ANALYTICS:
180
- return await self._handle_analytics(payload)
181
196
  if action is IntelligenceAction.UPDATE:
182
197
  return await self._handle_update(payload)
198
+ if action is IntelligenceAction.DELETE:
199
+ return await self._handle_delete(payload)
183
200
  return text(
184
201
  "❌ Ação inválida para intelligence.\n\nEscolha um dos valores:\n"
185
202
  + "\n".join(f"- `{value}`" for value in IntelligenceAction.choices())
@@ -276,31 +293,6 @@ class IntelligenceTool(Tool):
276
293
  ]
277
294
  return text("\n".join(lines))
278
295
 
279
- async def _handle_priority(self, payload: IntelligenceRequest):
280
- memories = await self._service.priority_memories(limit=payload.limit)
281
- if not memories:
282
- return text("✅ Nenhuma memória prioritária no momento.")
283
- body = "\n\n".join(_format_memory(mem) for mem in memories)
284
- return text(f"🧠 **Memórias prioritárias ({len(memories)}):**\n\n{body}")
285
-
286
- async def _handle_analytics(self, payload: IntelligenceRequest):
287
- analytics = await self._service.analytics(
288
- time_range=payload.time_range,
289
- group_by=payload.group_by,
290
- )
291
- lines = [
292
- "📊 **Memória - Analytics**",
293
- f"Total: {analytics.get('total_memories', 0)}",
294
- f"Novas: {analytics.get('new_memories', 0)}",
295
- f"Mais acessada: {analytics.get('most_accessed', 'N/A')}",
296
- f"Acesso médio: {analytics.get('avg_access_count', 'N/A')}",
297
- ]
298
- by_group = analytics.get("by_group")
299
- if isinstance(by_group, dict):
300
- lines.append("\nPor grupo:")
301
- lines.extend(f"- {key}: {value}" for key, value in by_group.items())
302
- return text("\n".join(lines))
303
-
304
296
  async def _handle_update(self, payload: IntelligenceRequest):
305
297
  if not payload.id:
306
298
  return text("❌ Informe o ID da memória para atualização.")
@@ -342,6 +334,12 @@ class IntelligenceTool(Tool):
342
334
  )
343
335
  )
344
336
 
337
+ async def _handle_delete(self, payload: IntelligenceRequest):
338
+ if not payload.id:
339
+ return text("❌ Informe o ID da memória para remover.")
340
+ await self._service.delete_memory(payload.id)
341
+ return text(f"🗑️ Memória {payload.id} removida com sucesso.")
342
+
345
343
  async def _handle_help(self):
346
344
  return text(
347
345
  "📚 **Ações disponíveis para intelligence**\n\n"
@@ -9,7 +9,20 @@ from typing import Any, Dict, List, Optional
9
9
  from pydantic import Field
10
10
 
11
11
  from fenix_mcp.application.presenters import text
12
- from fenix_mcp.application.tool_base import Tool, ToolRequest
12
+ from fenix_mcp.application.tool_base import (
13
+ CategoryStr,
14
+ DateTimeStr,
15
+ DescriptionStr,
16
+ EmojiStr,
17
+ LanguageStr,
18
+ MarkdownStr,
19
+ TagStr,
20
+ TitleStr,
21
+ Tool,
22
+ ToolRequest,
23
+ UUIDStr,
24
+ VersionStr,
25
+ )
13
26
  from fenix_mcp.domain.knowledge import KnowledgeService, _format_date
14
27
  from fenix_mcp.infrastructure.context import AppContext
15
28
 
@@ -41,6 +54,16 @@ class KnowledgeAction(str, Enum):
41
54
  WORK_ANALYTICS = ("work_analytics", "Retorna métricas consolidadas de work items.")
42
55
  WORK_BY_BOARD = ("work_by_board", "Lista work items associados a um board.")
43
56
  WORK_BY_SPRINT = ("work_by_sprint", "Lista work items associados a um sprint.")
57
+ WORK_BY_EPIC = ("work_by_epic", "Lista work items associados a um épico.")
58
+ WORK_CHILDREN = ("work_children", "Lista work items filhos de um item pai.")
59
+ WORK_STATUS_UPDATE = (
60
+ "work_status_update",
61
+ "Atualiza apenas o status de um work item.",
62
+ )
63
+ WORK_ASSIGN_SPRINT = (
64
+ "work_assign_sprint",
65
+ "Atribui work items a um sprint.",
66
+ )
44
67
 
45
68
  # Boards
46
69
  BOARD_LIST = ("board_list", "Lista boards disponíveis com filtros opcionais.")
@@ -130,19 +153,25 @@ _ALLOWED_DOC_TYPES = {
130
153
 
131
154
  class KnowledgeRequest(ToolRequest):
132
155
  action: KnowledgeAction = Field(description=ACTION_FIELD_DESCRIPTION)
133
- id: Optional[str] = Field(default=None, description="ID principal do recurso.")
156
+ id: Optional[UUIDStr] = Field(
157
+ default=None, description="ID principal do recurso (UUID)."
158
+ )
134
159
  limit: int = Field(default=20, ge=1, le=100, description="Limite de resultados.")
135
160
  offset: int = Field(
136
161
  default=0, ge=0, description="Offset de paginação (quando suportado)."
137
162
  )
138
- team_id: Optional[str] = Field(
139
- default=None, description="ID do time para filtros (boards/sprints/docs)."
163
+ team_id: Optional[UUIDStr] = Field(
164
+ default=None, description="ID do time para filtros (UUID)."
165
+ )
166
+ board_id: Optional[UUIDStr] = Field(
167
+ default=None, description="ID do board associado (UUID)."
168
+ )
169
+ sprint_id: Optional[UUIDStr] = Field(
170
+ default=None, description="ID do sprint associado (UUID)."
140
171
  )
141
- board_id: Optional[str] = Field(default=None, description="ID do board associado.")
142
- sprint_id: Optional[str] = Field(
143
- default=None, description="ID do sprint associado."
172
+ epic_id: Optional[UUIDStr] = Field(
173
+ default=None, description="ID do épico associado (UUID)."
144
174
  )
145
- epic_id: Optional[str] = Field(default=None, description="ID do épico associado.")
146
175
  query: Optional[str] = Field(default=None, description="Filtro/busca.")
147
176
  return_content: Optional[bool] = Field(
148
177
  default=None, description="Retorna conteúdo completo."
@@ -155,71 +184,130 @@ class KnowledgeRequest(ToolRequest):
155
184
  )
156
185
 
157
186
  # Work item fields
158
- work_title: Optional[str] = Field(default=None, description="Título do work item.")
159
- work_description: Optional[str] = Field(
160
- default=None, description="Descrição do work item."
187
+ work_title: Optional[TitleStr] = Field(
188
+ default=None, description="Título do work item."
189
+ )
190
+ work_description: Optional[MarkdownStr] = Field(
191
+ default=None, description="Descrição do work item (Markdown)."
192
+ )
193
+ work_type: Optional[str] = Field(
194
+ default="task",
195
+ description="Tipo do work item (epic, feature, story, task, subtask, bug).",
196
+ )
197
+ work_status: Optional[str] = Field(
198
+ default=None,
199
+ description="Status do work item (backlog, todo, in_progress, review, testing, done, cancelled).",
161
200
  )
162
- work_type: Optional[str] = Field(default="task", description="Tipo do work item.")
163
201
  work_priority: Optional[str] = Field(
164
- default=None, description="Prioridade do work item."
202
+ default=None,
203
+ description="Prioridade do work item (critical, high, medium, low).",
204
+ )
205
+ story_points: Optional[int] = Field(
206
+ default=None, ge=0, le=100, description="Story points (0-100)."
207
+ )
208
+ assignee_id: Optional[UUIDStr] = Field(
209
+ default=None, description="ID do responsável (UUID)."
210
+ )
211
+ parent_id: Optional[UUIDStr] = Field(
212
+ default=None, description="ID do item pai (UUID)."
213
+ )
214
+ work_due_date: Optional[DateTimeStr] = Field(
215
+ default=None, description="Data de vencimento do work item (ISO 8601)."
216
+ )
217
+ work_tags: Optional[List[TagStr]] = Field(
218
+ default=None, description="Tags do work item."
219
+ )
220
+ work_item_ids: Optional[List[UUIDStr]] = Field(
221
+ default=None,
222
+ description="Lista de IDs de work items para operações em lote (UUIDs).",
165
223
  )
166
- story_points: Optional[int] = Field(default=None, description="Story points.")
167
- assignee_id: Optional[str] = Field(default=None, description="ID do responsável.")
168
- parent_id: Optional[str] = Field(default=None, description="ID do item pai.")
169
224
 
170
225
  # Mode fields
171
- mode_id: Optional[str] = Field(default=None, description="ID do modo relacionado.")
172
- mode_name: Optional[str] = Field(default=None, description="Nome do modo.")
173
- mode_description: Optional[str] = Field(
226
+ mode_id: Optional[UUIDStr] = Field(
227
+ default=None, description="ID do modo relacionado (UUID)."
228
+ )
229
+ mode_name: Optional[TitleStr] = Field(default=None, description="Nome do modo.")
230
+ mode_description: Optional[DescriptionStr] = Field(
174
231
  default=None, description="Descrição do modo."
175
232
  )
176
- mode_content: Optional[str] = Field(default=None, description="Conteúdo do modo.")
233
+ mode_content: Optional[MarkdownStr] = Field(
234
+ default=None, description="Conteúdo do modo (Markdown)."
235
+ )
177
236
  mode_is_default: Optional[bool] = Field(
178
237
  default=None, description="Indica se o modo é padrão."
179
238
  )
239
+ mode_metadata: Optional[MarkdownStr] = Field(
240
+ default=None, description="Metadata do modo para processamento de IA."
241
+ )
180
242
 
181
243
  # Rule fields
182
- rule_id: Optional[str] = Field(default=None, description="ID da regra relacionada.")
183
- rule_name: Optional[str] = Field(default=None, description="Nome da regra.")
184
- rule_description: Optional[str] = Field(
244
+ rule_id: Optional[UUIDStr] = Field(
245
+ default=None, description="ID da regra relacionada (UUID)."
246
+ )
247
+ rule_name: Optional[TitleStr] = Field(default=None, description="Nome da regra.")
248
+ rule_description: Optional[DescriptionStr] = Field(
185
249
  default=None, description="Descrição da regra."
186
250
  )
187
- rule_content: Optional[str] = Field(default=None, description="Conteúdo da regra.")
251
+ rule_content: Optional[MarkdownStr] = Field(
252
+ default=None, description="Conteúdo da regra (Markdown)."
253
+ )
188
254
  rule_is_default: Optional[bool] = Field(default=None, description="Regra padrão.")
255
+ rule_metadata: Optional[MarkdownStr] = Field(
256
+ default=None, description="Metadata da regra para processamento de IA."
257
+ )
189
258
 
190
259
  # Documentation fields
191
- doc_title: Optional[str] = Field(
260
+ doc_title: Optional[TitleStr] = Field(
192
261
  default=None, description="Título da documentação."
193
262
  )
194
- doc_description: Optional[str] = Field(default=None, description="Descrição.")
195
- doc_content: Optional[str] = Field(
196
- default=None, description="Conteúdo da documentação."
263
+ doc_description: Optional[DescriptionStr] = Field(
264
+ default=None, description="Descrição da documentação."
265
+ )
266
+ doc_content: Optional[MarkdownStr] = Field(
267
+ default=None, description="Conteúdo da documentação (Markdown)."
197
268
  )
198
269
  doc_status: Optional[str] = Field(
199
- default=None, description="Status da documentação."
270
+ default=None,
271
+ description="Status da documentação (draft, review, published, archived).",
272
+ )
273
+ doc_type: Optional[str] = Field(
274
+ default=None, description="Tipo da documentação (folder, page, api_doc, guide)."
275
+ )
276
+ doc_language: Optional[LanguageStr] = Field(
277
+ default=None, description="Idioma da documentação (ex: pt, en, pt-BR)."
200
278
  )
201
- doc_type: Optional[str] = Field(default=None, description="Tipo da documentação.")
202
- doc_language: Optional[str] = Field(
203
- default=None, description="Idioma da documentação."
279
+ doc_parent_id: Optional[UUIDStr] = Field(
280
+ default=None, description="ID do documento pai (UUID)."
204
281
  )
205
- doc_parent_id: Optional[str] = Field(default=None, description="Documento pai.")
206
- doc_team_id: Optional[str] = Field(
207
- default=None, description="Time responsável pela documentação."
282
+ doc_team_id: Optional[UUIDStr] = Field(
283
+ default=None, description="ID do time responsável pela documentação (UUID)."
208
284
  )
209
- doc_owner_id: Optional[str] = Field(default=None, description="ID do dono.")
210
- doc_reviewer_id: Optional[str] = Field(default=None, description="ID do revisor.")
211
- doc_version: Optional[str] = Field(default=None, description="Versão.")
212
- doc_category: Optional[str] = Field(default=None, description="Categoria.")
213
- doc_tags: Optional[List[str]] = Field(default=None, description="Tags.")
285
+ doc_owner_id: Optional[UUIDStr] = Field(
286
+ default=None, description="ID do dono (UUID)."
287
+ )
288
+ doc_reviewer_id: Optional[UUIDStr] = Field(
289
+ default=None, description="ID do revisor (UUID)."
290
+ )
291
+ doc_version: Optional[VersionStr] = Field(
292
+ default=None, description="Versão do documento (ex: 1.0, 2.1.3)."
293
+ )
294
+ doc_category: Optional[CategoryStr] = Field(default=None, description="Categoria.")
295
+ doc_tags: Optional[List[TagStr]] = Field(default=None, description="Tags.")
214
296
  doc_position: Optional[int] = Field(
215
- default=None, description="Posição desejada ao mover documentos."
297
+ default=None, ge=0, description="Posição desejada ao mover documentos."
216
298
  )
217
- doc_emoji: Optional[str] = Field(
299
+ doc_emoji: Optional[EmojiStr] = Field(
218
300
  default=None, description="Emoji exibido junto ao documento."
219
301
  )
220
- doc_emote: Optional[str] = Field(
302
+ doc_emote: Optional[EmojiStr] = Field(
221
303
  default=None, description="Alias para emoji, mantido por compatibilidade."
222
304
  )
305
+ doc_keywords: Optional[List[TagStr]] = Field(
306
+ default=None, description="Palavras-chave para busca."
307
+ )
308
+ doc_is_public: Optional[bool] = Field(
309
+ default=None, description="Se o documento é público."
310
+ )
223
311
 
224
312
 
225
313
  class KnowledgeTool(Tool):
@@ -260,17 +348,23 @@ class KnowledgeTool(Tool):
260
348
  if action is KnowledgeAction.WORK_CREATE:
261
349
  if not payload.work_title:
262
350
  return text("❌ Informe work_title para criar o item.")
351
+ if not payload.team_id:
352
+ return text("❌ Informe team_id para criar o item.")
263
353
  work = await self._service.work_create(
264
354
  {
265
355
  "title": payload.work_title,
266
356
  "description": payload.work_description,
267
357
  "item_type": payload.work_type,
358
+ "status": payload.work_status,
268
359
  "priority": payload.work_priority,
269
360
  "story_points": payload.story_points,
270
361
  "assignee_id": payload.assignee_id,
271
362
  "sprint_id": payload.sprint_id,
272
363
  "board_id": payload.board_id,
273
364
  "parent_id": payload.parent_id,
365
+ "team_id": payload.team_id,
366
+ "due_date": payload.work_due_date,
367
+ "tags": payload.work_tags,
274
368
  }
275
369
  )
276
370
  return text(_format_work(work, header="✅ Work item criado"))
@@ -305,12 +399,16 @@ class KnowledgeTool(Tool):
305
399
  "title": payload.work_title,
306
400
  "description": payload.work_description,
307
401
  "item_type": payload.work_type,
402
+ "status": payload.work_status,
308
403
  "priority": payload.work_priority,
309
404
  "story_points": payload.story_points,
310
405
  "assignee_id": payload.assignee_id,
311
406
  "sprint_id": payload.sprint_id,
312
407
  "board_id": payload.board_id,
313
408
  "parent_id": payload.parent_id,
409
+ "team_id": payload.team_id,
410
+ "due_date": payload.work_due_date,
411
+ "tags": payload.work_tags,
314
412
  },
315
413
  )
316
414
  return text(_format_work(work, header="✅ Work item atualizado"))
@@ -370,6 +468,49 @@ class KnowledgeTool(Tool):
370
468
  body = "\n\n".join(_format_work(item) for item in items)
371
469
  return text(f"🏃 **Work items do sprint ({len(items)}):**\n\n{body}")
372
470
 
471
+ if action is KnowledgeAction.WORK_BY_EPIC:
472
+ if not payload.epic_id:
473
+ return text("❌ Informe epic_id para listar os itens.")
474
+ items = await self._service.work_by_epic(epic_id=payload.epic_id)
475
+ if not items:
476
+ return text("📦 Nenhum item vinculado ao épico informado.")
477
+ body = "\n\n".join(_format_work(item) for item in items)
478
+ return text(f"📦 **Work items do épico ({len(items)}):**\n\n{body}")
479
+
480
+ if action is KnowledgeAction.WORK_CHILDREN:
481
+ if not payload.id:
482
+ return text("❌ Informe o ID do work item pai.")
483
+ items = await self._service.work_children(payload.id)
484
+ if not items:
485
+ return text("👶 Nenhum item filho encontrado.")
486
+ body = "\n\n".join(_format_work(item) for item in items)
487
+ return text(f"👶 **Work items filhos ({len(items)}):**\n\n{body}")
488
+
489
+ if action is KnowledgeAction.WORK_STATUS_UPDATE:
490
+ if not payload.id:
491
+ return text("❌ Informe o ID do work item.")
492
+ if not payload.work_status:
493
+ return text("❌ Informe work_status para atualizar.")
494
+ work = await self._service.work_update_status(
495
+ payload.id,
496
+ {"status": payload.work_status},
497
+ )
498
+ return text(_format_work(work, header="✅ Status atualizado"))
499
+
500
+ if action is KnowledgeAction.WORK_ASSIGN_SPRINT:
501
+ if not payload.sprint_id:
502
+ return text("❌ Informe sprint_id para atribuir os itens.")
503
+ if not payload.work_item_ids:
504
+ return text("❌ Informe work_item_ids com a lista de IDs.")
505
+ await self._service.work_assign_to_sprint(
506
+ {
507
+ "sprint_id": payload.sprint_id,
508
+ "work_item_ids": payload.work_item_ids,
509
+ }
510
+ )
511
+ count = len(payload.work_item_ids)
512
+ return text(f"✅ {count} work item(s) atribuído(s) ao sprint.")
513
+
373
514
  return text(
374
515
  "❌ Ação de work item não suportada.\n\nEscolha um dos valores:\n"
375
516
  + "\n".join(
@@ -505,6 +646,7 @@ class KnowledgeTool(Tool):
505
646
  "description": payload.mode_description,
506
647
  "content": payload.mode_content,
507
648
  "is_default": payload.mode_is_default,
649
+ "metadata": payload.mode_metadata,
508
650
  }
509
651
  )
510
652
  return text(_format_mode(mode, header="✅ Modo criado"))
@@ -540,6 +682,7 @@ class KnowledgeTool(Tool):
540
682
  "description": payload.mode_description,
541
683
  "content": payload.mode_content,
542
684
  "is_default": payload.mode_is_default,
685
+ "metadata": payload.mode_metadata,
543
686
  },
544
687
  )
545
688
  return text(_format_mode(mode, header="✅ Modo atualizado"))
@@ -610,6 +753,7 @@ class KnowledgeTool(Tool):
610
753
  "description": payload.rule_description,
611
754
  "content": payload.rule_content,
612
755
  "is_default": payload.rule_is_default,
756
+ "metadata": payload.rule_metadata,
613
757
  }
614
758
  )
615
759
  return text(_format_rule(rule, header="✅ Regra criada"))
@@ -646,6 +790,7 @@ class KnowledgeTool(Tool):
646
790
  "description": payload.rule_description,
647
791
  "content": payload.rule_content,
648
792
  "is_default": payload.rule_is_default,
793
+ "metadata": payload.rule_metadata,
649
794
  },
650
795
  )
651
796
  return text(_format_rule(rule, header="✅ Regra atualizada"))
@@ -688,12 +833,14 @@ class KnowledgeTool(Tool):
688
833
  "language": payload.doc_language,
689
834
  "parent_id": payload.doc_parent_id,
690
835
  "team_id": payload.doc_team_id or payload.team_id,
691
- "owner_id": payload.doc_owner_id,
692
- "reviewer_id": payload.doc_reviewer_id,
836
+ "owner_user_id": payload.doc_owner_id,
837
+ "reviewer_user_id": payload.doc_reviewer_id,
693
838
  "version": payload.doc_version,
694
839
  "category": payload.doc_category,
695
840
  "tags": payload.doc_tags,
696
841
  "emoji": payload.doc_emoji or payload.doc_emote,
842
+ "keywords": payload.doc_keywords,
843
+ "is_public": payload.doc_is_public,
697
844
  }
698
845
  )
699
846
  return text(_format_doc(doc, header="✅ Documentação criada"))
@@ -737,12 +884,14 @@ class KnowledgeTool(Tool):
737
884
  "language": payload.doc_language,
738
885
  "parent_id": payload.doc_parent_id,
739
886
  "team_id": payload.doc_team_id or payload.team_id,
740
- "owner_id": payload.doc_owner_id,
741
- "reviewer_id": payload.doc_reviewer_id,
887
+ "owner_user_id": payload.doc_owner_id,
888
+ "reviewer_user_id": payload.doc_reviewer_id,
742
889
  "version": payload.doc_version,
743
890
  "category": payload.doc_category,
744
891
  "tags": payload.doc_tags,
745
892
  "emoji": payload.doc_emoji or payload.doc_emote,
893
+ "keywords": payload.doc_keywords,
894
+ "is_public": payload.doc_is_public,
746
895
  },
747
896
  )
748
897
  return text(_format_doc(doc, header="✅ Documentação atualizada"))
@@ -932,6 +1081,10 @@ def _format_work(item: Dict[str, Any], *, header: Optional[str] = None) -> str:
932
1081
  lines.append(
933
1082
  f"Vencimento: {_format_date(item.get('due_date') or item.get('dueDate'))}"
934
1083
  )
1084
+ if item.get("tags"):
1085
+ tags = item.get("tags", [])
1086
+ if tags:
1087
+ lines.append(f"Tags: {', '.join(tags)}")
935
1088
  return "\n".join(lines)
936
1089
 
937
1090
 
@@ -9,7 +9,16 @@ from typing import Any, Dict, List, Optional
9
9
  from pydantic import Field
10
10
 
11
11
  from fenix_mcp.application.presenters import text
12
- from fenix_mcp.application.tool_base import Tool, ToolRequest
12
+ from fenix_mcp.application.tool_base import (
13
+ CategoryStr,
14
+ DateTimeStr,
15
+ MarkdownStr,
16
+ TagStr,
17
+ TitleStr,
18
+ Tool,
19
+ ToolRequest,
20
+ UUIDStr,
21
+ )
13
22
  from fenix_mcp.domain.productivity import ProductivityService
14
23
  from fenix_mcp.infrastructure.context import AppContext
15
24
 
@@ -59,19 +68,26 @@ ACTION_FIELD_DESCRIPTION = (
59
68
 
60
69
  class ProductivityRequest(ToolRequest):
61
70
  action: TodoAction = Field(description=ACTION_FIELD_DESCRIPTION)
62
- id: Optional[str] = Field(default=None, description="Identificador do item TODO.")
63
- title: Optional[str] = Field(
71
+ id: Optional[UUIDStr] = Field(default=None, description="ID do item TODO (UUID).")
72
+ title: Optional[TitleStr] = Field(
64
73
  default=None, description="Título do TODO (obrigatório em create)."
65
74
  )
66
- content: Optional[str] = Field(
75
+ content: Optional[MarkdownStr] = Field(
67
76
  default=None, description="Conteúdo em Markdown (obrigatório em create)."
68
77
  )
69
- status: Optional[str] = Field(default=None, description="Status do TODO.")
70
- priority: Optional[str] = Field(default=None, description="Prioridade do TODO.")
71
- category: Optional[str] = Field(default=None, description="Categoria opcional.")
72
- tags: Optional[List[str]] = Field(default=None, description="Lista de tags.")
73
- due_date: Optional[str] = Field(
74
- default=None, description="Data de vencimento do TODO (ISO)."
78
+ status: Optional[str] = Field(
79
+ default=None,
80
+ description="Status do TODO (pending, in_progress, completed, cancelled).",
81
+ )
82
+ priority: Optional[str] = Field(
83
+ default=None, description="Prioridade do TODO (low, medium, high, urgent)."
84
+ )
85
+ category: Optional[CategoryStr] = Field(
86
+ default=None, description="Categoria opcional."
87
+ )
88
+ tags: Optional[List[TagStr]] = Field(default=None, description="Lista de tags.")
89
+ due_date: Optional[DateTimeStr] = Field(
90
+ default=None, description="Data de vencimento do TODO (ISO 8601)."
75
91
  )
76
92
  limit: int = Field(
77
93
  default=20, ge=1, le=100, description="Limite de resultados em list/search."
@@ -9,7 +9,13 @@ from typing import Any, Dict, List, Optional
9
9
  from pydantic import Field
10
10
 
11
11
  from fenix_mcp.application.presenters import text
12
- from fenix_mcp.application.tool_base import Tool, ToolRequest
12
+ from fenix_mcp.application.tool_base import (
13
+ MarkdownStr,
14
+ TitleStr,
15
+ Tool,
16
+ ToolRequest,
17
+ UUIDStr,
18
+ )
13
19
  from fenix_mcp.domain.user_config import UserConfigService, _strip_none
14
20
  from fenix_mcp.infrastructure.context import AppContext
15
21
 
@@ -51,15 +57,20 @@ ACTION_FIELD_DESCRIPTION = "Ação a executar. Escolha um dos valores: " + ", ".
51
57
 
52
58
  class UserConfigRequest(ToolRequest):
53
59
  action: UserConfigAction = Field(description=ACTION_FIELD_DESCRIPTION)
54
- id: Optional[str] = Field(default=None, description="ID do documento.")
55
- name: Optional[str] = Field(default=None, description="Nome do documento.")
56
- content: Optional[str] = Field(
60
+ id: Optional[UUIDStr] = Field(default=None, description="ID do documento (UUID).")
61
+ name: Optional[TitleStr] = Field(default=None, description="Nome do documento.")
62
+ content: Optional[MarkdownStr] = Field(
57
63
  default=None, description="Conteúdo em Markdown/JSON."
58
64
  )
59
- mode_id: Optional[str] = Field(default=None, description="ID do modo associado.")
65
+ mode_id: Optional[UUIDStr] = Field(
66
+ default=None, description="ID do modo associado (UUID)."
67
+ )
60
68
  is_default: Optional[bool] = Field(
61
69
  default=None, description="Marca o documento como padrão."
62
70
  )
71
+ metadata: Optional[MarkdownStr] = Field(
72
+ default=None, description="Metadata estruturada do documento (Markdown)."
73
+ )
63
74
  limit: int = Field(default=20, ge=1, le=100, description="Limite para listagem.")
64
75
  offset: int = Field(default=0, ge=0, description="Offset para listagem.")
65
76
  return_content: Optional[bool] = Field(
@@ -90,6 +101,7 @@ class UserConfigTool(Tool):
90
101
  "content": payload.content,
91
102
  "mode_id": payload.mode_id,
92
103
  "is_default": payload.is_default,
104
+ "metadata": payload.metadata,
93
105
  }
94
106
  )
95
107
  )
@@ -124,6 +136,7 @@ class UserConfigTool(Tool):
124
136
  "content": payload.content,
125
137
  "mode_id": payload.mode_id,
126
138
  "is_default": payload.is_default,
139
+ "metadata": payload.metadata,
127
140
  }
128
141
  )
129
142
  doc = await self._service.update(payload.id, data)
@@ -114,36 +114,6 @@ class IntelligenceService:
114
114
  }
115
115
  return await self._call(self.api.consolidate_memories, payload)
116
116
 
117
- async def priority_memories(self, *, limit: int) -> List[Dict[str, Any]]:
118
- params = {
119
- "limit": limit,
120
- "sortBy": "priority_score",
121
- "sortOrder": "desc",
122
- }
123
- return (
124
- await self._call(
125
- self.api.list_memories,
126
- include_content=False,
127
- include_metadata=False,
128
- **params,
129
- )
130
- or []
131
- )
132
-
133
- async def analytics(self, *, time_range: str, group_by: str) -> Dict[str, Any]:
134
- memories = await self.query_memories(
135
- limit=200, timeRange=time_range, groupBy=group_by
136
- )
137
- summary: Dict[str, Any] = {
138
- "total_memories": len(memories),
139
- "by_group": {},
140
- }
141
- group_key = group_by
142
- for memory in memories:
143
- key = memory.get(group_key) or memory.get("metadata") or "N/A"
144
- summary["by_group"][key] = summary["by_group"].get(key, 0) + 1
145
- return summary
146
-
147
117
  async def update_memory(self, memory_id: str, **fields: Any) -> Dict[str, Any]:
148
118
  payload = _strip_none(fields)
149
119
  if "importance" in payload:
@@ -162,6 +132,9 @@ class IntelligenceService:
162
132
  payload[new_key] = payload.pop(old_key)
163
133
  return await self._call(self.api.update_memory, memory_id, payload)
164
134
 
135
+ async def delete_memory(self, memory_id: str) -> None:
136
+ await self._call(self.api.delete_memory, memory_id)
137
+
165
138
  async def _call(self, func, *args, **kwargs):
166
139
  return await asyncio.to_thread(func, *args, **kwargs)
167
140
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fenix-mcp
3
- Version: 0.5.12
3
+ Version: 0.6.0
4
4
  Summary: Fênix Cloud MCP server implemented in Python
5
5
  Author: Fenix Inc
6
6
  Requires-Python: >=3.10
@@ -1,47 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- """Base abstractions for MCP tools."""
3
-
4
- from __future__ import annotations
5
-
6
- from abc import ABC, abstractmethod
7
- from typing import Any, Dict, Type
8
-
9
- from pydantic import BaseModel, ConfigDict
10
-
11
- from fenix_mcp.infrastructure.context import AppContext
12
-
13
-
14
- class ToolRequest(BaseModel):
15
- """Base request payload."""
16
-
17
- model_config = ConfigDict(extra="forbid")
18
-
19
-
20
- ToolResponse = Dict[str, Any]
21
-
22
-
23
- class Tool(ABC):
24
- """Interface implemented by all tools."""
25
-
26
- name: str
27
- description: str
28
- request_model: Type[ToolRequest] = ToolRequest
29
-
30
- def schema(self) -> Dict[str, Any]:
31
- """Return JSON schema describing the tool arguments."""
32
- return {
33
- "name": self.name,
34
- "description": self.description,
35
- "inputSchema": self.request_model.model_json_schema(),
36
- }
37
-
38
- async def execute(
39
- self, raw_arguments: Dict[str, Any], context: AppContext
40
- ) -> ToolResponse:
41
- """Validate raw arguments and run the tool."""
42
- payload = self.request_model.model_validate(raw_arguments or {})
43
- return await self.run(payload, context)
44
-
45
- @abstractmethod
46
- async def run(self, payload: ToolRequest, context: AppContext) -> ToolResponse:
47
- """Execute business logic and return a MCP-formatted response."""
File without changes
File without changes
File without changes
File without changes