fenix-mcp 1.13.0__tar.gz → 1.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/PKG-INFO +1 -1
  2. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/__init__.py +1 -1
  3. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/application/tools/knowledge.py +230 -252
  4. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/domain/knowledge.py +34 -117
  5. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/infrastructure/fenix_api/client.py +50 -99
  6. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp.egg-info/PKG-INFO +1 -1
  7. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/README.md +0 -0
  8. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/application/presenters.py +0 -0
  9. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/application/tool_base.py +0 -0
  10. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/application/tool_registry.py +0 -0
  11. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/application/tools/__init__.py +0 -0
  12. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/application/tools/health.py +0 -0
  13. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/application/tools/initialize.py +0 -0
  14. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/application/tools/intelligence.py +0 -0
  15. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/application/tools/productivity.py +0 -0
  16. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/application/tools/user_config.py +0 -0
  17. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/domain/initialization.py +0 -0
  18. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/domain/intelligence.py +0 -0
  19. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/domain/productivity.py +0 -0
  20. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/domain/user_config.py +0 -0
  21. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/infrastructure/config.py +0 -0
  22. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/infrastructure/context.py +0 -0
  23. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/infrastructure/http_client.py +0 -0
  24. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/infrastructure/logging.py +0 -0
  25. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/infrastructure/request_context.py +0 -0
  26. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/interface/mcp_server.py +0 -0
  27. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/interface/transports.py +0 -0
  28. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp/main.py +0 -0
  29. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp.egg-info/SOURCES.txt +0 -0
  30. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp.egg-info/dependency_links.txt +0 -0
  31. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp.egg-info/entry_points.txt +0 -0
  32. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp.egg-info/requires.txt +0 -0
  33. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/fenix_mcp.egg-info/top_level.txt +0 -0
  34. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/pyproject.toml +0 -0
  35. {fenix_mcp-1.13.0 → fenix_mcp-1.14.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fenix-mcp
3
- Version: 1.13.0
3
+ Version: 1.14.0
4
4
  Summary: Fênix Cloud MCP server implemented in Python
5
5
  Author: Fenix Inc
6
6
  Requires-Python: >=3.10
@@ -8,4 +8,4 @@ Fênix Cloud MCP Server (Python edition).
8
8
  __all__ = ["__version__"]
9
9
 
10
10
 
11
- __version__ = "1.13.0"
11
+ __version__ = "1.14.0"
@@ -105,25 +105,39 @@ class KnowledgeAction(str, Enum):
105
105
  "Lists work items linked to a sprint.",
106
106
  )
107
107
 
108
- # Modes
109
- MODE_CREATE = ("mode_create", "Creates a mode with content and optional metadata.")
110
- MODE_LIST = ("mode_list", "Lists registered modes.")
111
- MODE_GET = ("mode_get", "Gets full details of a mode.")
112
- MODE_UPDATE = ("mode_update", "Updates properties of an existing mode.")
113
- MODE_DELETE = ("mode_delete", "Removes a mode.")
114
- MODE_RULE_ADD = ("mode_rule_add", "Associates a rule with a mode.")
115
- MODE_RULE_REMOVE = (
116
- "mode_rule_remove",
117
- "Removes the association of a rule with a mode.",
118
- )
119
- MODE_RULES = ("mode_rules", "Lists rules associated with a mode.")
120
-
121
108
  # Rules
122
- RULE_CREATE = ("rule_create", "Creates a rule with content and metadata.")
123
- RULE_LIST = ("rule_list", "Lists registered rules.")
124
- RULE_GET = ("rule_get", "Gets rule details.")
125
- RULE_UPDATE = ("rule_update", "Updates an existing rule.")
126
- RULE_DELETE = ("rule_delete", "Removes a rule.")
109
+ RULE_CREATE = (
110
+ "rule_create",
111
+ "Creates a new rule. REQUIRED: rule_scope (personal|team|organization|marketplace), rule_name, rule_content. Optional: rule_description, rule_slug. If scope=team, rule_team_id is also required.",
112
+ )
113
+ RULE_LIST = (
114
+ "rule_list",
115
+ "Lists rules accessible to the current user. Optional filters: rule_scope, query, limit, offset.",
116
+ )
117
+ RULE_GET = (
118
+ "rule_get",
119
+ "Gets full details of a rule. REQUIRED: rule_id (or id).",
120
+ )
121
+ RULE_UPDATE = (
122
+ "rule_update",
123
+ "Updates an existing rule. REQUIRED: rule_id (or id). Optional: rule_name, rule_description, rule_content, rule_is_active.",
124
+ )
125
+ RULE_DELETE = (
126
+ "rule_delete",
127
+ "Deletes a rule. REQUIRED: rule_id (or id).",
128
+ )
129
+ RULE_MARKETPLACE = (
130
+ "rule_marketplace",
131
+ "Lists public marketplace rules. Optional filters: query, limit, offset.",
132
+ )
133
+ RULE_FORK = (
134
+ "rule_fork",
135
+ "Forks a rule to a new scope. REQUIRED: rule_id (or id), rule_scope (personal|team|organization). Optional: rule_name, rule_team_id (required if scope=team).",
136
+ )
137
+ RULE_EXPORT = (
138
+ "rule_export",
139
+ "Exports a rule to a specific format. REQUIRED: rule_id (or id), rule_export_format (cursor|claude|copilot|windsurf).",
140
+ )
127
141
 
128
142
  # Documentation - Navigation workflow: doc_full_tree -> doc_children -> doc_get
129
143
  DOC_CREATE = (
@@ -283,40 +297,6 @@ class KnowledgeRequest(ToolRequest):
283
297
  ),
284
298
  )
285
299
 
286
- # Mode fields
287
- mode_id: Optional[UUIDStr] = Field(
288
- default=None, description="Related mode ID (UUID)."
289
- )
290
- mode_name: Optional[TitleStr] = Field(default=None, description="Mode name.")
291
- mode_description: Optional[DescriptionStr] = Field(
292
- default=None, description="Mode description."
293
- )
294
- mode_content: Optional[MarkdownStr] = Field(
295
- default=None, description="Mode content (Markdown)."
296
- )
297
- mode_is_default: Optional[bool] = Field(
298
- default=None, description="Indicates if the mode is default."
299
- )
300
- mode_metadata: Optional[MarkdownStr] = Field(
301
- default=None, description="Mode metadata for AI processing."
302
- )
303
-
304
- # Rule fields
305
- rule_id: Optional[UUIDStr] = Field(
306
- default=None, description="Related rule ID (UUID)."
307
- )
308
- rule_name: Optional[TitleStr] = Field(default=None, description="Rule name.")
309
- rule_description: Optional[DescriptionStr] = Field(
310
- default=None, description="Rule description."
311
- )
312
- rule_content: Optional[MarkdownStr] = Field(
313
- default=None, description="Rule content (Markdown)."
314
- )
315
- rule_is_default: Optional[bool] = Field(default=None, description="Default rule.")
316
- rule_metadata: Optional[MarkdownStr] = Field(
317
- default=None, description="Rule metadata for AI processing."
318
- )
319
-
320
300
  # Documentation fields
321
301
  doc_title: Optional[TitleStr] = Field(
322
302
  default=None, description="Documentation title."
@@ -369,10 +349,39 @@ class KnowledgeRequest(ToolRequest):
369
349
  default=None, description="Whether the document is public."
370
350
  )
371
351
 
352
+ # Rule fields
353
+ rule_id: Optional[UUIDStr] = Field(default=None, description="Rule ID (UUID).")
354
+ rule_scope: Optional[str] = Field(
355
+ default=None,
356
+ description="Rule scope. Values: personal, team, organization, marketplace.",
357
+ )
358
+ rule_name: Optional[TitleStr] = Field(default=None, description="Rule name.")
359
+ rule_slug: Optional[str] = Field(
360
+ default=None, description="Rule slug (auto-generated if not provided)."
361
+ )
362
+ rule_description: Optional[DescriptionStr] = Field(
363
+ default=None, description="Rule description."
364
+ )
365
+ rule_content: Optional[MarkdownStr] = Field(
366
+ default=None, description=f"Rule content (Markdown).{MERMAID_HINT}"
367
+ )
368
+ rule_team_id: Optional[UUIDStr] = Field(
369
+ default=None, description="Team ID for team-scoped rules (UUID)."
370
+ )
371
+ rule_export_format: Optional[str] = Field(
372
+ default=None,
373
+ description="Export format. Values: cursor, claude, copilot, windsurf.",
374
+ )
375
+ rule_is_active: Optional[bool] = Field(
376
+ default=None, description="Whether the rule is active."
377
+ )
378
+
372
379
 
373
380
  class KnowledgeTool(Tool):
374
381
  name = "knowledge"
375
- description = "Fenix Cloud knowledge operations (Work Items, Boards, Sprints, Modes, Rules, Docs)."
382
+ description = (
383
+ "Fenix Cloud knowledge operations (Work Items, Boards, Sprints, Rules, Docs)."
384
+ )
376
385
  request_model = KnowledgeRequest
377
386
 
378
387
  def __init__(self, context: AppContext):
@@ -389,8 +398,6 @@ class KnowledgeTool(Tool):
389
398
  return await self._run_board(payload)
390
399
  if action.value.startswith("sprint_"):
391
400
  return await self._run_sprint(payload)
392
- if action.value.startswith("mode_"):
393
- return await self._run_mode(payload)
394
401
  if action.value.startswith("rule_"):
395
402
  return await self._run_rule(payload)
396
403
  if action.value.startswith("doc_"):
@@ -820,195 +827,123 @@ class KnowledgeTool(Tool):
820
827
  )
821
828
  )
822
829
 
823
- # ------------------------------------------------------------------
824
- # Modes
825
- # ------------------------------------------------------------------
826
- async def _run_mode(self, payload: KnowledgeRequest):
827
- action = payload.action
828
- if action is KnowledgeAction.MODE_CREATE:
829
- if not payload.mode_name:
830
- return text("❌ Provide mode_name to create the mode.")
831
- mode = await self._service.mode_create(
832
- {
833
- "name": payload.mode_name,
834
- "description": payload.mode_description,
835
- "content": payload.mode_content,
836
- "is_default": payload.mode_is_default,
837
- "metadata": payload.mode_metadata,
838
- }
839
- )
840
- return text(_format_mode(mode, header="✅ Mode created"))
841
-
842
- if action is KnowledgeAction.MODE_LIST:
843
- modes = await self._service.mode_list(
844
- include_rules=payload.return_metadata,
845
- return_description=payload.return_description,
846
- return_metadata=payload.return_metadata,
847
- )
848
- if not modes:
849
- return text("🎭 No modes found.")
850
- body = "\n\n".join(_format_mode(mode) for mode in modes)
851
- return text(f"🎭 **Modes ({len(modes)}):**\n\n{body}")
852
-
853
- if action is KnowledgeAction.MODE_GET:
854
- if not payload.mode_id:
855
- return text("❌ Provide mode_id to get details.")
856
- mode = await self._service.mode_get(
857
- payload.mode_id,
858
- return_description=payload.return_description,
859
- return_metadata=payload.return_metadata,
860
- )
861
- # Buscar rules associadas ao mode
862
- associations = await self._service.mode_rules(payload.mode_id)
863
- rules = [assoc.get("rule", assoc) for assoc in associations]
864
-
865
- # Formatar resposta com rules (incluindo content)
866
- output = _format_mode(mode, header="🎭 Mode details", show_content=True)
867
- if rules:
868
- rules_parts = []
869
- for r in rules:
870
- rule_text = f"### 📋 {r.get('name', 'Unnamed')}\n"
871
- rule_text += f"ID: {r.get('id')}\n"
872
- if r.get("content"):
873
- rule_text += f"\n{r.get('content')}"
874
- rules_parts.append(rule_text)
875
- output += f"\n\n**Rules ({len(rules)}):**\n\n" + "\n\n---\n\n".join(
876
- rules_parts
877
- )
878
- return text(output)
879
-
880
- if action is KnowledgeAction.MODE_UPDATE:
881
- if not payload.mode_id:
882
- return text("❌ Provide mode_id to update.")
883
- mode = await self._service.mode_update(
884
- payload.mode_id,
885
- {
886
- "name": payload.mode_name,
887
- "description": payload.mode_description,
888
- "content": payload.mode_content,
889
- "is_default": payload.mode_is_default,
890
- "metadata": payload.mode_metadata,
891
- },
892
- )
893
- return text(_format_mode(mode, header="✅ Mode updated"))
894
-
895
- if action is KnowledgeAction.MODE_DELETE:
896
- if not payload.mode_id:
897
- return text("❌ Provide mode_id to remove.")
898
- await self._service.mode_delete(payload.mode_id)
899
- return text(f"🗑️ Mode {payload.mode_id} removed.")
900
-
901
- if action is KnowledgeAction.MODE_RULE_ADD:
902
- if not payload.mode_id or not payload.rule_id:
903
- return text("❌ Provide mode_id and rule_id to associate.")
904
- link = await self._service.mode_rule_add(payload.mode_id, payload.rule_id)
905
- return text(
906
- "\n".join(
907
- [
908
- "🔗 **Rule associated with mode!**",
909
- f"Mode: {link.get('modeId', payload.mode_id)}",
910
- f"Rule: {link.get('ruleId', payload.rule_id)}",
911
- ]
912
- )
913
- )
914
-
915
- if action is KnowledgeAction.MODE_RULE_REMOVE:
916
- if not payload.mode_id or not payload.rule_id:
917
- return text("❌ Provide mode_id and rule_id to remove the association.")
918
- await self._service.mode_rule_remove(payload.mode_id, payload.rule_id)
919
- return text("🔗 Association removed.")
920
-
921
- if action is KnowledgeAction.MODE_RULES:
922
- if payload.mode_id:
923
- associations = await self._service.mode_rules(payload.mode_id)
924
- context_label = f"mode {payload.mode_id}"
925
- # API retorna [{id, mode_id, rule_id, rule: {...}}] - extrair rule
926
- items = [assoc.get("rule", assoc) for assoc in associations]
927
- elif payload.rule_id:
928
- associations = await self._service.mode_rules_for_rule(payload.rule_id)
929
- context_label = f"rule {payload.rule_id}"
930
- # API retorna [{id, mode_id, rule_id, mode: {...}}] - extrair mode
931
- items = [assoc.get("mode", assoc) for assoc in associations]
932
- else:
933
- return text("❌ Provide mode_id or rule_id to list associations.")
934
- if not items:
935
- return text("🔗 No associations found.")
936
- body = "\n".join(
937
- f"- {item.get('name', 'Unnamed')} (ID: {item.get('id')})"
938
- for item in items
939
- )
940
- return text(f"🔗 **Associations for {context_label}:**\n{body}")
941
-
942
- return text(
943
- "❌ Unsupported mode action.\n\nChoose one of the values:\n"
944
- + "\n".join(
945
- f"- `{value}`"
946
- for value in KnowledgeAction.choices()
947
- if value.startswith("mode_")
948
- )
949
- )
950
-
951
830
  # ------------------------------------------------------------------
952
831
  # Rules
953
832
  # ------------------------------------------------------------------
954
833
  async def _run_rule(self, payload: KnowledgeRequest):
955
834
  action = payload.action
835
+
956
836
  if action is KnowledgeAction.RULE_CREATE:
957
- if not payload.rule_name or not payload.rule_content:
958
- return text("❌ Provide rule_name and rule_content.")
837
+ if not payload.rule_name:
838
+ return text("❌ Provide rule_name to create the rule.")
839
+ if not payload.rule_content:
840
+ return text("❌ Provide rule_content to create the rule.")
841
+ if not payload.rule_scope:
842
+ return text(
843
+ "❌ Provide rule_scope to create the rule. "
844
+ "Values: personal, team, organization, marketplace."
845
+ )
846
+ if payload.rule_scope == "team" and not payload.rule_team_id:
847
+ return text("❌ Provide rule_team_id for team-scoped rules.")
848
+
959
849
  rule = await self._service.rule_create(
960
850
  {
851
+ "scope": payload.rule_scope,
961
852
  "name": payload.rule_name,
962
- "description": payload.rule_description,
853
+ "slug": sanitize_null(payload.rule_slug),
854
+ "description": sanitize_null(payload.rule_description),
963
855
  "content": payload.rule_content,
964
- "is_default": payload.rule_is_default,
965
- "metadata": payload.rule_metadata,
856
+ "team_id": sanitize_null(payload.rule_team_id),
966
857
  }
967
858
  )
968
859
  return text(_format_rule(rule, header="✅ Rule created"))
969
860
 
970
861
  if action is KnowledgeAction.RULE_LIST:
971
862
  rules = await self._service.rule_list(
972
- return_description=payload.return_description,
973
- return_metadata=payload.return_metadata,
974
- return_modes=payload.return_metadata,
863
+ limit=payload.limit,
864
+ offset=payload.offset,
865
+ scope=sanitize_null(payload.rule_scope),
866
+ query=sanitize_null(payload.query),
975
867
  )
976
868
  if not rules:
977
- return text("📋 No rules found.")
869
+ return text("📜 No rules found.")
978
870
  body = "\n\n".join(_format_rule(rule) for rule in rules)
979
- return text(f"📋 **Rules ({len(rules)}):**\n\n{body}")
871
+ return text(f"📜 **Rules ({len(rules)}):**\n\n{body}")
980
872
 
981
873
  if action is KnowledgeAction.RULE_GET:
982
- if not payload.rule_id:
983
- return text("❌ Provide rule_id to get details.")
984
- rule = await self._service.rule_get(
985
- payload.rule_id,
986
- return_description=payload.return_description,
987
- return_metadata=payload.return_metadata,
988
- return_modes=payload.return_metadata,
989
- )
990
- return text(_format_rule(rule, header="📋 Rule details", show_content=True))
874
+ rule_id = payload.rule_id or payload.id
875
+ if not rule_id:
876
+ return text("❌ Provide rule_id or id to get the rule.")
877
+ rule = await self._service.rule_get(rule_id)
878
+ return text(_format_rule(rule, header="📜 Rule details", show_content=True))
991
879
 
992
880
  if action is KnowledgeAction.RULE_UPDATE:
993
- if not payload.rule_id:
994
- return text("❌ Provide rule_id to update.")
881
+ rule_id = payload.rule_id or payload.id
882
+ if not rule_id:
883
+ return text("❌ Provide rule_id or id to update the rule.")
995
884
  rule = await self._service.rule_update(
996
- payload.rule_id,
885
+ rule_id,
997
886
  {
998
- "name": payload.rule_name,
999
- "description": payload.rule_description,
1000
- "content": payload.rule_content,
1001
- "is_default": payload.rule_is_default,
1002
- "metadata": payload.rule_metadata,
887
+ "name": sanitize_null(payload.rule_name),
888
+ "description": sanitize_null(payload.rule_description),
889
+ "content": sanitize_null(payload.rule_content),
890
+ "is_active": payload.rule_is_active,
1003
891
  },
1004
892
  )
1005
893
  return text(_format_rule(rule, header="✅ Rule updated"))
1006
894
 
1007
895
  if action is KnowledgeAction.RULE_DELETE:
1008
- if not payload.rule_id:
1009
- return text("❌ Provide rule_id to remove.")
1010
- await self._service.rule_delete(payload.rule_id)
1011
- return text(f"🗑️ Rule {payload.rule_id} removed.")
896
+ rule_id = payload.rule_id or payload.id
897
+ if not rule_id:
898
+ return text("❌ Provide rule_id or id to delete the rule.")
899
+ await self._service.rule_delete(rule_id)
900
+ return text(f"🗑️ Rule {rule_id} removed.")
901
+
902
+ if action is KnowledgeAction.RULE_MARKETPLACE:
903
+ rules = await self._service.rule_marketplace(
904
+ limit=payload.limit,
905
+ offset=payload.offset,
906
+ query=sanitize_null(payload.query),
907
+ )
908
+ if not rules:
909
+ return text("🏪 No marketplace rules found.")
910
+ body = "\n\n".join(_format_marketplace_rule(rule) for rule in rules)
911
+ return text(f"🏪 **Marketplace Rules ({len(rules)}):**\n\n{body}")
912
+
913
+ if action is KnowledgeAction.RULE_FORK:
914
+ rule_id = payload.rule_id or payload.id
915
+ if not rule_id:
916
+ return text("❌ Provide rule_id or id to fork the rule.")
917
+ if not payload.rule_scope:
918
+ return text(
919
+ "❌ Provide rule_scope for the forked rule. "
920
+ "Values: personal, team, organization."
921
+ )
922
+ rule = await self._service.rule_fork(
923
+ rule_id,
924
+ {
925
+ "scope": payload.rule_scope,
926
+ "name": sanitize_null(payload.rule_name),
927
+ "team_id": sanitize_null(payload.rule_team_id),
928
+ },
929
+ )
930
+ return text(_format_rule(rule, header="✅ Rule forked"))
931
+
932
+ if action is KnowledgeAction.RULE_EXPORT:
933
+ rule_id = payload.rule_id or payload.id
934
+ if not rule_id:
935
+ return text("❌ Provide rule_id or id to export the rule.")
936
+ if not payload.rule_export_format:
937
+ return text(
938
+ "❌ Provide rule_export_format. "
939
+ "Values: cursor, claude, copilot, windsurf."
940
+ )
941
+ content = await self._service.rule_export(
942
+ rule_id, payload.rule_export_format
943
+ )
944
+ return text(
945
+ f"📤 **Export ({payload.rule_export_format}):**\n\n```\n{content}\n```"
946
+ )
1012
947
 
1013
948
  return text(
1014
949
  "❌ Unsupported rule action.\n\nChoose one of the values:\n"
@@ -1361,8 +1296,8 @@ def _format_sprint(sprint: Dict[str, Any], header: Optional[str] = None) -> str:
1361
1296
  return "\n".join(lines)
1362
1297
 
1363
1298
 
1364
- def _format_mode(
1365
- mode: Dict[str, Any],
1299
+ def _format_doc(
1300
+ doc: Dict[str, Any],
1366
1301
  *,
1367
1302
  header: Optional[str] = None,
1368
1303
  show_content: bool = False,
@@ -1373,17 +1308,23 @@ def _format_mode(
1373
1308
  lines.append("")
1374
1309
  lines.extend(
1375
1310
  [
1376
- f"🎭 **{mode.get('name', 'Unnamed')}**",
1377
- f"ID: {mode.get('id', 'N/A')}",
1378
- f"Default: {mode.get('is_default', False)}",
1311
+ f"📄 **{doc.get('title') or doc.get('name', 'Untitled')}**",
1312
+ f"ID: {doc.get('id', 'N/A')}",
1313
+ f"Status: {doc.get('status', 'N/A')}",
1314
+ f"Team: {doc.get('team_id', 'N/A')}",
1379
1315
  ]
1380
1316
  )
1381
- if mode.get("description"):
1382
- lines.append(f"Description: {mode['description']}")
1383
- if show_content and mode.get("content"):
1317
+ if doc.get("updated_at") or doc.get("updatedAt"):
1318
+ lines.append(
1319
+ f"Updated at: {_format_date(doc.get('updated_at') or doc.get('updatedAt'))}"
1320
+ )
1321
+
1322
+ # Show content only when explicitly requested (e.g., doc_get)
1323
+ if show_content and doc.get("content"):
1384
1324
  lines.append("")
1385
1325
  lines.append("**Content:**")
1386
- lines.append(mode.get("content"))
1326
+ lines.append(doc.get("content"))
1327
+
1387
1328
  return "\n".join(lines)
1388
1329
 
1389
1330
 
@@ -1397,50 +1338,87 @@ def _format_rule(
1397
1338
  if header:
1398
1339
  lines.append(header)
1399
1340
  lines.append("")
1341
+
1342
+ name = rule.get("name", "Untitled")
1343
+ scope = rule.get("scope", "unknown")
1344
+ scope_emoji = {
1345
+ "personal": "👤",
1346
+ "team": "👥",
1347
+ "organization": "🏢",
1348
+ "marketplace": "🌍",
1349
+ }.get(scope, "📜")
1350
+
1400
1351
  lines.extend(
1401
1352
  [
1402
- f"📋 **{rule.get('name', 'Unnamed')}**",
1353
+ f"{scope_emoji} **{name}**",
1403
1354
  f"ID: {rule.get('id', 'N/A')}",
1404
- f"Default: {rule.get('is_default', False)}",
1355
+ f"Scope: {scope}",
1356
+ f"Version: {rule.get('version', '1.0')}",
1405
1357
  ]
1406
1358
  )
1359
+
1407
1360
  if rule.get("description"):
1408
- lines.append(f"Description: {rule['description']}")
1361
+ lines.append(f"Description: {rule.get('description')}")
1362
+
1363
+ # Author info
1364
+ author = rule.get("author")
1365
+ if isinstance(author, dict):
1366
+ lines.append(f"Author: {author.get('name', 'Unknown')}")
1367
+
1368
+ # Forked from info
1369
+ forked_from = rule.get("forked_from")
1370
+ if isinstance(forked_from, dict):
1371
+ forked_author = forked_from.get("author", {})
1372
+ author_name = (
1373
+ forked_author.get("name", "Unknown")
1374
+ if isinstance(forked_author, dict)
1375
+ else "Unknown"
1376
+ )
1377
+ lines.append(
1378
+ f"Forked from: {forked_from.get('name', 'Unknown')} by {author_name}"
1379
+ )
1380
+
1381
+ if rule.get("is_active") is False:
1382
+ lines.append("Status: Inactive")
1383
+
1384
+ if rule.get("updated_at") or rule.get("updatedAt"):
1385
+ lines.append(
1386
+ f"Updated: {_format_date(rule.get('updated_at') or rule.get('updatedAt'))}"
1387
+ )
1388
+
1389
+ # Show content only when explicitly requested
1409
1390
  if show_content and rule.get("content"):
1410
1391
  lines.append("")
1411
1392
  lines.append("**Content:**")
1412
1393
  lines.append(rule.get("content"))
1394
+
1413
1395
  return "\n".join(lines)
1414
1396
 
1415
1397
 
1416
- def _format_doc(
1417
- doc: Dict[str, Any],
1418
- *,
1419
- header: Optional[str] = None,
1420
- show_content: bool = False,
1421
- ) -> str:
1398
+ def _format_marketplace_rule(rule: Dict[str, Any]) -> str:
1399
+ """Format a marketplace rule with download/rating info."""
1422
1400
  lines: List[str] = []
1423
- if header:
1424
- lines.append(header)
1425
- lines.append("")
1426
- lines.extend(
1427
- [
1428
- f"📄 **{doc.get('title') or doc.get('name', 'Untitled')}**",
1429
- f"ID: {doc.get('id', 'N/A')}",
1430
- f"Status: {doc.get('status', 'N/A')}",
1431
- f"Team: {doc.get('team_id', 'N/A')}",
1432
- ]
1433
- )
1434
- if doc.get("updated_at") or doc.get("updatedAt"):
1435
- lines.append(
1436
- f"Updated at: {_format_date(doc.get('updated_at') or doc.get('updatedAt'))}"
1437
- )
1438
1401
 
1439
- # Show content only when explicitly requested (e.g., doc_get)
1440
- if show_content and doc.get("content"):
1441
- lines.append("")
1442
- lines.append("**Content:**")
1443
- lines.append(doc.get("content"))
1402
+ name = rule.get("name", "Untitled")
1403
+ downloads = rule.get("downloads", 0)
1404
+ rating = rule.get("rating")
1405
+
1406
+ lines.append(f"🌍 **{name}**")
1407
+ lines.append(f"ID: {rule.get('id', 'N/A')}")
1408
+
1409
+ if rule.get("description"):
1410
+ lines.append(f"Description: {rule.get('description')}")
1411
+
1412
+ # Author info
1413
+ author = rule.get("author")
1414
+ if isinstance(author, dict):
1415
+ lines.append(f"Author: {author.get('name', 'Unknown')}")
1416
+
1417
+ # Stats
1418
+ stats_parts = [f"⬇️ {downloads}"]
1419
+ if rating is not None:
1420
+ stats_parts.append(f"⭐ {float(rating):.1f}")
1421
+ lines.append(" | ".join(stats_parts))
1444
1422
 
1445
1423
  return "\n".join(lines)
1446
1424
 
@@ -360,123 +360,6 @@ class KnowledgeService:
360
360
  async def sprint_cancel(self, sprint_id: str) -> Dict[str, Any]:
361
361
  return await self._call(self.api.cancel_sprint, sprint_id)
362
362
 
363
- # ------------------------------------------------------------------
364
- # Modes and rules
365
- # ------------------------------------------------------------------
366
- async def mode_create(self, payload: Dict[str, Any]) -> Dict[str, Any]:
367
- result = await self._call(self.api.create_mode, _strip_none(payload))
368
- # API returns { message, mode } - extract mode
369
- if isinstance(result, dict) and "mode" in result:
370
- return result["mode"]
371
- return result or {}
372
-
373
- async def mode_list(
374
- self,
375
- *,
376
- include_rules: Optional[bool] = None,
377
- return_description: Optional[bool] = None,
378
- return_metadata: Optional[bool] = None,
379
- ) -> List[Dict[str, Any]]:
380
- return (
381
- await self._call(
382
- self.api.list_modes,
383
- include_rules=include_rules,
384
- return_description=return_description,
385
- return_metadata=return_metadata,
386
- )
387
- or []
388
- )
389
-
390
- async def mode_get(
391
- self,
392
- mode_id: str,
393
- *,
394
- return_description: Optional[bool] = None,
395
- return_metadata: Optional[bool] = None,
396
- ) -> Dict[str, Any]:
397
- return await self._call(
398
- self.api.get_mode,
399
- mode_id,
400
- return_description=return_description,
401
- return_metadata=return_metadata,
402
- )
403
-
404
- async def mode_update(
405
- self, mode_id: str, payload: Dict[str, Any]
406
- ) -> Dict[str, Any]:
407
- result = await self._call(self.api.update_mode, mode_id, _strip_none(payload))
408
- # API returns { message, mode } - extract mode
409
- if isinstance(result, dict) and "mode" in result:
410
- return result["mode"]
411
- return result or {}
412
-
413
- async def mode_delete(self, mode_id: str) -> None:
414
- await self._call(self.api.delete_mode, mode_id)
415
-
416
- async def mode_rule_add(self, mode_id: str, rule_id: str) -> Dict[str, Any]:
417
- return await self._call(self.api.add_mode_rule, mode_id, rule_id)
418
-
419
- async def mode_rule_remove(self, mode_id: str, rule_id: str) -> None:
420
- await self._call(self.api.remove_mode_rule, mode_id, rule_id)
421
-
422
- async def mode_rules(self, mode_id: str) -> List[Dict[str, Any]]:
423
- return await self._call(self.api.list_rules_by_mode, mode_id) or []
424
-
425
- async def mode_rules_for_rule(self, rule_id: str) -> List[Dict[str, Any]]:
426
- return await self._call(self.api.list_modes_by_rule, rule_id) or []
427
-
428
- async def rule_create(self, payload: Dict[str, Any]) -> Dict[str, Any]:
429
- result = await self._call(self.api.create_rule, _strip_none(payload))
430
- # API returns { message, rule } - extract rule
431
- if isinstance(result, dict) and "rule" in result:
432
- return result["rule"]
433
- return result or {}
434
-
435
- async def rule_list(
436
- self,
437
- *,
438
- return_description: Optional[bool] = None,
439
- return_metadata: Optional[bool] = None,
440
- return_modes: Optional[bool] = None,
441
- ) -> List[Dict[str, Any]]:
442
- return (
443
- await self._call(
444
- self.api.list_rules,
445
- return_description=return_description,
446
- return_metadata=return_metadata,
447
- return_modes=return_modes,
448
- )
449
- or []
450
- )
451
-
452
- async def rule_get(
453
- self,
454
- rule_id: str,
455
- *,
456
- return_description: Optional[bool] = None,
457
- return_metadata: Optional[bool] = None,
458
- return_modes: Optional[bool] = None,
459
- ) -> Dict[str, Any]:
460
- return await self._call(
461
- self.api.get_rule,
462
- rule_id,
463
- return_description=return_description,
464
- return_metadata=return_metadata,
465
- return_modes=return_modes,
466
- )
467
-
468
- async def rule_update(
469
- self, rule_id: str, payload: Dict[str, Any]
470
- ) -> Dict[str, Any]:
471
- result = await self._call(self.api.update_rule, rule_id, _strip_none(payload))
472
- # API returns { message, rule } - extract rule
473
- if isinstance(result, dict) and "rule" in result:
474
- return result["rule"]
475
- return result or {}
476
-
477
- async def rule_delete(self, rule_id: str) -> None:
478
- await self._call(self.api.delete_rule, rule_id)
479
-
480
363
  # ------------------------------------------------------------------
481
364
  # Documentation
482
365
  # ------------------------------------------------------------------
@@ -549,6 +432,40 @@ class KnowledgeService:
549
432
  self.api.duplicate_documentation_item, doc_id, _strip_none(payload)
550
433
  )
551
434
 
435
+ # ------------------------------------------------------------------
436
+ # Rules
437
+ # ------------------------------------------------------------------
438
+ async def rule_create(self, payload: Dict[str, Any]) -> Dict[str, Any]:
439
+ return await self._call_dict(self.api.create_rule, _strip_none(payload))
440
+
441
+ async def rule_list(self, **filters: Any) -> List[Dict[str, Any]]:
442
+ return await self._call_list(self.api.list_rules, **_strip_none(filters))
443
+
444
+ async def rule_get(self, rule_id: str) -> Dict[str, Any]:
445
+ return await self._call_dict(self.api.get_rule, rule_id)
446
+
447
+ async def rule_update(
448
+ self, rule_id: str, payload: Dict[str, Any]
449
+ ) -> Dict[str, Any]:
450
+ return await self._call_dict(
451
+ self.api.update_rule, rule_id, _strip_none(payload)
452
+ )
453
+
454
+ async def rule_delete(self, rule_id: str) -> None:
455
+ await self._call(self.api.delete_rule, rule_id)
456
+
457
+ async def rule_marketplace(self, **filters: Any) -> List[Dict[str, Any]]:
458
+ return await self._call_list(
459
+ self.api.list_marketplace_rules, **_strip_none(filters)
460
+ )
461
+
462
+ async def rule_fork(self, rule_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
463
+ return await self._call_dict(self.api.fork_rule, rule_id, _strip_none(payload))
464
+
465
+ async def rule_export(self, rule_id: str, format: str) -> str:
466
+ result = await self._call(self.api.export_rule, rule_id, format)
467
+ return result if isinstance(result, str) else str(result or "")
468
+
552
469
 
553
470
  __all__ = [
554
471
  "KnowledgeService",
@@ -284,105 +284,6 @@ class FenixApiClient:
284
284
  "POST", "/api/memory-intelligence/smart-create", json=payload
285
285
  )
286
286
 
287
- # ------------------------------------------------------------------
288
- # Configuration: modes and rules
289
- # ------------------------------------------------------------------
290
-
291
- def list_modes(
292
- self,
293
- *,
294
- include_rules: Optional[bool] = None,
295
- return_description: Optional[bool] = None,
296
- return_metadata: Optional[bool] = None,
297
- ) -> Any:
298
- params = self._build_params(
299
- optional={
300
- "includeRules": include_rules,
301
- "returnDescription": return_description,
302
- "returnMetadata": return_metadata,
303
- }
304
- )
305
- return self._request("GET", "/api/modes", params=params)
306
-
307
- def get_mode(
308
- self,
309
- mode_id: str,
310
- *,
311
- return_description: Optional[bool] = None,
312
- return_metadata: Optional[bool] = None,
313
- ) -> Any:
314
- params = self._build_params(
315
- optional={
316
- "returnDescription": return_description,
317
- "returnMetadata": return_metadata,
318
- }
319
- )
320
- return self._request("GET", f"/api/modes/{mode_id}", params=params)
321
-
322
- def create_mode(self, payload: Mapping[str, Any]) -> Any:
323
- return self._request("POST", "/api/modes", json=payload)
324
-
325
- def update_mode(self, mode_id: str, payload: Mapping[str, Any]) -> Any:
326
- return self._request("PATCH", f"/api/modes/{mode_id}", json=payload)
327
-
328
- def delete_mode(self, mode_id: str) -> Any:
329
- return self._request("DELETE", f"/api/modes/{mode_id}")
330
-
331
- def list_rules(
332
- self,
333
- *,
334
- return_description: Optional[bool] = None,
335
- return_metadata: Optional[bool] = None,
336
- return_modes: Optional[bool] = None,
337
- ) -> Any:
338
- params = self._build_params(
339
- optional={
340
- "returnDescription": return_description,
341
- "returnMetadata": return_metadata,
342
- "returnModes": return_modes,
343
- }
344
- )
345
- return self._request("GET", "/api/rules", params=params)
346
-
347
- def get_rule(
348
- self,
349
- rule_id: str,
350
- *,
351
- return_description: Optional[bool] = None,
352
- return_metadata: Optional[bool] = None,
353
- return_modes: Optional[bool] = None,
354
- ) -> Any:
355
- params = self._build_params(
356
- optional={
357
- "returnDescription": return_description,
358
- "returnMetadata": return_metadata,
359
- "returnModes": return_modes,
360
- }
361
- )
362
- return self._request("GET", f"/api/rules/{rule_id}", params=params)
363
-
364
- def create_rule(self, payload: Mapping[str, Any]) -> Any:
365
- return self._request("POST", "/api/rules", json=payload)
366
-
367
- def update_rule(self, rule_id: str, payload: Mapping[str, Any]) -> Any:
368
- return self._request("PATCH", f"/api/rules/{rule_id}", json=payload)
369
-
370
- def delete_rule(self, rule_id: str) -> Any:
371
- return self._request("DELETE", f"/api/rules/{rule_id}")
372
-
373
- def add_mode_rule(self, mode_id: str, rule_id: str) -> Any:
374
- payload = {"modeId": mode_id, "ruleId": rule_id}
375
- return self._request("POST", "/api/mode-rules", json=payload)
376
-
377
- def remove_mode_rule(self, mode_id: str, rule_id: str) -> Any:
378
- return self._request("DELETE", f"/api/mode-rules/mode/{mode_id}/rule/{rule_id}")
379
-
380
- def list_rules_by_mode(self, mode_id: str) -> Any:
381
- return self._request("GET", f"/api/mode-rules/mode/{mode_id}/rules")
382
-
383
- def list_modes_by_rule(self, rule_id: str) -> Any:
384
- return self._request("GET", f"/api/mode-rules/rule/{rule_id}/modes")
385
-
386
287
  # ------------------------------------------------------------------
387
288
  # Knowledge: documentation
388
289
  # ------------------------------------------------------------------
@@ -691,3 +592,53 @@ class FenixApiClient:
691
592
 
692
593
  def cancel_sprint(self, sprint_id: str) -> Any:
693
594
  return self._request("PATCH", f"/api/sprints/{sprint_id}/cancel")
595
+
596
+ # ------------------------------------------------------------------
597
+ # Rules
598
+ # ------------------------------------------------------------------
599
+
600
+ def create_rule(self, payload: Mapping[str, Any]) -> Any:
601
+ return self._request("POST", "/api/rules", json=payload)
602
+
603
+ def list_rules(self, **filters: Any) -> Any:
604
+ return self._request("GET", "/api/rules", params=_strip_none(filters))
605
+
606
+ def get_rule(self, rule_id: str) -> Any:
607
+ return self._request("GET", f"/api/rules/{rule_id}")
608
+
609
+ def update_rule(self, rule_id: str, payload: Mapping[str, Any]) -> Any:
610
+ return self._request("PATCH", f"/api/rules/{rule_id}", json=payload)
611
+
612
+ def delete_rule(self, rule_id: str) -> Any:
613
+ return self._request("DELETE", f"/api/rules/{rule_id}")
614
+
615
+ def list_marketplace_rules(self, **filters: Any) -> Any:
616
+ return self._request(
617
+ "GET", "/api/rules/marketplace/list", params=_strip_none(filters)
618
+ )
619
+
620
+ def search_marketplace_rules(self, *, query: str, limit: int = 20) -> Any:
621
+ params = self._build_params(required={"q": query}, optional={"limit": limit})
622
+ return self._request("GET", "/api/rules/marketplace/search", params=params)
623
+
624
+ def get_top_marketplace_rules(self, *, limit: int = 10) -> Any:
625
+ params = self._build_params(optional={"limit": limit})
626
+ return self._request("GET", "/api/rules/marketplace/top", params=params)
627
+
628
+ def fork_rule(self, rule_id: str, payload: Mapping[str, Any]) -> Any:
629
+ return self._request("POST", f"/api/rules/{rule_id}/fork", json=payload)
630
+
631
+ def download_rule(self, rule_id: str) -> Any:
632
+ return self._request("POST", f"/api/rules/{rule_id}/download")
633
+
634
+ def rate_rule(self, rule_id: str, rating: float) -> Any:
635
+ return self._request(
636
+ "POST", f"/api/rules/{rule_id}/rate", json={"rating": rating}
637
+ )
638
+
639
+ def export_rule(self, rule_id: str, format: str) -> Any:
640
+ return self._request("GET", f"/api/rules/{rule_id}/export/{format}")
641
+
642
+ def export_merged_rules(self, format: str) -> Any:
643
+ params = self._build_params(required={"format": format})
644
+ return self._request("GET", "/api/rules/export/merged", params=params)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fenix-mcp
3
- Version: 1.13.0
3
+ Version: 1.14.0
4
4
  Summary: Fênix Cloud MCP server implemented in Python
5
5
  Author: Fenix Inc
6
6
  Requires-Python: >=3.10
File without changes
File without changes
File without changes
File without changes