fenix-mcp 0.4.0__py3-none-any.whl → 0.5.1__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
@@ -17,4 +17,4 @@ Updated with improved error handling and better documentation.
17
17
 
18
18
  __all__ = ["__version__"]
19
19
 
20
- __version__ = "0.4.0"
20
+ __version__ = "0.5.1"
@@ -10,7 +10,7 @@ from pydantic import Field
10
10
 
11
11
  from fenix_mcp.application.presenters import text
12
12
  from fenix_mcp.application.tool_base import Tool, ToolRequest
13
- from fenix_mcp.domain.intelligence import IntelligenceService
13
+ from fenix_mcp.domain.intelligence import IntelligenceService, build_metadata
14
14
  from fenix_mcp.infrastructure.context import AppContext
15
15
 
16
16
 
@@ -66,10 +66,14 @@ class IntelligenceRequest(ToolRequest):
66
66
  content: Optional[str] = Field(
67
67
  default=None, description="Conteúdo/texto da memória."
68
68
  )
69
+ metadata: Optional[str] = Field(
70
+ default=None,
71
+ description="Metadata estruturada da memória (formato pipe, toml compacto, etc.).",
72
+ )
69
73
  context: Optional[str] = Field(default=None, description="Contexto adicional.")
70
74
  source: Optional[str] = Field(default=None, description="Fonte da memória.")
71
- importance: str = Field(
72
- default="medium", description="Nível de importância da memória."
75
+ importance: Optional[str] = Field(
76
+ default=None, description="Nível de importância da memória."
73
77
  )
74
78
  include_content: bool = Field(
75
79
  default=False,
@@ -151,13 +155,15 @@ class IntelligenceTool(Tool):
151
155
  async def _handle_smart_create(self, payload: IntelligenceRequest):
152
156
  if not payload.title or not payload.content:
153
157
  return text("❌ Informe título e conteúdo para criar uma memória.")
158
+ normalized_tags = _ensure_tag_sequence(payload.tags)
154
159
  memory = await self._service.smart_create_memory(
155
160
  title=payload.title,
156
161
  content=payload.content,
162
+ metadata=payload.metadata,
157
163
  context=payload.context,
158
164
  source=payload.source,
159
165
  importance=payload.importance,
160
- tags=payload.tags,
166
+ tags=normalized_tags,
161
167
  )
162
168
  lines = [
163
169
  "🧠 **Memória criada com sucesso!**",
@@ -251,10 +257,21 @@ class IntelligenceTool(Tool):
251
257
  async def _handle_update(self, payload: IntelligenceRequest):
252
258
  if not payload.id:
253
259
  return text("❌ Informe o ID da memória para atualização.")
260
+ existing = await self._service.get_memory(
261
+ payload.id, include_content=False, include_metadata=True
262
+ )
263
+ normalized_tags = _ensure_tag_sequence(payload.tags)
264
+ metadata = build_metadata(
265
+ payload.metadata,
266
+ importance=payload.importance,
267
+ tags=normalized_tags,
268
+ existing=existing.get("metadata") if isinstance(existing, dict) else None,
269
+ )
254
270
  update_fields: Dict[str, Any] = {
255
271
  "title": payload.title,
256
272
  "content": payload.content,
257
- "tags": payload.tags,
273
+ "metadata": metadata,
274
+ "tags": normalized_tags,
258
275
  "documentation_item_id": payload.documentation_item_id,
259
276
  "mode_id": payload.mode_id,
260
277
  "rule_id": payload.rule_id,
@@ -298,3 +315,15 @@ def format_percentage(value: Optional[float]) -> str:
298
315
  if value is None:
299
316
  return "N/A"
300
317
  return f"{value * 100:.1f}%"
318
+
319
+
320
+ def _ensure_tag_sequence(raw: Optional[Any]) -> Optional[List[str]]:
321
+ if raw is None or raw == "":
322
+ return None
323
+ if isinstance(raw, (list, tuple, set)):
324
+ result = [str(item).strip() for item in raw if str(item).strip()]
325
+ return result or None
326
+ if isinstance(raw, str):
327
+ items = [part.strip() for part in raw.split(",") if part.strip()]
328
+ return items or None
329
+ return [str(raw).strip()]
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
 
6
6
  import asyncio
7
7
  from dataclasses import dataclass
8
- from typing import Any, Dict, Iterable, List, Optional
8
+ from typing import Any, Dict, Iterable, List, Optional, Union
9
9
 
10
10
  from fenix_mcp.infrastructure.fenix_api.client import FenixApiClient
11
11
 
@@ -20,23 +20,27 @@ class IntelligenceService:
20
20
  *,
21
21
  title: str,
22
22
  content: str,
23
+ metadata: Optional[str],
23
24
  context: Optional[str],
24
25
  source: Optional[str],
25
26
  importance: str,
26
- tags: Optional[Iterable[str]] = None,
27
+ tags: Optional[Union[Iterable[str], str]] = None,
27
28
  ) -> Dict[str, Any]:
28
- metadata_parts: List[str] = []
29
- if context:
30
- metadata_parts.append(f"Contexto: {context}")
31
- if source:
32
- metadata_parts.append(f"Fonte: {source}")
33
-
29
+ importance_value = importance or "medium"
30
+ normalized_tags = normalize_tags(tags)
31
+ metadata_str = build_metadata(
32
+ metadata,
33
+ importance=importance_value,
34
+ tags=normalized_tags,
35
+ context=context,
36
+ source=source,
37
+ )
34
38
  payload = {
35
39
  "title": title,
36
40
  "content": content,
37
- "metadata": "\n".join(metadata_parts) if metadata_parts else None,
38
- "priority_score": _importance_to_priority(importance),
39
- "tags": list(tags) if tags else None,
41
+ "metadata": metadata_str,
42
+ "priority_score": _importance_to_priority(importance_value),
43
+ "tags": normalized_tags,
40
44
  }
41
45
  return await self._call(self.api.smart_create_memory, _strip_none(payload))
42
46
 
@@ -145,6 +149,20 @@ class IntelligenceService:
145
149
  async def _call(self, func, *args, **kwargs):
146
150
  return await asyncio.to_thread(func, *args, **kwargs)
147
151
 
152
+ async def get_memory(
153
+ self,
154
+ memory_id: str,
155
+ *,
156
+ include_content: bool = False,
157
+ include_metadata: bool = False,
158
+ ) -> Dict[str, Any]:
159
+ return await self._call(
160
+ self.api.get_memory,
161
+ memory_id,
162
+ include_content=include_content,
163
+ include_metadata=include_metadata,
164
+ )
165
+
148
166
 
149
167
  def _importance_to_priority(importance: Optional[str]) -> float:
150
168
  mapping = {
@@ -176,3 +194,96 @@ def _coerce_bool(value: Any, default: bool = False) -> bool:
176
194
 
177
195
  def _strip_none(data: Dict[str, Any]) -> Dict[str, Any]:
178
196
  return {key: value for key, value in data.items() if value not in (None, "")}
197
+
198
+
199
+ def build_metadata(
200
+ explicit: Optional[str],
201
+ *,
202
+ importance: Optional[str],
203
+ tags: Optional[Union[Iterable[str], str]],
204
+ context: Optional[str] = None,
205
+ source: Optional[str] = None,
206
+ existing: Optional[str] = None,
207
+ ) -> str:
208
+ if explicit and explicit.strip():
209
+ return explicit.strip()
210
+
211
+ existing_map = _parse_metadata(existing) if existing else {}
212
+ metadata_map: Dict[str, str] = {}
213
+
214
+ metadata_map["t"] = existing_map.get("t", "memory")
215
+ metadata_map["src"] = _slugify(source) if source else existing_map.get("src", "mcp")
216
+
217
+ ctx_value = _slugify(context) if context else existing_map.get("ctx")
218
+ if ctx_value:
219
+ metadata_map["ctx"] = ctx_value
220
+
221
+ priority_key = importance.lower() if importance else existing_map.get("p")
222
+ if priority_key:
223
+ metadata_map["p"] = priority_key
224
+
225
+ tag_string = _format_tags(tags)
226
+ if tag_string:
227
+ metadata_map["tags"] = tag_string
228
+ elif "tags" in existing_map:
229
+ metadata_map["tags"] = existing_map["tags"]
230
+
231
+ for key, value in existing_map.items():
232
+ if key not in metadata_map:
233
+ metadata_map[key] = value
234
+
235
+ if not metadata_map:
236
+ metadata_map["t"] = "memory"
237
+ metadata_map["src"] = "mcp"
238
+
239
+ return "|".join(f"{key}:{metadata_map[key]}" for key in metadata_map)
240
+
241
+
242
+ def _parse_metadata(metadata: str) -> Dict[str, str]:
243
+ items = {}
244
+ for entry in metadata.split("|"):
245
+ if ":" not in entry:
246
+ continue
247
+ key, value = entry.split(":", 1)
248
+ key = key.strip()
249
+ value = value.strip()
250
+ if key:
251
+ items[key] = value
252
+ return items
253
+
254
+
255
+ def _slugify(value: Optional[str]) -> str:
256
+ if not value:
257
+ return ""
258
+ sanitized = value.replace("|", " ").replace(":", " ")
259
+ return "-".join(part for part in sanitized.split() if part).lower()
260
+
261
+
262
+ def _format_tags(tags: Optional[Union[Iterable[str], str]]) -> str:
263
+ if tags is None or tags == "":
264
+ return ""
265
+ if isinstance(tags, str):
266
+ iterable: Iterable[str] = [part.strip() for part in tags.split(",")]
267
+ else:
268
+ iterable = tags
269
+ normalized = {
270
+ _slugify(tag) for tag in iterable if isinstance(tag, str) and _slugify(tag)
271
+ }
272
+ if not normalized:
273
+ return ""
274
+ return ",".join(sorted(normalized))
275
+
276
+
277
+ def normalize_tags(tags: Optional[Union[Iterable[str], str]]) -> Optional[List[str]]:
278
+ if tags is None or tags == "":
279
+ return None
280
+ if isinstance(tags, str):
281
+ parts = [part.strip() for part in tags.split(",") if part.strip()]
282
+ else:
283
+ parts = [str(part).strip() for part in tags if str(part).strip()]
284
+ if not parts:
285
+ return None
286
+ slugged = [_slugify(part) for part in parts if _slugify(part)]
287
+ if not slugged:
288
+ return None
289
+ return sorted(set(slugged))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fenix-mcp
3
- Version: 0.4.0
3
+ Version: 0.5.1
4
4
  Summary: Fênix Cloud MCP server implemented in Python
5
5
  Author: Fenix Inc
6
6
  Requires-Python: >=3.10
@@ -1,4 +1,4 @@
1
- fenix_mcp/__init__.py,sha256=RC_GHV0oxTvs3Y2i7XSIUtYYtjwepfTw40Tcnx2CdfU,615
1
+ fenix_mcp/__init__.py,sha256=Nle0-p8uRDw1ypGPxnoKnAY0Mv0N8_6glE1whGPNnlw,615
2
2
  fenix_mcp/main.py,sha256=iJV-9btNMDJMObvcn7wBQdbLLKjkYCQ1ANGEwHGHlMU,2857
3
3
  fenix_mcp/application/presenters.py,sha256=fGME54PdCDhTBhXO-JUB9yLdBHiE1aeXLTC2fCuxnxM,689
4
4
  fenix_mcp/application/tool_base.py,sha256=qUcb46qx9gHQfrSHgj4RD4NCHW-OIvKQdR5G9uxZ5l4,1316
@@ -6,12 +6,12 @@ fenix_mcp/application/tool_registry.py,sha256=bPT5g8GfxG_qu28R1WaDOZHvtmG6TPDvZi
6
6
  fenix_mcp/application/tools/__init__.py,sha256=Gi1YvYh-KdL9HD8gLVrknHrxiKKEOhHBEZ02KBXJaKQ,796
7
7
  fenix_mcp/application/tools/health.py,sha256=m5DxhoRbdwl6INzd6PISxv1NAv-ljCrezsr773VB0wE,834
8
8
  fenix_mcp/application/tools/initialize.py,sha256=f33DNDn9u8IYwpqiBj6bjJ-wHgaUP1zEuAvUM1rMYPc,4674
9
- fenix_mcp/application/tools/intelligence.py,sha256=oRGmmaeRJG_6ckZaByAdZbMKmr2E1cZPxGeFwSFaCTE,12761
9
+ fenix_mcp/application/tools/intelligence.py,sha256=DNWGu6NhPYZXahnHDVHIgYYdWn_jCP-Nw9FwS_Tat34,13945
10
10
  fenix_mcp/application/tools/knowledge.py,sha256=4ClGoFRqyFIPuzzg2DAg-w2eMvTP37mH0THXDGftinw,44634
11
11
  fenix_mcp/application/tools/productivity.py,sha256=2IMkNdZ-Kd1CFAO7geruAVjtf_BWoDdbnwkl76vhtC8,9973
12
12
  fenix_mcp/application/tools/user_config.py,sha256=8mPOZuwszO0TapxgrA7Foe15VQE874_mvfQYlGzyv4Y,6230
13
13
  fenix_mcp/domain/initialization.py,sha256=AZhdSNITQ7O3clELBuqGvjJc-c8pFKc7zQz-XR2xXPc,6933
14
- fenix_mcp/domain/intelligence.py,sha256=rOQWxtEceTWGlnWMfUXrBTzjt-yATrMPgFktWF1L5QA,5707
14
+ fenix_mcp/domain/intelligence.py,sha256=IyZUA_fbCHDgHaWCgxkahXAaAT5qQlaz5AT7Cw2Gwko,9006
15
15
  fenix_mcp/domain/knowledge.py,sha256=fKQOTt20u5aa5Yo7gPeQ1Qxa_K5pBxn1yn8FEfOFltM,20241
16
16
  fenix_mcp/domain/productivity.py,sha256=nmHRuVJGRRu1s4eMoAv8vXHKsSauCPl-FvFx3I_yCTE,6661
17
17
  fenix_mcp/domain/user_config.py,sha256=LzBDCk31gLMtKHTbBmYb9VoFPHDW6OydpmDXeHHd0Mw,1642
@@ -22,8 +22,8 @@ fenix_mcp/infrastructure/logging.py,sha256=bHrWlSi_0HshRe3--BK_5nzUszW-gh37q6jsd
22
22
  fenix_mcp/infrastructure/fenix_api/client.py,sha256=Navi7cGAOradghcbYkFIQQINpjFrdINlNhdfZ4iSSYQ,28338
23
23
  fenix_mcp/interface/mcp_server.py,sha256=5UM2NJuNbwHkmCEprIFataJ5nFZiO8efTtP_oW3_iX0,2331
24
24
  fenix_mcp/interface/transports.py,sha256=PxdhfjH8UMl03f7nuCLc-M6tMx6-Y-btVz_mSqXKrSI,8138
25
- fenix_mcp-0.4.0.dist-info/METADATA,sha256=fIm7WGYy9JK5VwYIlc_iFWgLVh6XuNFvGJKkPrjS3O0,7260
26
- fenix_mcp-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- fenix_mcp-0.4.0.dist-info/entry_points.txt,sha256=o52x_YHBupEd-1Z1GSfUjv3gJrx5_I-EkHhCgt1WBaE,49
28
- fenix_mcp-0.4.0.dist-info/top_level.txt,sha256=2G1UtKpwjaIGQyE7sRoHecxaGLeuexfjrOUjv9DDKh4,10
29
- fenix_mcp-0.4.0.dist-info/RECORD,,
25
+ fenix_mcp-0.5.1.dist-info/METADATA,sha256=yvg8J1G-jemzju-BKR2tMd8Y_f5KKjHX6GrNQIONjR0,7260
26
+ fenix_mcp-0.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ fenix_mcp-0.5.1.dist-info/entry_points.txt,sha256=o52x_YHBupEd-1Z1GSfUjv3gJrx5_I-EkHhCgt1WBaE,49
28
+ fenix_mcp-0.5.1.dist-info/top_level.txt,sha256=2G1UtKpwjaIGQyE7sRoHecxaGLeuexfjrOUjv9DDKh4,10
29
+ fenix_mcp-0.5.1.dist-info/RECORD,,