fenix-mcp 0.5.12__tar.gz → 0.7.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.7.0}/PKG-INFO +1 -1
  2. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/__init__.py +1 -1
  3. fenix_mcp-0.7.0/fenix_mcp/application/tool_base.py +159 -0
  4. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/application/tools/initialize.py +23 -2
  5. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/application/tools/intelligence.py +50 -52
  6. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/application/tools/knowledge.py +250 -90
  7. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/application/tools/productivity.py +26 -10
  8. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/application/tools/user_config.py +18 -5
  9. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/domain/intelligence.py +3 -30
  10. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp.egg-info/PKG-INFO +1 -1
  11. fenix_mcp-0.5.12/fenix_mcp/application/tool_base.py +0 -47
  12. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/README.md +0 -0
  13. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/application/presenters.py +0 -0
  14. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/application/tool_registry.py +0 -0
  15. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/application/tools/__init__.py +0 -0
  16. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/application/tools/health.py +0 -0
  17. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/domain/initialization.py +0 -0
  18. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/domain/knowledge.py +0 -0
  19. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/domain/productivity.py +0 -0
  20. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/domain/user_config.py +0 -0
  21. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/infrastructure/config.py +0 -0
  22. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/infrastructure/context.py +0 -0
  23. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/infrastructure/fenix_api/client.py +0 -0
  24. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/infrastructure/http_client.py +0 -0
  25. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/infrastructure/logging.py +0 -0
  26. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/interface/mcp_server.py +0 -0
  27. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/interface/transports.py +0 -0
  28. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp/main.py +0 -0
  29. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp.egg-info/SOURCES.txt +0 -0
  30. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp.egg-info/dependency_links.txt +0 -0
  31. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp.egg-info/entry_points.txt +0 -0
  32. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp.egg-info/requires.txt +0 -0
  33. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/fenix_mcp.egg-info/top_level.txt +0 -0
  34. {fenix_mcp-0.5.12 → fenix_mcp-0.7.0}/pyproject.toml +0 -0
  35. {fenix_mcp-0.5.12 → fenix_mcp-0.7.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.7.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.7.0"
@@ -0,0 +1,159 @@
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, List, Optional, Type, TypeVar
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, StringConstraints
10
+
11
+ from fenix_mcp.infrastructure.context import AppContext
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ def sanitize_null(value: Optional[T]) -> Optional[T]:
17
+ """Convert string 'null' to None. Handles AI agents passing 'null' as string."""
18
+ if value == "null" or value == "None" or value == "":
19
+ return None
20
+ return value
21
+
22
+
23
+ def sanitize_null_list(value: Optional[List[T]]) -> Optional[List[T]]:
24
+ """Sanitize list values, converting 'null' string to None."""
25
+ if value == "null" or value == "None" or value == "": # type: ignore
26
+ return None
27
+ if isinstance(value, list):
28
+ return [v for v in value if v != "null" and v != "None" and v != ""]
29
+ return value
30
+
31
+
32
+ # =============================================================================
33
+ # Type aliases for common field types - generates proper JSON schema
34
+ # =============================================================================
35
+
36
+ # UUID string - format: uuid, with regex pattern validation
37
+ UUIDStr = Annotated[
38
+ str,
39
+ Field(
40
+ 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}$",
41
+ json_schema_extra={"format": "uuid"},
42
+ ),
43
+ ]
44
+
45
+ # ISO 8601 date string (YYYY-MM-DD)
46
+ DateStr = Annotated[
47
+ str,
48
+ Field(
49
+ pattern=r"^\d{4}-\d{2}-\d{2}$",
50
+ json_schema_extra={"format": "date"},
51
+ examples=["2025-01-15"],
52
+ ),
53
+ ]
54
+
55
+ # ISO 8601 datetime string (YYYY-MM-DDTHH:MM:SSZ or YYYY-MM-DD)
56
+ DateTimeStr = Annotated[
57
+ str,
58
+ Field(
59
+ pattern=r"^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$",
60
+ json_schema_extra={"format": "date-time"},
61
+ examples=["2025-01-15T10:30:00Z", "2025-01-15"],
62
+ ),
63
+ ]
64
+
65
+ # Markdown content - indicates rich text
66
+ MarkdownStr = Annotated[
67
+ str,
68
+ Field(
69
+ json_schema_extra={"format": "markdown", "contentMediaType": "text/markdown"},
70
+ ),
71
+ ]
72
+
73
+ # Short title/name strings (1-300 chars)
74
+ TitleStr = Annotated[
75
+ str,
76
+ StringConstraints(min_length=1, max_length=300),
77
+ ]
78
+
79
+ # Description strings (up to 1000 chars)
80
+ DescriptionStr = Annotated[
81
+ str,
82
+ StringConstraints(max_length=1000),
83
+ ]
84
+
85
+ # Tag string (lowercase, alphanumeric with hyphens/underscores)
86
+ TagStr = Annotated[
87
+ str,
88
+ Field(
89
+ pattern=r"^[a-zA-Z0-9_-]+$",
90
+ examples=["bug", "feature", "high-priority"],
91
+ ),
92
+ ]
93
+
94
+ # Category strings
95
+ CategoryStr = Annotated[
96
+ str,
97
+ StringConstraints(max_length=100),
98
+ ]
99
+
100
+ # Language code (ISO 639-1)
101
+ LanguageStr = Annotated[
102
+ str,
103
+ Field(
104
+ pattern=r"^[a-z]{2}(-[A-Z]{2})?$",
105
+ examples=["pt", "en", "pt-BR", "en-US"],
106
+ ),
107
+ ]
108
+
109
+ # Version string (semver-like)
110
+ VersionStr = Annotated[
111
+ str,
112
+ Field(
113
+ pattern=r"^[0-9]+(\.[0-9]+)*(-[a-zA-Z0-9]+)?$",
114
+ max_length=20,
115
+ examples=["1.0", "2.1.3", "1.0.0-beta"],
116
+ ),
117
+ ]
118
+
119
+ # Emoji string (single emoji or short code)
120
+ EmojiStr = Annotated[
121
+ str,
122
+ StringConstraints(max_length=10),
123
+ ]
124
+
125
+
126
+ class ToolRequest(BaseModel):
127
+ """Base request payload."""
128
+
129
+ model_config = ConfigDict(extra="forbid")
130
+
131
+
132
+ ToolResponse = Dict[str, Any]
133
+
134
+
135
+ class Tool(ABC):
136
+ """Interface implemented by all tools."""
137
+
138
+ name: str
139
+ description: str
140
+ request_model: Type[ToolRequest] = ToolRequest
141
+
142
+ def schema(self) -> Dict[str, Any]:
143
+ """Return JSON schema describing the tool arguments."""
144
+ return {
145
+ "name": self.name,
146
+ "description": self.description,
147
+ "inputSchema": self.request_model.model_json_schema(),
148
+ }
149
+
150
+ async def execute(
151
+ self, raw_arguments: Dict[str, Any], context: AppContext
152
+ ) -> ToolResponse:
153
+ """Validate raw arguments and run the tool."""
154
+ payload = self.request_model.model_validate(raw_arguments or {})
155
+ return await self.run(payload, context)
156
+
157
+ @abstractmethod
158
+ async def run(self, payload: ToolRequest, context: AppContext) -> ToolResponse:
159
+ """Execute business logic and return a MCP-formatted response."""
@@ -3,8 +3,8 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- from enum import Enum
7
6
  import json
7
+ from enum import Enum
8
8
  from typing import List, Optional
9
9
 
10
10
  from pydantic import Field
@@ -93,7 +93,28 @@ class InitializeTool(Tool):
93
93
  if data.recent_memories:
94
94
  payload_dict["recent_memories"] = data.recent_memories
95
95
 
96
- message_lines = [
96
+ # Extract key IDs for easy reference
97
+ profile = data.profile or {}
98
+ user_info = profile.get("user") or {}
99
+ tenant_info = profile.get("tenant") or {}
100
+ team_info = profile.get("team") or {}
101
+
102
+ context_lines = ["📋 **Contexto do Usuário**"]
103
+ if user_info.get("id"):
104
+ context_lines.append(f"- **user_id**: `{user_info['id']}`")
105
+ if user_info.get("name"):
106
+ context_lines.append(f"- **user_name**: {user_info['name']}")
107
+ if tenant_info.get("id"):
108
+ context_lines.append(f"- **tenant_id**: `{tenant_info['id']}`")
109
+ if tenant_info.get("name"):
110
+ context_lines.append(f"- **tenant_name**: {tenant_info['name']}")
111
+ if team_info.get("id"):
112
+ context_lines.append(f"- **team_id**: `{team_info['id']}`")
113
+ if team_info.get("name"):
114
+ context_lines.append(f"- **team_name**: {team_info['name']}")
115
+
116
+ message_lines = context_lines + [
117
+ "",
97
118
  "📦 **Dados de inicialização completos**",
98
119
  "```json",
99
120
  json.dumps(payload_dict, ensure_ascii=False, indent=2),
@@ -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,22 @@ 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
+ sanitize_null,
26
+ sanitize_null_list,
27
+ )
13
28
  from fenix_mcp.domain.knowledge import KnowledgeService, _format_date
14
29
  from fenix_mcp.infrastructure.context import AppContext
15
30
 
@@ -41,6 +56,16 @@ class KnowledgeAction(str, Enum):
41
56
  WORK_ANALYTICS = ("work_analytics", "Retorna métricas consolidadas de work items.")
42
57
  WORK_BY_BOARD = ("work_by_board", "Lista work items associados a um board.")
43
58
  WORK_BY_SPRINT = ("work_by_sprint", "Lista work items associados a um sprint.")
59
+ WORK_BY_EPIC = ("work_by_epic", "Lista work items associados a um épico.")
60
+ WORK_CHILDREN = ("work_children", "Lista work items filhos de um item pai.")
61
+ WORK_STATUS_UPDATE = (
62
+ "work_status_update",
63
+ "Atualiza apenas o status de um work item.",
64
+ )
65
+ WORK_ASSIGN_SPRINT = (
66
+ "work_assign_sprint",
67
+ "Atribui work items a um sprint.",
68
+ )
44
69
 
45
70
  # Boards
46
71
  BOARD_LIST = ("board_list", "Lista boards disponíveis com filtros opcionais.")
@@ -130,19 +155,25 @@ _ALLOWED_DOC_TYPES = {
130
155
 
131
156
  class KnowledgeRequest(ToolRequest):
132
157
  action: KnowledgeAction = Field(description=ACTION_FIELD_DESCRIPTION)
133
- id: Optional[str] = Field(default=None, description="ID principal do recurso.")
158
+ id: Optional[UUIDStr] = Field(
159
+ default=None, description="ID principal do recurso (UUID)."
160
+ )
134
161
  limit: int = Field(default=20, ge=1, le=100, description="Limite de resultados.")
135
162
  offset: int = Field(
136
163
  default=0, ge=0, description="Offset de paginação (quando suportado)."
137
164
  )
138
- team_id: Optional[str] = Field(
139
- default=None, description="ID do time para filtros (boards/sprints/docs)."
165
+ team_id: Optional[UUIDStr] = Field(
166
+ default=None, description="ID do time para filtros (UUID)."
140
167
  )
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."
168
+ board_id: Optional[UUIDStr] = Field(
169
+ default=None, description="ID do board associado (UUID)."
170
+ )
171
+ sprint_id: Optional[UUIDStr] = Field(
172
+ default=None, description="ID do sprint associado (UUID)."
173
+ )
174
+ epic_id: Optional[UUIDStr] = Field(
175
+ default=None, description="ID do épico associado (UUID)."
144
176
  )
145
- epic_id: Optional[str] = Field(default=None, description="ID do épico associado.")
146
177
  query: Optional[str] = Field(default=None, description="Filtro/busca.")
147
178
  return_content: Optional[bool] = Field(
148
179
  default=None, description="Retorna conteúdo completo."
@@ -155,71 +186,130 @@ class KnowledgeRequest(ToolRequest):
155
186
  )
156
187
 
157
188
  # 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."
189
+ work_title: Optional[TitleStr] = Field(
190
+ default=None, description="Título do work item."
191
+ )
192
+ work_description: Optional[MarkdownStr] = Field(
193
+ default=None, description="Descrição do work item (Markdown)."
194
+ )
195
+ work_type: Optional[str] = Field(
196
+ default="task",
197
+ description="Tipo do work item (epic, feature, story, task, subtask, bug).",
198
+ )
199
+ work_status: Optional[str] = Field(
200
+ default=None,
201
+ description="Status do work item (backlog, todo, in_progress, review, testing, done, cancelled).",
161
202
  )
162
- work_type: Optional[str] = Field(default="task", description="Tipo do work item.")
163
203
  work_priority: Optional[str] = Field(
164
- default=None, description="Prioridade do work item."
204
+ default=None,
205
+ description="Prioridade do work item (critical, high, medium, low).",
206
+ )
207
+ story_points: Optional[int] = Field(
208
+ default=None, ge=0, le=100, description="Story points (0-100)."
209
+ )
210
+ assignee_id: Optional[UUIDStr] = Field(
211
+ default=None, description="ID do responsável (UUID)."
212
+ )
213
+ parent_id: Optional[UUIDStr] = Field(
214
+ default=None, description="ID do item pai (UUID)."
215
+ )
216
+ work_due_date: Optional[DateTimeStr] = Field(
217
+ default=None, description="Data de vencimento do work item (ISO 8601)."
218
+ )
219
+ work_tags: Optional[List[TagStr]] = Field(
220
+ default=None, description="Tags do work item."
221
+ )
222
+ work_item_ids: Optional[List[UUIDStr]] = Field(
223
+ default=None,
224
+ description="Lista de IDs de work items para operações em lote (UUIDs).",
165
225
  )
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
226
 
170
227
  # 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(
228
+ mode_id: Optional[UUIDStr] = Field(
229
+ default=None, description="ID do modo relacionado (UUID)."
230
+ )
231
+ mode_name: Optional[TitleStr] = Field(default=None, description="Nome do modo.")
232
+ mode_description: Optional[DescriptionStr] = Field(
174
233
  default=None, description="Descrição do modo."
175
234
  )
176
- mode_content: Optional[str] = Field(default=None, description="Conteúdo do modo.")
235
+ mode_content: Optional[MarkdownStr] = Field(
236
+ default=None, description="Conteúdo do modo (Markdown)."
237
+ )
177
238
  mode_is_default: Optional[bool] = Field(
178
239
  default=None, description="Indica se o modo é padrão."
179
240
  )
241
+ mode_metadata: Optional[MarkdownStr] = Field(
242
+ default=None, description="Metadata do modo para processamento de IA."
243
+ )
180
244
 
181
245
  # 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(
246
+ rule_id: Optional[UUIDStr] = Field(
247
+ default=None, description="ID da regra relacionada (UUID)."
248
+ )
249
+ rule_name: Optional[TitleStr] = Field(default=None, description="Nome da regra.")
250
+ rule_description: Optional[DescriptionStr] = Field(
185
251
  default=None, description="Descrição da regra."
186
252
  )
187
- rule_content: Optional[str] = Field(default=None, description="Conteúdo da regra.")
253
+ rule_content: Optional[MarkdownStr] = Field(
254
+ default=None, description="Conteúdo da regra (Markdown)."
255
+ )
188
256
  rule_is_default: Optional[bool] = Field(default=None, description="Regra padrão.")
257
+ rule_metadata: Optional[MarkdownStr] = Field(
258
+ default=None, description="Metadata da regra para processamento de IA."
259
+ )
189
260
 
190
261
  # Documentation fields
191
- doc_title: Optional[str] = Field(
262
+ doc_title: Optional[TitleStr] = Field(
192
263
  default=None, description="Título da documentação."
193
264
  )
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."
265
+ doc_description: Optional[DescriptionStr] = Field(
266
+ default=None, description="Descrição da documentação."
267
+ )
268
+ doc_content: Optional[MarkdownStr] = Field(
269
+ default=None, description="Conteúdo da documentação (Markdown)."
197
270
  )
198
271
  doc_status: Optional[str] = Field(
199
- default=None, description="Status da documentação."
272
+ default=None,
273
+ description="Status da documentação (draft, review, published, archived).",
274
+ )
275
+ doc_type: Optional[str] = Field(
276
+ default=None, description="Tipo da documentação (folder, page, api_doc, guide)."
277
+ )
278
+ doc_language: Optional[LanguageStr] = Field(
279
+ default=None, description="Idioma da documentação (ex: pt, en, pt-BR)."
280
+ )
281
+ doc_parent_id: Optional[UUIDStr] = Field(
282
+ default=None, description="ID do documento pai (UUID)."
200
283
  )
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."
284
+ doc_team_id: Optional[UUIDStr] = Field(
285
+ default=None, description="ID do time responsável pela documentação (UUID)."
204
286
  )
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."
287
+ doc_owner_id: Optional[UUIDStr] = Field(
288
+ default=None, description="ID do dono (UUID)."
208
289
  )
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.")
290
+ doc_reviewer_id: Optional[UUIDStr] = Field(
291
+ default=None, description="ID do revisor (UUID)."
292
+ )
293
+ doc_version: Optional[VersionStr] = Field(
294
+ default=None, description="Versão do documento (ex: 1.0, 2.1.3)."
295
+ )
296
+ doc_category: Optional[CategoryStr] = Field(default=None, description="Categoria.")
297
+ doc_tags: Optional[List[TagStr]] = Field(default=None, description="Tags.")
214
298
  doc_position: Optional[int] = Field(
215
- default=None, description="Posição desejada ao mover documentos."
299
+ default=None, ge=0, description="Posição desejada ao mover documentos."
216
300
  )
217
- doc_emoji: Optional[str] = Field(
301
+ doc_emoji: Optional[EmojiStr] = Field(
218
302
  default=None, description="Emoji exibido junto ao documento."
219
303
  )
220
- doc_emote: Optional[str] = Field(
304
+ doc_emote: Optional[EmojiStr] = Field(
221
305
  default=None, description="Alias para emoji, mantido por compatibilidade."
222
306
  )
307
+ doc_keywords: Optional[List[TagStr]] = Field(
308
+ default=None, description="Palavras-chave para busca."
309
+ )
310
+ doc_is_public: Optional[bool] = Field(
311
+ default=None, description="Se o documento é público."
312
+ )
223
313
 
224
314
 
225
315
  class KnowledgeTool(Tool):
@@ -260,17 +350,23 @@ class KnowledgeTool(Tool):
260
350
  if action is KnowledgeAction.WORK_CREATE:
261
351
  if not payload.work_title:
262
352
  return text("❌ Informe work_title para criar o item.")
353
+ if not payload.team_id:
354
+ return text("❌ Informe team_id para criar o item.")
263
355
  work = await self._service.work_create(
264
356
  {
265
357
  "title": payload.work_title,
266
358
  "description": payload.work_description,
267
359
  "item_type": payload.work_type,
360
+ "status": payload.work_status,
268
361
  "priority": payload.work_priority,
269
362
  "story_points": payload.story_points,
270
363
  "assignee_id": payload.assignee_id,
271
364
  "sprint_id": payload.sprint_id,
272
365
  "board_id": payload.board_id,
273
366
  "parent_id": payload.parent_id,
367
+ "team_id": payload.team_id,
368
+ "due_date": payload.work_due_date,
369
+ "tags": payload.work_tags,
274
370
  }
275
371
  )
276
372
  return text(_format_work(work, header="✅ Work item criado"))
@@ -305,12 +401,16 @@ class KnowledgeTool(Tool):
305
401
  "title": payload.work_title,
306
402
  "description": payload.work_description,
307
403
  "item_type": payload.work_type,
404
+ "status": payload.work_status,
308
405
  "priority": payload.work_priority,
309
406
  "story_points": payload.story_points,
310
407
  "assignee_id": payload.assignee_id,
311
408
  "sprint_id": payload.sprint_id,
312
409
  "board_id": payload.board_id,
313
410
  "parent_id": payload.parent_id,
411
+ "team_id": payload.team_id,
412
+ "due_date": payload.work_due_date,
413
+ "tags": payload.work_tags,
314
414
  },
315
415
  )
316
416
  return text(_format_work(work, header="✅ Work item atualizado"))
@@ -322,20 +422,23 @@ class KnowledgeTool(Tool):
322
422
  return text(f"🗑️ Work item {payload.id} removido.")
323
423
 
324
424
  if action is KnowledgeAction.WORK_BACKLOG:
325
- if not payload.team_id:
425
+ team_id = sanitize_null(payload.team_id)
426
+ if not team_id:
326
427
  return text("❌ Informe team_id para consultar o backlog.")
327
- items = await self._service.work_backlog(team_id=payload.team_id)
428
+ items = await self._service.work_backlog(team_id=team_id)
328
429
  if not items:
329
430
  return text("📋 Backlog vazio para o time informado.")
330
431
  body = "\n\n".join(_format_work(item) for item in items)
331
432
  return text(f"📋 **Backlog ({len(items)}):**\n\n{body}")
332
433
 
333
434
  if action is KnowledgeAction.WORK_SEARCH:
334
- if not payload.query or not payload.team_id:
435
+ query = sanitize_null(payload.query)
436
+ team_id = sanitize_null(payload.team_id)
437
+ if not query or not team_id:
335
438
  return text("❌ Informe query e team_id para buscar work items.")
336
439
  items = await self._service.work_search(
337
- query=payload.query,
338
- team_id=payload.team_id,
440
+ query=query,
441
+ team_id=team_id,
339
442
  limit=payload.limit,
340
443
  )
341
444
  if not items:
@@ -344,9 +447,10 @@ class KnowledgeTool(Tool):
344
447
  return text(f"🔍 **Resultados ({len(items)}):**\n\n{body}")
345
448
 
346
449
  if action is KnowledgeAction.WORK_ANALYTICS:
347
- if not payload.team_id:
450
+ team_id = sanitize_null(payload.team_id)
451
+ if not team_id:
348
452
  return text("❌ Informe team_id para obter analytics.")
349
- analytics = await self._service.work_analytics(team_id=payload.team_id)
453
+ analytics = await self._service.work_analytics(team_id=team_id)
350
454
  lines = ["📊 **Analytics de Work Items**"]
351
455
  for key, value in analytics.items():
352
456
  lines.append(f"- {key}: {value}")
@@ -370,6 +474,49 @@ class KnowledgeTool(Tool):
370
474
  body = "\n\n".join(_format_work(item) for item in items)
371
475
  return text(f"🏃 **Work items do sprint ({len(items)}):**\n\n{body}")
372
476
 
477
+ if action is KnowledgeAction.WORK_BY_EPIC:
478
+ if not payload.epic_id:
479
+ return text("❌ Informe epic_id para listar os itens.")
480
+ items = await self._service.work_by_epic(epic_id=payload.epic_id)
481
+ if not items:
482
+ return text("📦 Nenhum item vinculado ao épico informado.")
483
+ body = "\n\n".join(_format_work(item) for item in items)
484
+ return text(f"📦 **Work items do épico ({len(items)}):**\n\n{body}")
485
+
486
+ if action is KnowledgeAction.WORK_CHILDREN:
487
+ if not payload.id:
488
+ return text("❌ Informe o ID do work item pai.")
489
+ items = await self._service.work_children(payload.id)
490
+ if not items:
491
+ return text("👶 Nenhum item filho encontrado.")
492
+ body = "\n\n".join(_format_work(item) for item in items)
493
+ return text(f"👶 **Work items filhos ({len(items)}):**\n\n{body}")
494
+
495
+ if action is KnowledgeAction.WORK_STATUS_UPDATE:
496
+ if not payload.id:
497
+ return text("❌ Informe o ID do work item.")
498
+ if not payload.work_status:
499
+ return text("❌ Informe work_status para atualizar.")
500
+ work = await self._service.work_update_status(
501
+ payload.id,
502
+ {"status": payload.work_status},
503
+ )
504
+ return text(_format_work(work, header="✅ Status atualizado"))
505
+
506
+ if action is KnowledgeAction.WORK_ASSIGN_SPRINT:
507
+ if not payload.sprint_id:
508
+ return text("❌ Informe sprint_id para atribuir os itens.")
509
+ if not payload.work_item_ids:
510
+ return text("❌ Informe work_item_ids com a lista de IDs.")
511
+ await self._service.work_assign_to_sprint(
512
+ {
513
+ "sprint_id": payload.sprint_id,
514
+ "work_item_ids": payload.work_item_ids,
515
+ }
516
+ )
517
+ count = len(payload.work_item_ids)
518
+ return text(f"✅ {count} work item(s) atribuído(s) ao sprint.")
519
+
373
520
  return text(
374
521
  "❌ Ação de work item não suportada.\n\nEscolha um dos valores:\n"
375
522
  + "\n".join(
@@ -505,6 +652,7 @@ class KnowledgeTool(Tool):
505
652
  "description": payload.mode_description,
506
653
  "content": payload.mode_content,
507
654
  "is_default": payload.mode_is_default,
655
+ "metadata": payload.mode_metadata,
508
656
  }
509
657
  )
510
658
  return text(_format_mode(mode, header="✅ Modo criado"))
@@ -540,6 +688,7 @@ class KnowledgeTool(Tool):
540
688
  "description": payload.mode_description,
541
689
  "content": payload.mode_content,
542
690
  "is_default": payload.mode_is_default,
691
+ "metadata": payload.mode_metadata,
543
692
  },
544
693
  )
545
694
  return text(_format_mode(mode, header="✅ Modo atualizado"))
@@ -610,6 +759,7 @@ class KnowledgeTool(Tool):
610
759
  "description": payload.rule_description,
611
760
  "content": payload.rule_content,
612
761
  "is_default": payload.rule_is_default,
762
+ "metadata": payload.rule_metadata,
613
763
  }
614
764
  )
615
765
  return text(_format_rule(rule, header="✅ Regra criada"))
@@ -646,6 +796,7 @@ class KnowledgeTool(Tool):
646
796
  "description": payload.rule_description,
647
797
  "content": payload.rule_content,
648
798
  "is_default": payload.rule_is_default,
799
+ "metadata": payload.rule_metadata,
649
800
  },
650
801
  )
651
802
  return text(_format_rule(rule, header="✅ Regra atualizada"))
@@ -681,19 +832,21 @@ class KnowledgeTool(Tool):
681
832
  doc = await self._service.doc_create(
682
833
  {
683
834
  "title": payload.doc_title,
684
- "description": payload.doc_description,
685
- "content": payload.doc_content,
686
- "status": payload.doc_status,
687
- "doc_type": payload.doc_type,
688
- "language": payload.doc_language,
689
- "parent_id": payload.doc_parent_id,
690
- "team_id": payload.doc_team_id or payload.team_id,
691
- "owner_id": payload.doc_owner_id,
692
- "reviewer_id": payload.doc_reviewer_id,
693
- "version": payload.doc_version,
694
- "category": payload.doc_category,
695
- "tags": payload.doc_tags,
696
- "emoji": payload.doc_emoji or payload.doc_emote,
835
+ "description": sanitize_null(payload.doc_description),
836
+ "content": sanitize_null(payload.doc_content),
837
+ "status": sanitize_null(payload.doc_status),
838
+ "doc_type": sanitize_null(payload.doc_type),
839
+ "language": sanitize_null(payload.doc_language),
840
+ "parent_id": sanitize_null(payload.doc_parent_id),
841
+ "team_id": sanitize_null(payload.doc_team_id or payload.team_id),
842
+ "owner_user_id": sanitize_null(payload.doc_owner_id),
843
+ "reviewer_user_id": sanitize_null(payload.doc_reviewer_id),
844
+ "version": sanitize_null(payload.doc_version),
845
+ "category": sanitize_null(payload.doc_category),
846
+ "tags": sanitize_null_list(payload.doc_tags),
847
+ "emoji": sanitize_null(payload.doc_emoji or payload.doc_emote),
848
+ "keywords": sanitize_null_list(payload.doc_keywords),
849
+ "is_public": payload.doc_is_public,
697
850
  }
698
851
  )
699
852
  return text(_format_doc(doc, header="✅ Documentação criada"))
@@ -729,20 +882,22 @@ class KnowledgeTool(Tool):
729
882
  doc = await self._service.doc_update(
730
883
  payload.id,
731
884
  {
732
- "title": payload.doc_title,
733
- "description": payload.doc_description,
734
- "content": payload.doc_content,
735
- "status": payload.doc_status,
736
- "doc_type": payload.doc_type,
737
- "language": payload.doc_language,
738
- "parent_id": payload.doc_parent_id,
739
- "team_id": payload.doc_team_id or payload.team_id,
740
- "owner_id": payload.doc_owner_id,
741
- "reviewer_id": payload.doc_reviewer_id,
742
- "version": payload.doc_version,
743
- "category": payload.doc_category,
744
- "tags": payload.doc_tags,
745
- "emoji": payload.doc_emoji or payload.doc_emote,
885
+ "title": sanitize_null(payload.doc_title),
886
+ "description": sanitize_null(payload.doc_description),
887
+ "content": sanitize_null(payload.doc_content),
888
+ "status": sanitize_null(payload.doc_status),
889
+ "doc_type": sanitize_null(payload.doc_type),
890
+ "language": sanitize_null(payload.doc_language),
891
+ "parent_id": sanitize_null(payload.doc_parent_id),
892
+ "team_id": sanitize_null(payload.doc_team_id or payload.team_id),
893
+ "owner_user_id": sanitize_null(payload.doc_owner_id),
894
+ "reviewer_user_id": sanitize_null(payload.doc_reviewer_id),
895
+ "version": sanitize_null(payload.doc_version),
896
+ "category": sanitize_null(payload.doc_category),
897
+ "tags": sanitize_null_list(payload.doc_tags),
898
+ "emoji": sanitize_null(payload.doc_emoji or payload.doc_emote),
899
+ "keywords": sanitize_null_list(payload.doc_keywords),
900
+ "is_public": payload.doc_is_public,
746
901
  },
747
902
  )
748
903
  return text(_format_doc(doc, header="✅ Documentação atualizada"))
@@ -754,11 +909,13 @@ class KnowledgeTool(Tool):
754
909
  return text(f"🗑️ Documentação {payload.id} removida.")
755
910
 
756
911
  if action is KnowledgeAction.DOC_SEARCH:
757
- if not payload.query or not (payload.doc_team_id or payload.team_id):
912
+ query = sanitize_null(payload.query)
913
+ team_id = sanitize_null(payload.doc_team_id or payload.team_id)
914
+ if not query or not team_id:
758
915
  return text("❌ Informe query e team_id para buscar documentação.")
759
916
  docs = await self._service.doc_search(
760
- query=payload.query,
761
- team_id=payload.doc_team_id or payload.team_id,
917
+ query=query,
918
+ team_id=team_id,
762
919
  limit=payload.limit,
763
920
  )
764
921
  if not docs:
@@ -769,11 +926,10 @@ class KnowledgeTool(Tool):
769
926
  return text(f"🔍 **Resultados ({len(docs)}):**\n\n{body}")
770
927
 
771
928
  if action is KnowledgeAction.DOC_ROOTS:
772
- if not (payload.doc_team_id or payload.team_id):
929
+ team_id = sanitize_null(payload.doc_team_id or payload.team_id)
930
+ if not team_id:
773
931
  return text("❌ Informe team_id para listar raízes.")
774
- docs = await self._service.doc_roots(
775
- team_id=payload.doc_team_id or payload.team_id
776
- )
932
+ docs = await self._service.doc_roots(team_id=team_id)
777
933
  if not docs:
778
934
  return text("📚 Nenhuma raiz encontrada.")
779
935
  body = "\n".join(
@@ -783,10 +939,11 @@ class KnowledgeTool(Tool):
783
939
  return text(f"📚 **Raízes de documentação:**\n{body}")
784
940
 
785
941
  if action is KnowledgeAction.DOC_RECENT:
786
- if not (payload.doc_team_id or payload.team_id):
942
+ team_id = sanitize_null(payload.doc_team_id or payload.team_id)
943
+ if not team_id:
787
944
  return text("❌ Informe team_id para listar documentos recentes.")
788
945
  docs = await self._service.doc_recent(
789
- team_id=payload.doc_team_id or payload.team_id,
946
+ team_id=team_id,
790
947
  limit=payload.limit,
791
948
  )
792
949
  if not docs:
@@ -795,11 +952,10 @@ class KnowledgeTool(Tool):
795
952
  return text(f"🕒 **Documentos recentes ({len(docs)}):**\n\n{body}")
796
953
 
797
954
  if action is KnowledgeAction.DOC_ANALYTICS:
798
- if not (payload.doc_team_id or payload.team_id):
955
+ team_id = sanitize_null(payload.doc_team_id or payload.team_id)
956
+ if not team_id:
799
957
  return text("❌ Informe team_id para obter analytics.")
800
- analytics = await self._service.doc_analytics(
801
- team_id=payload.doc_team_id or payload.team_id
802
- )
958
+ analytics = await self._service.doc_analytics(team_id=team_id)
803
959
  lines = ["📊 **Analytics de Documentação**"]
804
960
  for key, value in analytics.items():
805
961
  lines.append(f"- {key}: {value}")
@@ -932,6 +1088,10 @@ def _format_work(item: Dict[str, Any], *, header: Optional[str] = None) -> str:
932
1088
  lines.append(
933
1089
  f"Vencimento: {_format_date(item.get('due_date') or item.get('dueDate'))}"
934
1090
  )
1091
+ if item.get("tags"):
1092
+ tags = item.get("tags", [])
1093
+ if tags:
1094
+ lines.append(f"Tags: {', '.join(tags)}")
935
1095
  return "\n".join(lines)
936
1096
 
937
1097
 
@@ -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.7.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