fenix-mcp 0.7.0__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fenix_mcp/__init__.py CHANGED
@@ -8,4 +8,4 @@ Fênix Cloud MCP Server (Python edition).
8
8
  __all__ = ["__version__"]
9
9
 
10
10
 
11
- __version__ = "0.7.0"
11
+ __version__ = "1.1.0"
@@ -21,34 +21,29 @@ class InitializeAction(str, Enum):
21
21
 
22
22
 
23
23
  class InitializeRequest(ToolRequest):
24
- action: InitializeAction = Field(
25
- description="Operação de inicialização a executar."
26
- )
24
+ action: InitializeAction = Field(description="Initialization operation to execute.")
27
25
  include_user_docs: bool = Field(
28
26
  default=True,
29
27
  description=(
30
- "Inclui documentos pessoais durante a inicialização "
31
- "(apenas para ação init)."
28
+ "Include personal documents during initialization (only for init action)."
32
29
  ),
33
30
  )
34
31
  limit: int = Field(
35
32
  default=50,
36
33
  ge=1,
37
34
  le=200,
38
- description=("Quantidade máxima de documentos principais/pessoais carregados."),
35
+ description=("Maximum number of core/personal documents to load."),
39
36
  )
40
37
  answers: Optional[List[str]] = Field(
41
38
  default=None,
42
- description=(
43
- "Lista com 9 respostas textuais para processar o setup personalizado."
44
- ),
39
+ description=("List of 9 text answers to process the personalized setup."),
45
40
  )
46
41
 
47
42
 
48
43
  class InitializeTool(Tool):
49
44
  name = "initialize"
50
45
  description = (
51
- "Inicializa o ambiente do Fênix Cloud ou processa o setup personalizado."
46
+ "Initializes the Fenix Cloud environment or processes the personalized setup."
52
47
  )
53
48
  request_model = InitializeRequest
54
49
 
@@ -61,7 +56,7 @@ class InitializeTool(Tool):
61
56
  return await self._handle_init(payload)
62
57
  if payload.action is InitializeAction.SETUP:
63
58
  return await self._handle_setup(payload)
64
- return text("❌ Ação de inicialização desconhecida.")
59
+ return text("❌ Unknown initialization action.")
65
60
 
66
61
  async def _handle_init(self, payload: InitializeRequest):
67
62
  try:
@@ -72,8 +67,8 @@ class InitializeTool(Tool):
72
67
  except Exception as exc: # pragma: no cover - defensive
73
68
  self._context.logger.error("Initialize failed: %s", exc)
74
69
  return text(
75
- "❌ Falha ao carregar dados de inicialização. "
76
- "Verifique se o token tem acesso à API."
70
+ "❌ Failed to load initialization data. "
71
+ "Verify that the token has API access."
77
72
  )
78
73
 
79
74
  if (
@@ -82,7 +77,7 @@ class InitializeTool(Tool):
82
77
  and not data.profile
83
78
  ):
84
79
  return text(
85
- "⚠️ Não consegui carregar documentos nem perfil. Confirme o token e, se for o primeiro acesso, use `initialize action=setup` para responder ao questionário inicial."
80
+ "⚠️ Could not load documents or profile. Confirm the token and, if this is your first access, use `initialize action=setup` to answer the initial questionnaire."
86
81
  )
87
82
 
88
83
  payload_dict = {
@@ -99,7 +94,7 @@ class InitializeTool(Tool):
99
94
  tenant_info = profile.get("tenant") or {}
100
95
  team_info = profile.get("team") or {}
101
96
 
102
- context_lines = ["📋 **Contexto do Usuário**"]
97
+ context_lines = ["📋 **User Context**"]
103
98
  if user_info.get("id"):
104
99
  context_lines.append(f"- **user_id**: `{user_info['id']}`")
105
100
  if user_info.get("name"):
@@ -115,7 +110,7 @@ class InitializeTool(Tool):
115
110
 
116
111
  message_lines = context_lines + [
117
112
  "",
118
- "📦 **Dados de inicialização completos**",
113
+ "📦 **Complete initialization data**",
119
114
  "```json",
120
115
  json.dumps(payload_dict, ensure_ascii=False, indent=2),
121
116
  "```",
@@ -138,11 +133,11 @@ class InitializeTool(Tool):
138
133
  return text(f"❌ {validation_error}")
139
134
 
140
135
  summary_lines = [
141
- "📝 **Setup personalizado recebido!**",
136
+ "📝 **Personalized setup received!**",
142
137
  "",
143
- "Suas respostas foram registradas. Vou sugerir documentos, regras e rotinas com base nessas informações.",
138
+ "Your answers have been registered. I will suggest documents, rules and routines based on this information.",
144
139
  "",
145
- "Resumo das respostas:",
140
+ "Answer summary:",
146
141
  ]
147
142
  for idx, answer in enumerate(answers, start=1):
148
143
  summary_lines.append(f"{idx}. {answer.strip()}")
@@ -150,7 +145,7 @@ class InitializeTool(Tool):
150
145
  summary_lines.extend(
151
146
  [
152
147
  "",
153
- "Agora você pode pedir conteúdos específicos, por exemplo:",
148
+ "You can now request specific content, for example:",
154
149
  "- `productivity action=todo_create ...`",
155
150
  "- `knowledge action=mode_list`",
156
151
  ]
@@ -32,17 +32,17 @@ class IntelligenceAction(str, Enum):
32
32
 
33
33
  SMART_CREATE = (
34
34
  "memory_smart_create",
35
- "Cria memórias inteligentes com análise de similaridade.",
35
+ "Creates intelligent memories with similarity analysis.",
36
36
  )
37
- QUERY = ("memory_query", "Lista memórias aplicando filtros e busca textual.")
38
- SIMILARITY = ("memory_similarity", "Busca memórias similares a um conteúdo base.")
37
+ QUERY = ("memory_query", "Lists memories applying filters and text search.")
38
+ SIMILARITY = ("memory_similarity", "Finds memories similar to a base content.")
39
39
  CONSOLIDATE = (
40
40
  "memory_consolidate",
41
- "Consolida múltiplas memórias em uma principal.",
41
+ "Consolidates multiple memories into a primary one.",
42
42
  )
43
- UPDATE = ("memory_update", "Atualiza campos de uma memória existente.")
44
- DELETE = ("memory_delete", "Remove uma memória pelo ID.")
45
- HELP = ("memory_help", "Mostra as ações suportadas e seus usos.")
43
+ UPDATE = ("memory_update", "Updates fields of an existing memory.")
44
+ DELETE = ("memory_delete", "Removes a memory by ID.")
45
+ HELP = ("memory_help", "Shows supported actions and their uses.")
46
46
 
47
47
  @classmethod
48
48
  def choices(cls) -> List[str]:
@@ -51,7 +51,7 @@ class IntelligenceAction(str, Enum):
51
51
  @classmethod
52
52
  def formatted_help(cls) -> str:
53
53
  lines = [
54
- "| **Ação** | **Descrição** |",
54
+ "| **Action** | **Description** |",
55
55
  "| --- | --- |",
56
56
  ]
57
57
  for member in cls:
@@ -60,7 +60,7 @@ class IntelligenceAction(str, Enum):
60
60
 
61
61
 
62
62
  ACTION_FIELD_DESCRIPTION = (
63
- "Ação de inteligência a executar. Use um dos valores: "
63
+ "Intelligence action to execute. Use one of the values: "
64
64
  + ", ".join(
65
65
  f"`{member.value}` ({member.description.rstrip('.')})."
66
66
  for member in IntelligenceAction
@@ -70,31 +70,31 @@ ACTION_FIELD_DESCRIPTION = (
70
70
 
71
71
  class IntelligenceRequest(ToolRequest):
72
72
  action: IntelligenceAction = Field(description=ACTION_FIELD_DESCRIPTION)
73
- title: Optional[TitleStr] = Field(default=None, description="Título da memória.")
73
+ title: Optional[TitleStr] = Field(default=None, description="Memory title.")
74
74
  content: Optional[MarkdownStr] = Field(
75
- default=None, description="Conteúdo/texto da memória (Markdown)."
75
+ default=None, description="Memory content/text (Markdown)."
76
76
  )
77
77
  metadata: Optional[MarkdownStr] = Field(
78
78
  default=None,
79
- description="Metadata estruturada da memória (formato pipe, toml compacto, etc.).",
79
+ description="Structured memory metadata (pipe format, compact toml, etc.).",
80
80
  )
81
- context: Optional[str] = Field(default=None, description="Contexto adicional.")
82
- source: Optional[str] = Field(default=None, description="Fonte da memória.")
81
+ context: Optional[str] = Field(default=None, description="Additional context.")
82
+ source: Optional[str] = Field(default=None, description="Memory source.")
83
83
  importance: Optional[str] = Field(
84
84
  default=None,
85
- description="Nível de importância da memória (low, medium, high, critical).",
85
+ description="Memory importance level (low, medium, high, critical).",
86
86
  )
87
87
  include_content: bool = Field(
88
88
  default=False,
89
- description="Retornar conteúdo completo das memórias? Defina true para incluir o texto integral.",
89
+ description="Return full memory content? Set true to include the full text.",
90
90
  )
91
91
  include_metadata: bool = Field(
92
92
  default=False,
93
- description="Retornar metadata completa das memórias? Defina true para incluir o campo bruto.",
93
+ description="Return full memory metadata? Set true to include the raw field.",
94
94
  )
95
95
  tags: Optional[List[TagStr]] = Field(
96
96
  default=None,
97
- description="Tags da memória como array JSON de strings.",
97
+ description='Memory tags. REQUIRED for create. Format: JSON array of strings, e.g.: ["tag1", "tag2"]. Do not use a single string.',
98
98
  json_schema_extra={"example": ["tag1", "tag2", "tag3"]},
99
99
  )
100
100
 
@@ -126,55 +126,51 @@ class IntelligenceRequest(ToolRequest):
126
126
  # For any other type, convert to string and wrap in list
127
127
  return [str(v).strip()] if str(v).strip() else None
128
128
 
129
- limit: int = Field(default=20, ge=1, le=100, description="Limite de resultados.")
130
- offset: int = Field(default=0, ge=0, description="Offset para paginação.")
131
- query: Optional[str] = Field(default=None, description="Termo de busca.")
129
+ limit: int = Field(default=20, ge=1, le=100, description="Result limit.")
130
+ offset: int = Field(default=0, ge=0, description="Pagination offset.")
131
+ query: Optional[str] = Field(default=None, description="Search term.")
132
132
  category: Optional[CategoryStr] = Field(
133
- default=None, description="Categoria para filtro."
133
+ default=None, description="Category for filtering."
134
134
  )
135
135
  date_from: Optional[DateTimeStr] = Field(
136
- default=None, description="Filtro inicial (ISO 8601)."
136
+ default=None, description="Start date filter (ISO 8601)."
137
137
  )
138
138
  date_to: Optional[DateTimeStr] = Field(
139
- default=None, description="Filtro final (ISO 8601)."
139
+ default=None, description="End date filter (ISO 8601)."
140
140
  )
141
141
  threshold: float = Field(
142
- default=0.8, ge=0, le=1, description="Limite mínimo de similaridade."
142
+ default=0.8, ge=0, le=1, description="Minimum similarity threshold."
143
143
  )
144
144
  max_results: int = Field(
145
- default=5, ge=1, le=20, description="Máximo de memórias similares."
145
+ default=5, ge=1, le=20, description="Maximum similar memories."
146
146
  )
147
147
  memory_ids: Optional[List[UUIDStr]] = Field(
148
- default=None, description="IDs das memórias para consolidação (UUIDs)."
148
+ default=None, description="Memory IDs for consolidation (UUIDs)."
149
149
  )
150
- strategy: str = Field(default="merge", description="Estratégia de consolidação.")
151
- time_range: str = Field(
152
- default="month", description="Janela de tempo para analytics."
153
- )
154
- group_by: str = Field(default="category", description="Agrupamento para analytics.")
155
- id: Optional[UUIDStr] = Field(default=None, description="ID da memória (UUID).")
150
+ strategy: str = Field(default="merge", description="Consolidation strategy.")
151
+ time_range: str = Field(default="month", description="Time window for analytics.")
152
+ group_by: str = Field(default="category", description="Grouping for analytics.")
153
+ id: Optional[UUIDStr] = Field(default=None, description="Memory ID (UUID).")
156
154
  documentation_item_id: Optional[UUIDStr] = Field(
157
- default=None, description="ID de documentação relacionada (UUID)."
155
+ default=None, description="Related documentation ID (UUID)."
158
156
  )
159
157
  mode_id: Optional[UUIDStr] = Field(
160
- default=None, description="ID do modo relacionado (UUID)."
158
+ default=None, description="Related mode ID (UUID)."
161
159
  )
162
160
  rule_id: Optional[UUIDStr] = Field(
163
- default=None, description="ID da regra relacionada (UUID)."
161
+ default=None, description="Related rule ID (UUID)."
164
162
  )
165
163
  work_item_id: Optional[UUIDStr] = Field(
166
- default=None, description="ID do work item relacionado (UUID)."
164
+ default=None, description="Related work item ID (UUID)."
167
165
  )
168
166
  sprint_id: Optional[UUIDStr] = Field(
169
- default=None, description="ID do sprint relacionado (UUID)."
167
+ default=None, description="Related sprint ID (UUID)."
170
168
  )
171
169
 
172
170
 
173
171
  class IntelligenceTool(Tool):
174
172
  name = "intelligence"
175
- description = (
176
- "Operações de inteligência do Fênix Cloud (memórias e smart operations)."
177
- )
173
+ description = "Fenix Cloud intelligence operations (memories and smart operations)."
178
174
  request_model = IntelligenceRequest
179
175
 
180
176
  def __init__(self, context: AppContext):
@@ -198,19 +194,19 @@ class IntelligenceTool(Tool):
198
194
  if action is IntelligenceAction.DELETE:
199
195
  return await self._handle_delete(payload)
200
196
  return text(
201
- "❌ Ação inválida para intelligence.\n\nEscolha um dos valores:\n"
197
+ "❌ Invalid action for intelligence.\n\nChoose one of the values:\n"
202
198
  + "\n".join(f"- `{value}`" for value in IntelligenceAction.choices())
203
199
  )
204
200
 
205
201
  async def _handle_smart_create(self, payload: IntelligenceRequest):
206
202
  if not payload.title or not payload.content:
207
- return text("❌ Informe título e conteúdo para criar uma memória.")
203
+ return text("❌ Provide title and content to create a memory.")
208
204
 
209
205
  if not payload.metadata or not payload.metadata.strip():
210
- return text("❌ Informe metadata para criar uma memória.")
206
+ return text("❌ Provide metadata to create a memory.")
211
207
 
212
208
  if not payload.source or not payload.source.strip():
213
- return text("❌ Informe source para criar uma memória.")
209
+ return text("❌ Provide source to create a memory.")
214
210
 
215
211
  try:
216
212
  normalized_tags = _ensure_tag_sequence(payload.tags)
@@ -218,7 +214,7 @@ class IntelligenceTool(Tool):
218
214
  return text(f"❌ {exc}")
219
215
 
220
216
  if not normalized_tags or len(normalized_tags) == 0:
221
- return text("❌ Informe tags para criar uma memória.")
217
+ return text("❌ Provide tags to create a memory.")
222
218
 
223
219
  memory = await self._service.smart_create_memory(
224
220
  title=payload.title,
@@ -230,12 +226,12 @@ class IntelligenceTool(Tool):
230
226
  tags=normalized_tags,
231
227
  )
232
228
  lines = [
233
- "🧠 **Memória criada com sucesso!**",
229
+ "🧠 **Memory created successfully!**",
234
230
  f"ID: {memory.get('memoryId') or memory.get('id', 'N/A')}",
235
- f"Ação: {memory.get('action') or 'criado'}",
236
- f"Similaridade: {format_percentage(memory.get('similarity'))}",
237
- f"Tags: {', '.join(memory.get('tags', [])) or 'Automáticas'}",
238
- f"Categoria: {memory.get('category') or 'Automática'}",
231
+ f"Action: {memory.get('action') or 'created'}",
232
+ f"Similarity: {format_percentage(memory.get('similarity'))}",
233
+ f"Tags: {', '.join(memory.get('tags', [])) or 'Automatic'}",
234
+ f"Category: {memory.get('category') or 'Automatic'}",
239
235
  ]
240
236
  return text("\n".join(lines))
241
237
 
@@ -258,44 +254,44 @@ class IntelligenceTool(Tool):
258
254
  importance=payload.importance,
259
255
  )
260
256
  if not memories:
261
- return text("🧠 Nenhuma memória encontrada.")
257
+ return text("🧠 No memories found.")
262
258
  body = "\n\n".join(_format_memory(mem) for mem in memories)
263
- return text(f"🧠 **Memórias ({len(memories)}):**\n\n{body}")
259
+ return text(f"🧠 **Memories ({len(memories)}):**\n\n{body}")
264
260
 
265
261
  async def _handle_similarity(self, payload: IntelligenceRequest):
266
262
  if not payload.content:
267
- return text("❌ Informe o conteúdo base para comparar similitude.")
263
+ return text("❌ Provide the base content to compare similarity.")
268
264
  memories = await self._service.similar_memories(
269
265
  content=payload.content,
270
266
  threshold=payload.threshold,
271
267
  max_results=payload.max_results,
272
268
  )
273
269
  if not memories:
274
- return text("🔍 Nenhuma memória similar encontrada.")
270
+ return text("🔍 No similar memories found.")
275
271
  body = "\n\n".join(
276
- f"🔍 **{mem.get('title', 'Sem título')}**\n Similaridade: {format_percentage(mem.get('finalScore'))}\n ID: {mem.get('memoryId', 'N/A')}"
272
+ f"🔍 **{mem.get('title', 'Untitled')}**\n Similarity: {format_percentage(mem.get('finalScore'))}\n ID: {mem.get('memoryId', 'N/A')}"
277
273
  for mem in memories
278
274
  )
279
- return text(f"🔍 **Memórias similares ({len(memories)}):**\n\n{body}")
275
+ return text(f"🔍 **Similar memories ({len(memories)}):**\n\n{body}")
280
276
 
281
277
  async def _handle_consolidate(self, payload: IntelligenceRequest):
282
278
  if not payload.memory_ids or len(payload.memory_ids) < 2:
283
- return text("❌ Informe ao menos 2 IDs de memória para consolidar.")
279
+ return text("❌ Provide at least 2 memory IDs to consolidate.")
284
280
  result = await self._service.consolidate_memories(
285
281
  memory_ids=payload.memory_ids,
286
282
  strategy=payload.strategy,
287
283
  )
288
284
  lines = [
289
- "🔄 **Consolidação concluída!**",
290
- f"Memória principal: {result.get('primary_memory_id', 'N/A')}",
291
- f"Consolidadas: {result.get('consolidated_count', 'N/A')}",
292
- f"Ação executada: {result.get('action', 'N/A')}",
285
+ "🔄 **Consolidation complete!**",
286
+ f"Primary memory: {result.get('primary_memory_id', 'N/A')}",
287
+ f"Consolidated: {result.get('consolidated_count', 'N/A')}",
288
+ f"Action executed: {result.get('action', 'N/A')}",
293
289
  ]
294
290
  return text("\n".join(lines))
295
291
 
296
292
  async def _handle_update(self, payload: IntelligenceRequest):
297
293
  if not payload.id:
298
- return text("❌ Informe o ID da memória para atualização.")
294
+ return text("❌ Provide the memory ID for update.")
299
295
  existing = await self._service.get_memory(
300
296
  payload.id, include_content=False, include_metadata=True
301
297
  )
@@ -326,23 +322,23 @@ class IntelligenceTool(Tool):
326
322
  return text(
327
323
  "\n".join(
328
324
  [
329
- "✅ **Memória atualizada!**",
325
+ "✅ **Memory updated!**",
330
326
  f"ID: {memory.get('id', payload.id)}",
331
- f"Título: {memory.get('title', 'N/A')}",
332
- f"Prioridade: {memory.get('priority_score', 'N/A')}",
327
+ f"Title: {memory.get('title', 'N/A')}",
328
+ f"Priority: {memory.get('priority_score', 'N/A')}",
333
329
  ]
334
330
  )
335
331
  )
336
332
 
337
333
  async def _handle_delete(self, payload: IntelligenceRequest):
338
334
  if not payload.id:
339
- return text("❌ Informe o ID da memória para remover.")
335
+ return text("❌ Provide the memory ID to remove.")
340
336
  await self._service.delete_memory(payload.id)
341
- return text(f"🗑️ Memória {payload.id} removida com sucesso.")
337
+ return text(f"🗑️ Memory {payload.id} removed successfully.")
342
338
 
343
339
  async def _handle_help(self):
344
340
  return text(
345
- "📚 **Ações disponíveis para intelligence**\n\n"
341
+ "📚 **Available actions for intelligence**\n\n"
346
342
  + IntelligenceAction.formatted_help()
347
343
  )
348
344
 
@@ -350,12 +346,12 @@ class IntelligenceTool(Tool):
350
346
  def _format_memory(memory: Dict[str, Any]) -> str:
351
347
  return "\n".join(
352
348
  [
353
- f"🧠 **{memory.get('title', 'Sem título')}**",
349
+ f"🧠 **{memory.get('title', 'Untitled')}**",
354
350
  f"ID: {memory.get('id', memory.get('memoryId', 'N/A'))}",
355
- f"Categoria: {memory.get('category', 'N/A')}",
356
- f"Tags: {', '.join(memory.get('tags', [])) or 'Nenhuma'}",
357
- f"Importância: {memory.get('importance', 'N/A')}",
358
- f"Acessos: {memory.get('access_count', 'N/A')}",
351
+ f"Category: {memory.get('category', 'N/A')}",
352
+ f"Tags: {', '.join(memory.get('tags', [])) or 'None'}",
353
+ f"Importance: {memory.get('importance', 'N/A')}",
354
+ f"Accesses: {memory.get('access_count', 'N/A')}",
359
355
  ]
360
356
  )
361
357
 
@@ -385,7 +381,7 @@ def _ensure_tag_sequence(raw: Optional[Any]) -> Optional[List[str]]:
385
381
  pass
386
382
 
387
383
  raise ValueError(
388
- "O campo `tags` deve ser enviado como array JSON, por exemplo: "
384
+ "The `tags` field must be sent as a JSON array, for example: "
389
385
  '["tag1", "tag2"].'
390
386
  )
391
387
  return [str(raw).strip()]