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.
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/PKG-INFO +1 -1
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/__init__.py +1 -1
- fenix_mcp-0.6.0/fenix_mcp/application/tool_base.py +140 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/intelligence.py +50 -52
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/knowledge.py +200 -47
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/productivity.py +26 -10
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/user_config.py +18 -5
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/domain/intelligence.py +3 -30
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp.egg-info/PKG-INFO +1 -1
- fenix_mcp-0.5.12/fenix_mcp/application/tool_base.py +0 -47
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/README.md +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/presenters.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tool_registry.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/__init__.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/health.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/application/tools/initialize.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/domain/initialization.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/domain/knowledge.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/domain/productivity.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/domain/user_config.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/infrastructure/config.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/infrastructure/context.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/infrastructure/fenix_api/client.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/infrastructure/http_client.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/infrastructure/logging.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/interface/mcp_server.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/interface/transports.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp/main.py +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp.egg-info/SOURCES.txt +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp.egg-info/dependency_links.txt +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp.egg-info/entry_points.txt +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp.egg-info/requires.txt +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/fenix_mcp.egg-info/top_level.txt +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/pyproject.toml +0 -0
- {fenix_mcp-0.5.12 → fenix_mcp-0.6.0}/setup.cfg +0 -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
|
|
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[
|
|
66
|
-
content: Optional[
|
|
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[
|
|
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,
|
|
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[
|
|
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[
|
|
124
|
-
|
|
125
|
-
|
|
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[
|
|
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[
|
|
141
|
-
documentation_item_id: Optional[
|
|
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[
|
|
145
|
-
|
|
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
|
-
|
|
150
|
-
default=None, description="ID
|
|
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
|
|
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[
|
|
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[
|
|
139
|
-
default=None, description="ID do time para filtros (
|
|
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
|
-
|
|
142
|
-
|
|
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[
|
|
159
|
-
|
|
160
|
-
|
|
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,
|
|
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[
|
|
172
|
-
|
|
173
|
-
|
|
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[
|
|
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[
|
|
183
|
-
|
|
184
|
-
|
|
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[
|
|
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[
|
|
260
|
+
doc_title: Optional[TitleStr] = Field(
|
|
192
261
|
default=None, description="Título da documentação."
|
|
193
262
|
)
|
|
194
|
-
doc_description: Optional[
|
|
195
|
-
|
|
196
|
-
|
|
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,
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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[
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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[
|
|
299
|
+
doc_emoji: Optional[EmojiStr] = Field(
|
|
218
300
|
default=None, description="Emoji exibido junto ao documento."
|
|
219
301
|
)
|
|
220
|
-
doc_emote: Optional[
|
|
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
|
-
"
|
|
692
|
-
"
|
|
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
|
-
"
|
|
741
|
-
"
|
|
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
|
|
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[
|
|
63
|
-
title: Optional[
|
|
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[
|
|
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(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
default=None, description="
|
|
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
|
|
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[
|
|
55
|
-
name: Optional[
|
|
56
|
-
content: Optional[
|
|
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[
|
|
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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|