fenix-mcp 1.14.0__py3-none-any.whl → 2.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.
@@ -81,7 +81,7 @@ class KnowledgeAction(str, Enum):
81
81
  )
82
82
  WORK_MINE = (
83
83
  "work_mine",
84
- "Lists work items assigned to the current user. Automatically excludes items with status 'done' or 'cancelled'. Supports pagination via limit and offset parameters.",
84
+ "START HERE for 'my tasks' or 'what am I working on'. Lists work items assigned to current user. Excludes done/cancelled. Supports limit and offset.",
85
85
  )
86
86
  WORK_BULK_CREATE = (
87
87
  "work_bulk_create",
@@ -150,7 +150,7 @@ class KnowledgeAction(str, Enum):
150
150
  )
151
151
  DOC_GET = (
152
152
  "doc_get",
153
- "Reads document content by ID. Get the ID from doc_full_tree or doc_children first.",
153
+ "Reads document content by ID. PREREQUISITE: Get the ID from doc_full_tree first. Do NOT guess IDs.",
154
154
  )
155
155
  DOC_UPDATE = ("doc_update", "Updates a documentation item.")
156
156
  DOC_DELETE = ("doc_delete", "Removes a documentation item.")
@@ -167,13 +167,27 @@ class KnowledgeAction(str, Enum):
167
167
  DOC_TREE = ("doc_tree", "Retrieves tree starting from a specific document.")
168
168
  DOC_FULL_TREE = (
169
169
  "doc_full_tree",
170
- "Retrieves complete documentation tree. Start here to find documents.",
170
+ "START HERE when looking for documentation. Returns complete folder structure with IDs. You NEED the ID from this to use doc_get. Workflow: 1) doc_full_tree to see all docs with IDs, 2) doc_get(id) to read content.",
171
171
  )
172
172
  DOC_MOVE = ("doc_move", "Moves a document to another parent.")
173
173
  DOC_PUBLISH = ("doc_publish", "Changes publication status of a document.")
174
174
  DOC_VERSION = ("doc_version", "Generates or retrieves a document version.")
175
175
  DOC_DUPLICATE = ("doc_duplicate", "Duplicates an existing document.")
176
176
 
177
+ # API Catalog
178
+ API_CATALOG_SEARCH = (
179
+ "api_catalog_search",
180
+ "Semantic search to find APIs by natural language query (e.g., 'API de usuários').",
181
+ )
182
+ API_CATALOG_LIST = (
183
+ "api_catalog_list",
184
+ "Lists API specifications with optional filters (status, tags).",
185
+ )
186
+ API_CATALOG_GET = (
187
+ "api_catalog_get",
188
+ "Gets full details of an API including all endpoints and environments.",
189
+ )
190
+
177
191
  HELP = ("knowledge_help", "Shows available actions and their uses.")
178
192
 
179
193
  @classmethod
@@ -191,10 +205,73 @@ class KnowledgeAction(str, Enum):
191
205
  return "\n".join(lines)
192
206
 
193
207
 
194
- ACTION_FIELD_DESCRIPTION = "Knowledge action. Choose one of the values: " + ", ".join(
195
- f"`{member.value}` ({member.description.rstrip('.')})."
196
- for member in KnowledgeAction
197
- )
208
+ ACTION_FIELD_DESCRIPTION = """Knowledge action grouped by category:
209
+
210
+ **WORK ITEMS** (tasks, bugs, features):
211
+ - `work_mine`: YOUR assigned tasks (START HERE for "my tasks")
212
+ - `work_list`: List with filters (status, priority, type)
213
+ - `work_get`: Get details by ID or key (use work_key for "FENIX-123")
214
+ - `work_create`: Create new (requires: work_title, work_category)
215
+ - `work_update`: Update fields (title, description, priority, story_points, tags, due_date)
216
+ - `work_search`: Search by text
217
+ - `work_bulk_create`: Create multiple with hierarchy
218
+ - `work_backlog`: Team backlog items
219
+ - `work_children`: Child items of a parent
220
+ - `work_by_sprint`: Items in a sprint
221
+ - `work_by_board`: Items in a board
222
+ - `work_by_epic`: Items in an epic
223
+ - `work_status_update`: Update only status
224
+ - `work_assign_sprint`: Assign items to sprint
225
+ - `work_assign_to_me`: Assign item to yourself
226
+ - `work_analytics`: Work item metrics
227
+
228
+ **DOCUMENTATION**:
229
+ - `doc_full_tree`: Complete tree (START HERE to find docs and get IDs)
230
+ - `doc_get`: Read content (needs ID from doc_full_tree)
231
+ - `doc_children`: Folder contents
232
+ - `doc_create`: Create new doc (requires: doc_title, doc_emoji for non-folders)
233
+ - `doc_update`: Update doc
234
+ - `doc_delete`: Remove doc
235
+ - `doc_roots`: Root folders
236
+ - `doc_recent`: Recently accessed
237
+ - `doc_tree`: Tree from specific doc
238
+ - `doc_move`: Move to another parent
239
+ - `doc_publish`: Change publication status
240
+ - `doc_version`: Create/get version
241
+ - `doc_duplicate`: Duplicate doc
242
+ - `doc_analytics`: Doc metrics
243
+
244
+ **SPRINTS**:
245
+ - `sprint_active`: Current sprint (START HERE)
246
+ - `sprint_list`: All sprints
247
+ - `sprint_get`: Sprint details
248
+ - `sprint_by_team`: Team sprints
249
+ - `sprint_work_items`: Items in sprint
250
+
251
+ **BOARDS**:
252
+ - `board_list`: All boards
253
+ - `board_get`: Board details
254
+ - `board_by_team`: Team boards
255
+ - `board_favorites`: Favorite boards
256
+ - `board_columns`: Board columns
257
+
258
+ **RULES** (coding standards):
259
+ - `rule_list`: Team rules
260
+ - `rule_get`: Rule content
261
+ - `rule_create`: Create rule (requires: rule_scope, rule_name, rule_content)
262
+ - `rule_update`: Update rule
263
+ - `rule_delete`: Remove rule
264
+ - `rule_export`: Export for IDE (cursor, claude, copilot, windsurf)
265
+ - `rule_marketplace`: Public rules
266
+ - `rule_fork`: Fork a rule
267
+
268
+ **API CATALOG**:
269
+ - `api_catalog_search`: Semantic search for APIs (natural language)
270
+ - `api_catalog_list`: List all APIs
271
+ - `api_catalog_get`: API details with endpoints
272
+
273
+ `knowledge_help`: Show all actions with descriptions
274
+ """
198
275
 
199
276
 
200
277
  _ALLOWED_DOC_TYPES = {
@@ -376,12 +453,44 @@ class KnowledgeRequest(ToolRequest):
376
453
  default=None, description="Whether the rule is active."
377
454
  )
378
455
 
456
+ # API Catalog fields
457
+ api_spec_id: Optional[UUIDStr] = Field(
458
+ default=None,
459
+ description="API specification ID (UUID). Can also use 'id' field instead.",
460
+ )
461
+
379
462
 
380
463
  class KnowledgeTool(Tool):
381
464
  name = "knowledge"
382
- description = (
383
- "Fenix Cloud knowledge operations (Work Items, Boards, Sprints, Rules, Docs)."
384
- )
465
+ description = """Fenix knowledge base - use this tool when user asks about:
466
+
467
+ WORK ITEMS (tasks, bugs, features):
468
+ - "what are my tasks?" -> action: work_mine
469
+ - "show task FENIX-123" -> action: work_get, work_key: "FENIX-123"
470
+ - "what's in the backlog?" -> action: work_backlog
471
+ - "create a task" -> action: work_create (requires: work_title, work_category)
472
+
473
+ DOCUMENTATION:
474
+ - "find docs about X" -> action: doc_full_tree (START HERE to get IDs)
475
+ - "read the architecture doc" -> action: doc_get (needs ID from doc_full_tree)
476
+
477
+ SPRINTS:
478
+ - "current sprint?" -> action: sprint_active
479
+ - "sprint items?" -> action: sprint_work_items
480
+
481
+ RULES (coding standards):
482
+ - "team coding standards?" -> action: rule_list
483
+ - "export rules for cursor" -> action: rule_export
484
+
485
+ API CATALOG:
486
+ - "find user API" -> action: api_catalog_search
487
+ - "list all APIs" -> action: api_catalog_list
488
+
489
+ IMPORTANT WORKFLOWS:
490
+ 1. For MY tasks: Always use work_mine first
491
+ 2. For docs: Always use doc_full_tree first to get IDs, then doc_get
492
+ 3. For work item by key: Use work_key parameter (e.g., "FENIX-123")
493
+ """
385
494
  request_model = KnowledgeRequest
386
495
 
387
496
  def __init__(self, context: AppContext):
@@ -402,6 +511,8 @@ class KnowledgeTool(Tool):
402
511
  return await self._run_rule(payload)
403
512
  if action.value.startswith("doc_"):
404
513
  return await self._run_doc(payload)
514
+ if action.value.startswith("api_catalog_"):
515
+ return await self._run_api_catalog(payload)
405
516
  return text(
406
517
  "❌ Invalid action for knowledge.\n\nChoose one of the values:\n"
407
518
  + "\n".join(f"- `{value}`" for value in KnowledgeAction.choices())
@@ -1154,6 +1265,63 @@ class KnowledgeTool(Tool):
1154
1265
  )
1155
1266
  )
1156
1267
 
1268
+ # ------------------------------------------------------------------
1269
+ # API Catalog
1270
+ # ------------------------------------------------------------------
1271
+ async def _run_api_catalog(self, payload: KnowledgeRequest):
1272
+ action = payload.action
1273
+
1274
+ if action is KnowledgeAction.API_CATALOG_SEARCH:
1275
+ query = sanitize_null(payload.query)
1276
+ if not query:
1277
+ return text("❌ Provide query to search APIs.")
1278
+
1279
+ result = await self._service.api_catalog_search(
1280
+ query=query,
1281
+ limit=payload.limit,
1282
+ )
1283
+
1284
+ data = result.get("data", [])
1285
+ if not data:
1286
+ return text(
1287
+ f"🔍 No APIs found for: **{query}**\n\n"
1288
+ "Try a different search term or use `api_catalog_list` to see all APIs."
1289
+ )
1290
+
1291
+ body = "\n\n".join(_format_api_search_result(api) for api in data)
1292
+ total = len(data)
1293
+
1294
+ return text(f"🔍 **APIs found for '{query}'** ({total} results)\n\n{body}")
1295
+
1296
+ if action is KnowledgeAction.API_CATALOG_LIST:
1297
+ apis = await self._service.api_catalog_list(
1298
+ limit=payload.limit,
1299
+ offset=payload.offset,
1300
+ )
1301
+
1302
+ if not apis:
1303
+ return text("📚 No APIs found in the catalog.")
1304
+
1305
+ body = "\n\n".join(_format_api_item(api) for api in apis)
1306
+ return text(f"📚 **API Catalog ({len(apis)} APIs)**\n\n{body}")
1307
+
1308
+ if action is KnowledgeAction.API_CATALOG_GET:
1309
+ spec_id = payload.api_spec_id or payload.id
1310
+ if not spec_id:
1311
+ return text("❌ Provide api_spec_id or id to get the API details.")
1312
+
1313
+ api = await self._service.api_catalog_get(spec_id)
1314
+ return text(_format_api_detail(api))
1315
+
1316
+ return text(
1317
+ "❌ Unsupported API Catalog action.\n\nChoose one of the values:\n"
1318
+ + "\n".join(
1319
+ f"- `{value}`"
1320
+ for value in KnowledgeAction.choices()
1321
+ if value.startswith("api_catalog_")
1322
+ )
1323
+ )
1324
+
1157
1325
  async def _handle_help(self):
1158
1326
  workflow_guide = """
1159
1327
  ## 📖 How to find and read a document
@@ -1423,4 +1591,168 @@ def _format_marketplace_rule(rule: Dict[str, Any]) -> str:
1423
1591
  return "\n".join(lines)
1424
1592
 
1425
1593
 
1594
+ def _format_api_search_result(api: Dict[str, Any]) -> str:
1595
+ """Format an API search result with similarity and matched endpoint."""
1596
+ lines: List[str] = []
1597
+
1598
+ title = api.get("title", "Untitled API")
1599
+ slug = api.get("slug", "")
1600
+ similarity = api.get("similarity")
1601
+
1602
+ # Title with similarity
1603
+ if similarity is not None:
1604
+ pct = float(similarity) * 100
1605
+ lines.append(f"🔗 **{title}** ({pct:.0f}% match)")
1606
+ else:
1607
+ lines.append(f"🔗 **{title}**")
1608
+
1609
+ lines.append(f"Slug: {slug}")
1610
+
1611
+ # Status
1612
+ status = api.get("lifecycle_status", "unknown")
1613
+ lines.append(f"Status: {status}")
1614
+
1615
+ # Description (truncated)
1616
+ desc = api.get("description")
1617
+ if desc:
1618
+ truncated = desc[:150] + "..." if len(desc) > 150 else desc
1619
+ lines.append(f"Description: {truncated}")
1620
+
1621
+ # Team info
1622
+ team = api.get("team")
1623
+ if isinstance(team, dict):
1624
+ lines.append(f"Team: {team.get('name', 'Unknown')}")
1625
+
1626
+ # Matched endpoint (for semantic search)
1627
+ matched = api.get("matchedEndpoint")
1628
+ if matched:
1629
+ method = matched.get("method", "").upper()
1630
+ path = matched.get("path", "")
1631
+ summary = matched.get("summary", "")
1632
+ lines.append(f"Best match: **{method} {path}**")
1633
+ if summary:
1634
+ lines.append(f" └─ {summary}")
1635
+
1636
+ # Current version
1637
+ version = api.get("current_version")
1638
+ if isinstance(version, dict):
1639
+ lines.append(f"Version: {version.get('version', 'N/A')}")
1640
+
1641
+ return "\n".join(lines)
1642
+
1643
+
1644
+ def _format_api_item(api: Dict[str, Any]) -> str:
1645
+ """Format an API item for listing."""
1646
+ lines: List[str] = []
1647
+
1648
+ title = api.get("title", "Untitled API")
1649
+ slug = api.get("slug", "")
1650
+ status = api.get("lifecycle_status", "unknown")
1651
+
1652
+ lines.append(f"📦 **{title}**")
1653
+ lines.append(f"ID: {api.get('id', 'N/A')}")
1654
+ lines.append(f"Slug: {slug}")
1655
+ lines.append(f"Status: {status}")
1656
+
1657
+ # Tags
1658
+ tags = api.get("tags", [])
1659
+ if tags:
1660
+ lines.append(f"Tags: {', '.join(tags)}")
1661
+
1662
+ # Team
1663
+ team = api.get("team")
1664
+ if isinstance(team, dict):
1665
+ lines.append(f"Team: {team.get('name', 'Unknown')}")
1666
+
1667
+ # Version
1668
+ version = api.get("current_version")
1669
+ if isinstance(version, dict):
1670
+ lines.append(f"Version: {version.get('version', 'N/A')}")
1671
+
1672
+ return "\n".join(lines)
1673
+
1674
+
1675
+ def _format_api_detail(api: Dict[str, Any]) -> str:
1676
+ """Format detailed API specification view."""
1677
+ lines: List[str] = []
1678
+
1679
+ title = api.get("title", "Untitled API")
1680
+ slug = api.get("slug", "")
1681
+
1682
+ lines.append(f"📚 **{title}**")
1683
+ lines.append("")
1684
+ lines.append(f"ID: {api.get('id', 'N/A')}")
1685
+ lines.append(f"Slug: {slug}")
1686
+ lines.append(f"Status: {api.get('lifecycle_status', 'unknown')}")
1687
+
1688
+ # Description
1689
+ desc = api.get("description")
1690
+ if desc:
1691
+ lines.append("")
1692
+ lines.append("**Description:**")
1693
+ lines.append(desc)
1694
+
1695
+ # Tags
1696
+ tags = api.get("tags", [])
1697
+ if tags:
1698
+ lines.append("")
1699
+ lines.append(f"Tags: {', '.join(tags)}")
1700
+
1701
+ # Team
1702
+ team = api.get("team")
1703
+ if isinstance(team, dict):
1704
+ lines.append(f"Team: {team.get('name', 'Unknown')}")
1705
+
1706
+ # Current version
1707
+ version = api.get("current_version")
1708
+ if isinstance(version, dict):
1709
+ lines.append("")
1710
+ lines.append("**Current Version:**")
1711
+ lines.append(f"- Version: {version.get('version', 'N/A')}")
1712
+ lines.append(f"- OpenAPI: {version.get('openapi_version', 'N/A')}")
1713
+
1714
+ # Endpoints from current version
1715
+ endpoints = version.get("endpoint_embeddings", [])
1716
+ if endpoints:
1717
+ lines.append(f"- Endpoints: {len(endpoints)}")
1718
+ lines.append("")
1719
+ lines.append("**Endpoints:**")
1720
+ for ep in endpoints[:10]: # Limit to 10
1721
+ method = ep.get("method", "").upper()
1722
+ path = ep.get("path", "")
1723
+ summary = ep.get("summary", "")
1724
+ if summary:
1725
+ lines.append(f"- **{method}** {path} - {summary}")
1726
+ else:
1727
+ lines.append(f"- **{method}** {path}")
1728
+ if len(endpoints) > 10:
1729
+ lines.append(f" ... and {len(endpoints) - 10} more endpoints")
1730
+
1731
+ # Environments
1732
+ environments = api.get("environments", [])
1733
+ if environments:
1734
+ lines.append("")
1735
+ lines.append("**Environments:**")
1736
+ for env in environments:
1737
+ name = env.get("name", "Unnamed")
1738
+ url = env.get("base_url", "")
1739
+ lines.append(f"- {name}: {url}")
1740
+
1741
+ # Permissions
1742
+ permissions = api.get("permissions", {})
1743
+ if permissions:
1744
+ lines.append("")
1745
+ lines.append("**Your Permissions:**")
1746
+ if permissions.get("canEdit"):
1747
+ lines.append("- ✅ Edit")
1748
+ if permissions.get("canDelete"):
1749
+ lines.append("- ✅ Delete")
1750
+ if permissions.get("canPublish"):
1751
+ lines.append("- ✅ Publish")
1752
+ if permissions.get("canManageVersions"):
1753
+ lines.append("- ✅ Manage Versions")
1754
+
1755
+ return "\n".join(lines)
1756
+
1757
+
1426
1758
  __all__ = ["KnowledgeTool", "KnowledgeAction"]
@@ -15,7 +15,7 @@ class InitializationData:
15
15
  profile: Optional[Dict[str, Any]]
16
16
  core_documents: List[Dict[str, Any]]
17
17
  user_documents: List[Dict[str, Any]]
18
- recent_memories: List[Dict[str, Any]]
18
+ my_work_items: List[Dict[str, Any]]
19
19
 
20
20
 
21
21
  class InitializationService:
@@ -25,9 +25,7 @@ class InitializationService:
25
25
  self._api = api_client
26
26
  self._logger = logger
27
27
 
28
- async def gather_data(
29
- self, *, include_user_docs: bool, limit: int
30
- ) -> InitializationData:
28
+ async def gather_data(self, *, include_user_docs: bool) -> InitializationData:
31
29
  profile = await self._safe_call(self._api.get_profile)
32
30
  core_docs = await self._safe_call(
33
31
  self._api.list_core_documents,
@@ -46,11 +44,19 @@ class InitializationService:
46
44
  )
47
45
  if self._logger:
48
46
  self._logger.debug("User docs response", extra={"user_docs": user_docs})
47
+
48
+ # Fetch user's assigned work items for proactive context
49
+ work_items_response = await self._safe_call(
50
+ self._api.get_work_items_mine,
51
+ limit=10,
52
+ )
53
+ my_work_items = self._extract_items(work_items_response, "workItems")
54
+
49
55
  return InitializationData(
50
56
  profile=profile,
51
57
  core_documents=self._extract_items(core_docs, "coreDocuments"),
52
58
  user_documents=self._extract_items(user_docs, "userCoreDocuments"),
53
- recent_memories=[],
59
+ my_work_items=my_work_items,
54
60
  )
55
61
 
56
62
  async def _safe_call(self, func, *args, **kwargs):
@@ -71,110 +77,3 @@ class InitializationService:
71
77
  if isinstance(value, list):
72
78
  return [item for item in value if isinstance(item, dict)]
73
79
  return []
74
-
75
- @staticmethod
76
- def build_existing_user_summary(
77
- data: InitializationData, include_user_docs: bool
78
- ) -> str:
79
- profile = data.profile or {}
80
- user_info = profile.get("user") or {}
81
- tenant_info = profile.get("tenant") or {}
82
- core_count = len(data.core_documents)
83
- user_count = len(data.user_documents)
84
- memories_count = len(data.recent_memories)
85
-
86
- lines = [
87
- "✅ **Fênix Cloud inicializado com sucesso!**",
88
- ]
89
-
90
- if user_info:
91
- lines.append(
92
- f"- Usuário: {user_info.get('name') or 'Desconhecido'} "
93
- f"(ID: {user_info.get('id', 'N/A')})"
94
- )
95
- if tenant_info:
96
- lines.append(f"- Organização: {tenant_info.get('name', 'N/A')}")
97
-
98
- lines.append(f"- Documentos principais carregados: {core_count}")
99
-
100
- if include_user_docs:
101
- lines.append(f"- Documentos pessoais carregados: {user_count}")
102
-
103
- lines.append(f"- Memórias recentes disponíveis: {memories_count}")
104
-
105
- if core_count:
106
- preview = ", ".join(
107
- doc.get("name", doc.get("title", "sem título"))
108
- for doc in data.core_documents[:5]
109
- )
110
- lines.append(f"- Exemplos de documentos principais: {preview}")
111
-
112
- if include_user_docs and user_count:
113
- preview = ", ".join(
114
- doc.get("name", "sem título") for doc in data.user_documents[:5]
115
- )
116
- lines.append(f"- Exemplos de documentos pessoais: {preview}")
117
-
118
- if memories_count:
119
- preview = ", ".join(
120
- mem.get("title", "sem título") for mem in data.recent_memories[:3]
121
- )
122
- lines.append(f"- Memórias recentes: {preview}")
123
-
124
- lines.append("")
125
- lines.append(
126
- "Agora você pode usar as ferramentas de produtividade, conhecimento e inteligência para continuar."
127
- )
128
- return "\n".join(lines)
129
-
130
- @staticmethod
131
- def build_new_user_prompt(data: InitializationData) -> str:
132
- profile = data.profile or {}
133
- user_name = (profile.get("user") or {}).get("name") or "Bem-vindo(a)"
134
-
135
- questions = [
136
- "Qual é o foco principal do seu trabalho atualmente?",
137
- "Quais são seus objetivos para as próximas semanas?",
138
- "Existem tarefas ou projetos que gostaria de priorizar?",
139
- "Como você prefere organizar suas rotinas ou checklists?",
140
- "Quais são os principais blocos de conhecimento que precisa acessar rapidamente?",
141
- "Há regras ou procedimentos que precisam ser seguidos com frequência?",
142
- "Que tipo de memória ou registro você consulta com frequência?",
143
- "Quais ferramentas ou integrações externas são essenciais no seu dia a dia?",
144
- "O que significaria sucesso para você utilizando o Fênix?",
145
- ]
146
-
147
- body = [
148
- f"👋 **{user_name}, bem-vindo(a) ao Fênix Cloud!**",
149
- "",
150
- "Notamos que você ainda não tem documentos pessoais configurados. "
151
- "Vamos criar uma experiência personalizada para você.",
152
- "",
153
- "Responda às perguntas abaixo para que possamos preparar modos, regras e documentos alinhados às suas necessidades:",
154
- "",
155
- ]
156
-
157
- body.extend(f"{idx + 1}. {question}" for idx, question in enumerate(questions))
158
- body.extend(
159
- [
160
- "",
161
- "Envie as respostas no formato:",
162
- "```",
163
- 'initialize action=setup answers=["resposta 1", "resposta 2", ..., "resposta 9"]',
164
- "```",
165
- "",
166
- "Com base nisso, sugerirei documentos, regras e rotinas para você utilizar.",
167
- ]
168
- )
169
-
170
- return "\n".join(body)
171
-
172
- @staticmethod
173
- def validate_setup_answers(answers: List[str]) -> Optional[str]:
174
- if not answers:
175
- return "Envie uma lista com 9 respostas para processarmos o setup."
176
- if len(answers) != 9:
177
- return f"Esperado receber 9 respostas, mas recebi {len(answers)}."
178
- if not all(isinstance(answer, str) and answer.strip() for answer in answers):
179
- return "Todas as respostas devem ser textos preenchidos."
180
- return None