fenix-mcp 2.0.0__py3-none-any.whl → 2.2.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__ = "2.0.0"
11
+ __version__ = "2.2.0"
@@ -0,0 +1,76 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Base abstractions for MCP prompts."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Dict, List, Optional
9
+
10
+
11
+ @dataclass
12
+ class PromptArgument:
13
+ """Defines an argument that a prompt accepts."""
14
+
15
+ name: str
16
+ description: str
17
+ required: bool = False
18
+
19
+
20
+ @dataclass
21
+ class PromptMessage:
22
+ """A single message in a prompt response."""
23
+
24
+ role: str # "user" or "assistant"
25
+ text: str
26
+
27
+ def to_dict(self) -> Dict[str, Any]:
28
+ return {
29
+ "role": self.role,
30
+ "content": {"type": "text", "text": self.text},
31
+ }
32
+
33
+
34
+ @dataclass
35
+ class PromptResult:
36
+ """Result returned by prompts/get."""
37
+
38
+ messages: List[PromptMessage] = field(default_factory=list)
39
+ description: Optional[str] = None
40
+
41
+ def to_dict(self) -> Dict[str, Any]:
42
+ result: Dict[str, Any] = {
43
+ "messages": [m.to_dict() for m in self.messages],
44
+ }
45
+ if self.description:
46
+ result["description"] = self.description
47
+ return result
48
+
49
+
50
+ class Prompt(ABC):
51
+ """Interface implemented by all prompts."""
52
+
53
+ name: str
54
+ description: str
55
+ arguments: List[PromptArgument] = []
56
+
57
+ def schema(self) -> Dict[str, Any]:
58
+ """Return JSON schema describing the prompt."""
59
+ result: Dict[str, Any] = {
60
+ "name": self.name,
61
+ "description": self.description,
62
+ }
63
+ if self.arguments:
64
+ result["arguments"] = [
65
+ {
66
+ "name": arg.name,
67
+ "description": arg.description,
68
+ "required": arg.required,
69
+ }
70
+ for arg in self.arguments
71
+ ]
72
+ return result
73
+
74
+ @abstractmethod
75
+ def get_messages(self, arguments: Dict[str, Any]) -> PromptResult:
76
+ """Generate prompt messages based on provided arguments."""
@@ -0,0 +1,39 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Registry that keeps the mapping between prompt names and instances."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Dict, Iterable, List
7
+
8
+ from fenix_mcp.application.prompt_base import Prompt, PromptResult
9
+
10
+
11
+ class PromptRegistry:
12
+ """Lookup table for prompt execution."""
13
+
14
+ def __init__(self, prompts: Iterable[Prompt]):
15
+ self._prompts: Dict[str, Prompt] = {}
16
+ for prompt in prompts:
17
+ if prompt.name in self._prompts:
18
+ raise ValueError(f"Duplicate prompt name detected: {prompt.name}")
19
+ self._prompts[prompt.name] = prompt
20
+
21
+ def list_definitions(self) -> List[dict]:
22
+ """Return list of prompt schemas for prompts/list."""
23
+ return [prompt.schema() for prompt in self._prompts.values()]
24
+
25
+ def get(self, name: str, arguments: Dict[str, Any]) -> PromptResult:
26
+ """Get prompt messages for prompts/get."""
27
+ try:
28
+ prompt = self._prompts[name]
29
+ except KeyError as exc:
30
+ raise KeyError(f"Unknown prompt '{name}'") from exc
31
+ return prompt.get_messages(arguments)
32
+
33
+
34
+ def build_default_prompt_registry() -> PromptRegistry:
35
+ """Build the default prompt registry with all available prompts."""
36
+ from fenix_mcp.application.prompts import build_prompts
37
+
38
+ prompts = build_prompts()
39
+ return PromptRegistry(prompts)
@@ -0,0 +1,28 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Prompt implementations for Fenix MCP server."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import List
7
+
8
+ from fenix_mcp.application.prompt_base import Prompt
9
+ from fenix_mcp.application.prompts.memory import FenixMemoryPrompt
10
+ from fenix_mcp.application.prompts.mine import FenixMinePrompt
11
+ from fenix_mcp.application.prompts.rules import FenixRulesPrompt
12
+
13
+
14
+ def build_prompts() -> List[Prompt]:
15
+ """Build and return all available prompts."""
16
+ return [
17
+ FenixMemoryPrompt(),
18
+ FenixMinePrompt(),
19
+ FenixRulesPrompt(),
20
+ ]
21
+
22
+
23
+ __all__ = [
24
+ "build_prompts",
25
+ "FenixMemoryPrompt",
26
+ "FenixMinePrompt",
27
+ "FenixRulesPrompt",
28
+ ]
@@ -0,0 +1,60 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Fenix Memory prompt - save content to semantic memory."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Dict, List
7
+
8
+ from fenix_mcp.application.prompt_base import (
9
+ Prompt,
10
+ PromptArgument,
11
+ PromptMessage,
12
+ PromptResult,
13
+ )
14
+
15
+
16
+ class FenixMemoryPrompt(Prompt):
17
+ """Prompt to save content to Fenix semantic memory."""
18
+
19
+ name = "fenix-memory"
20
+ description = "Save content to Fenix semantic memory for future reference"
21
+ arguments: List[PromptArgument] = [
22
+ PromptArgument(
23
+ name="content",
24
+ description="The content to save to memory",
25
+ required=True,
26
+ ),
27
+ ]
28
+
29
+ def get_messages(self, arguments: Dict[str, Any]) -> PromptResult:
30
+ content = arguments.get("content", "")
31
+
32
+ if not content:
33
+ return PromptResult(
34
+ messages=[
35
+ PromptMessage(
36
+ role="user",
37
+ text=(
38
+ "I want to save something to Fenix memory but didn't "
39
+ "provide content. Please ask me what I want to save."
40
+ ),
41
+ )
42
+ ]
43
+ )
44
+
45
+ instruction = f"""Save the following content to Fenix semantic memory using \
46
+ the `mcp__fenix__intelligence` tool with `action: memory_save`.
47
+
48
+ **Content to save:**
49
+ {content}
50
+
51
+ **Instructions:**
52
+ 1. Create an appropriate title that summarizes the content
53
+ 2. Generate relevant tags (at least 2-3 tags)
54
+ 3. Use the content as provided for the memory content field
55
+ 4. Confirm once saved successfully"""
56
+
57
+ return PromptResult(
58
+ description="Save content to Fenix semantic memory",
59
+ messages=[PromptMessage(role="user", text=instruction)],
60
+ )
@@ -0,0 +1,37 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Fenix Mine prompt - list work items assigned to the user."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Dict, List
7
+
8
+ from fenix_mcp.application.prompt_base import (
9
+ Prompt,
10
+ PromptArgument,
11
+ PromptMessage,
12
+ PromptResult,
13
+ )
14
+
15
+
16
+ class FenixMinePrompt(Prompt):
17
+ """Prompt to list work items assigned to the current user."""
18
+
19
+ name = "fenix-mine"
20
+ description = "List all work items assigned to me"
21
+ arguments: List[PromptArgument] = []
22
+
23
+ def get_messages(self, arguments: Dict[str, Any]) -> PromptResult:
24
+ instruction = """List all work items assigned to me using the \
25
+ `mcp__fenix__knowledge` tool with `action: work_mine`.
26
+
27
+ **Instructions:**
28
+ 1. Fetch my assigned work items
29
+ 2. Present them in a clear, organized format (table or list)
30
+ 3. Include: key, title, status, priority, and type for each item
31
+ 4. Group by status if there are many items (e.g., In Progress, Pending, etc.)
32
+ 5. Highlight any high-priority or urgent items"""
33
+
34
+ return PromptResult(
35
+ description="List work items assigned to me",
36
+ messages=[PromptMessage(role="user", text=instruction)],
37
+ )
@@ -0,0 +1,37 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Fenix Rules prompt - list team coding rules and standards."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Dict, List
7
+
8
+ from fenix_mcp.application.prompt_base import (
9
+ Prompt,
10
+ PromptArgument,
11
+ PromptMessage,
12
+ PromptResult,
13
+ )
14
+
15
+
16
+ class FenixRulesPrompt(Prompt):
17
+ """Prompt to list team coding rules and standards."""
18
+
19
+ name = "fenix-rules"
20
+ description = "List team coding rules and standards"
21
+ arguments: List[PromptArgument] = []
22
+
23
+ def get_messages(self, arguments: Dict[str, Any]) -> PromptResult:
24
+ instruction = """List the team's coding rules and standards using the \
25
+ `mcp__fenix__knowledge` tool with `action: rule_list`.
26
+
27
+ **Instructions:**
28
+ 1. Fetch all active team rules
29
+ 2. Present them in a clear, organized format
30
+ 3. Include: rule name, description, and scope for each rule
31
+ 4. If there are many rules, group them by category or scope
32
+ 5. Highlight any rules that are particularly important or frequently referenced"""
33
+
34
+ return PromptResult(
35
+ description="List team coding rules and standards",
36
+ messages=[PromptMessage(role="user", text=instruction)],
37
+ )
@@ -127,8 +127,12 @@ class FenixApiClient:
127
127
  def get_health(self) -> Any:
128
128
  return self._request("GET", "/health")
129
129
 
130
- def get_profile(self) -> Any:
131
- return self._request("GET", "/api/auth/profile")
130
+ def get_profile(self, *, include_documents: bool = False) -> Any:
131
+ """Get user profile. Use include_documents=True to include core and user documents."""
132
+ params = self._build_params(
133
+ optional={"include": "documents" if include_documents else None}
134
+ )
135
+ return self._request("GET", "/api/auth/profile", params=params)
132
136
 
133
137
  # ------------------------------------------------------------------
134
138
  # Core documents (requires PAT authentication)
@@ -3,10 +3,15 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ import asyncio
6
7
  import uuid
7
- from dataclasses import dataclass
8
+ from dataclasses import dataclass, field
8
9
  from typing import Any, Dict, Optional
9
10
 
11
+ from fenix_mcp.application.prompt_registry import (
12
+ PromptRegistry,
13
+ build_default_prompt_registry,
14
+ )
10
15
  from fenix_mcp.application.tool_registry import ToolRegistry, build_default_registry
11
16
  from fenix_mcp.infrastructure.context import AppContext
12
17
 
@@ -15,29 +20,108 @@ class McpServerError(RuntimeError):
15
20
  pass
16
21
 
17
22
 
18
- @dataclass(slots=True)
23
+ @dataclass
19
24
  class SimpleMcpServer:
20
25
  context: AppContext
21
26
  registry: ToolRegistry
27
+ prompt_registry: PromptRegistry
22
28
  session_id: str
29
+ _init_instructions: Optional[str] = field(default=None, repr=False)
23
30
 
24
31
  def set_personal_access_token(self, token: Optional[str]) -> None:
25
32
  self.context.api_client.update_token(token)
26
33
 
34
+ async def _build_auto_init_instructions(self) -> str:
35
+ """Load Fenix context automatically on MCP protocol initialize."""
36
+ api = self.context.api_client
37
+ logger = self.context.logger
38
+
39
+ # Single API call to get profile with documents
40
+ try:
41
+ profile = await asyncio.to_thread(api.get_profile, include_documents=True)
42
+ except Exception as exc:
43
+ logger.warning("Auto-init API call failed: %s", exc)
44
+ profile = None
45
+
46
+ if profile is None:
47
+ profile = {}
48
+
49
+ core_documents = profile.get("coreDocuments") or []
50
+ user_documents = profile.get("userDocuments") or []
51
+
52
+ # Build context summary
53
+ user_info = (profile or {}).get("user") or {}
54
+ tenant_info = (profile or {}).get("tenant") or {}
55
+ team_info = (profile or {}).get("team") or {}
56
+
57
+ lines = [
58
+ "# Fenix Cloud Context (Auto-initialized)",
59
+ "",
60
+ "## User Context",
61
+ f"- **User**: {user_info.get('name', 'Unknown')} (`{user_info.get('id', 'N/A')}`)",
62
+ f"- **Tenant**: {tenant_info.get('name', 'Unknown')} (`{tenant_info.get('id', 'N/A')}`)",
63
+ f"- **Team**: {team_info.get('name', 'Unknown')} (`{team_info.get('id', 'N/A')}`)",
64
+ ]
65
+
66
+ if core_documents:
67
+ lines.extend(["", "## Core Documents"])
68
+ for doc in core_documents:
69
+ name = doc.get("name", "untitled")
70
+ content = doc.get("content", "")
71
+ metadata = doc.get("metadata", "")
72
+ lines.append(f"\n### {name}")
73
+ if content:
74
+ lines.append(content)
75
+ if metadata:
76
+ lines.append(f"\n**Metadata:**\n{metadata}")
77
+
78
+ if user_documents:
79
+ lines.extend(["", "## User Documents"])
80
+ for doc in user_documents:
81
+ name = doc.get("name", "untitled")
82
+ content = doc.get("content", "")
83
+ lines.append(f"\n### {name}")
84
+ if content:
85
+ lines.append(content)
86
+
87
+ lines.extend(
88
+ [
89
+ "",
90
+ "## Available Tools",
91
+ "Use `mcp__fenix__knowledge` for work items, docs, sprints, rules.",
92
+ "Use `mcp__fenix__intelligence` for semantic memory search/save.",
93
+ "Use `mcp__fenix__productivity` for TODOs.",
94
+ ]
95
+ )
96
+
97
+ return "\n".join(lines)
98
+
27
99
  async def handle(self, request: Dict[str, Any]) -> Optional[Dict[str, Any]]:
28
100
  method = request.get("method")
29
101
  request_id = request.get("id")
30
102
 
31
103
  if method == "initialize":
104
+ # Auto-load Fenix context
105
+ try:
106
+ self._init_instructions = await self._build_auto_init_instructions()
107
+ except Exception as exc:
108
+ self.context.logger.warning("Auto-init failed: %s", exc)
109
+ self._init_instructions = None
110
+
111
+ result = {
112
+ "protocolVersion": "2024-11-05",
113
+ "capabilities": {"tools": {}, "prompts": {}, "logging": {}},
114
+ "serverInfo": {"name": "fenix_cloud_mcp_py", "version": "0.1.0"},
115
+ "sessionId": self.session_id,
116
+ }
117
+
118
+ if self._init_instructions:
119
+ result["instructions"] = self._init_instructions
120
+
32
121
  return {
33
122
  "jsonrpc": "2.0",
34
123
  "id": request_id,
35
- "result": {
36
- "protocolVersion": "2024-11-05",
37
- "capabilities": {"tools": {}, "logging": {}},
38
- "serverInfo": {"name": "fenix_cloud_mcp_py", "version": "0.1.0"},
39
- "sessionId": self.session_id,
40
- },
124
+ "result": result,
41
125
  }
42
126
 
43
127
  if method == "tools/list":
@@ -59,6 +143,29 @@ class SimpleMcpServer:
59
143
 
60
144
  return {"jsonrpc": "2.0", "id": request_id, "result": result}
61
145
 
146
+ if method == "prompts/list":
147
+ return {
148
+ "jsonrpc": "2.0",
149
+ "id": request_id,
150
+ "result": {"prompts": self.prompt_registry.list_definitions()},
151
+ }
152
+
153
+ if method == "prompts/get":
154
+ params = request.get("params") or {}
155
+ name = params.get("name")
156
+ arguments = params.get("arguments") or {}
157
+
158
+ if not name:
159
+ raise McpServerError("Missing prompt name in prompts/get payload")
160
+
161
+ prompt_result = self.prompt_registry.get(name, arguments)
162
+
163
+ return {
164
+ "jsonrpc": "2.0",
165
+ "id": request_id,
166
+ "result": prompt_result.to_dict(),
167
+ }
168
+
62
169
  if method == "notifications/initialized":
63
170
  # Notifications do not require a response
64
171
  return None
@@ -80,8 +187,10 @@ class SimpleMcpServer:
80
187
 
81
188
  def build_server(context: AppContext) -> SimpleMcpServer:
82
189
  registry = build_default_registry(context)
190
+ prompt_registry = build_default_prompt_registry()
83
191
  return SimpleMcpServer(
84
192
  context=context,
85
193
  registry=registry,
194
+ prompt_registry=prompt_registry,
86
195
  session_id=str(uuid.uuid4()),
87
196
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fenix-mcp
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: Fênix Cloud MCP server implemented in Python
5
5
  Author: Fenix Inc
6
6
  Requires-Python: >=3.10
@@ -1,8 +1,14 @@
1
- fenix_mcp/__init__.py,sha256=ewwBzUPQxM4BTmesnfhTvSBnnkCQQPE4nOkAVo-lKYM,180
1
+ fenix_mcp/__init__.py,sha256=NhyfXjUDYQJ1T_aVSBfzjCFzfSguUe0vqJ-6cyfbZC0,180
2
2
  fenix_mcp/main.py,sha256=iJV-9btNMDJMObvcn7wBQdbLLKjkYCQ1ANGEwHGHlMU,2857
3
3
  fenix_mcp/application/presenters.py,sha256=fGME54PdCDhTBhXO-JUB9yLdBHiE1aeXLTC2fCuxnxM,689
4
+ fenix_mcp/application/prompt_base.py,sha256=PJ0XB-ao4bj4Hs27beQJewF8if18hufxUUyDn2gDaTU,2023
5
+ fenix_mcp/application/prompt_registry.py,sha256=gTB-lhQgj0Hhq3sPPMfYDxnfwFDlswgC6SGm9mHULWk,1423
4
6
  fenix_mcp/application/tool_base.py,sha256=ZCb9g4ij5Hbb0410NEZTYXvPWq-Zkg8ZCsinTa3gCY4,4741
5
7
  fenix_mcp/application/tool_registry.py,sha256=bPT5g8GfxG_qu28R1WaDOZHvtmG6TPDvZi8eWj1T9xE,1250
8
+ fenix_mcp/application/prompts/__init__.py,sha256=0wkPoNUYmIQKotam2qE8uGZmCi_CPGEdG8YP0vZ3LqM,721
9
+ fenix_mcp/application/prompts/memory.py,sha256=Nqpxqtk59DJVfv25DFqHwFjTNajGsW3IGTBflFplFQc,1869
10
+ fenix_mcp/application/prompts/mine.py,sha256=qYOaAckJxXk8diZ7LZp6f0vCbl15AxuR48SZzA7yVXk,1215
11
+ fenix_mcp/application/prompts/rules.py,sha256=RMI14UWfJls8tsr9fZtNSCnF4mCgfz0Wu4H1GjMhEjo,1219
6
12
  fenix_mcp/application/tools/__init__.py,sha256=Gi1YvYh-KdL9HD8gLVrknHrxiKKEOhHBEZ02KBXJaKQ,796
7
13
  fenix_mcp/application/tools/health.py,sha256=m5DxhoRbdwl6INzd6PISxv1NAv-ljCrezsr773VB0wE,834
8
14
  fenix_mcp/application/tools/initialize.py,sha256=YQpZ0K6uz_LiLdE_GdQ0uJKtj6cgFru38xwoaCl5Jmg,5146
@@ -20,11 +26,11 @@ fenix_mcp/infrastructure/context.py,sha256=kiDiamiPbHZpTGyZMylcQwtLhfaDXrxAkWSst
20
26
  fenix_mcp/infrastructure/http_client.py,sha256=uJwt_iBGSFa1XPFBeqtm7eznkEm8aZ1v2DSxYjloAQs,2753
21
27
  fenix_mcp/infrastructure/logging.py,sha256=bHrWlSi_0HshRe3--BK_5nzUszW-gh37q6jsd0ShS2Y,1371
22
28
  fenix_mcp/infrastructure/request_context.py,sha256=hAHXHh-SKizBN7-YgdaRv0JsRYXBdurO2sr9btHPjKI,1101
23
- fenix_mcp/infrastructure/fenix_api/client.py,sha256=fkXIzo2_Qs4AfnkPX8Z1k-3dwTTJ5wEG-MBCfUZ8Axo,29119
24
- fenix_mcp/interface/mcp_server.py,sha256=xSIMZwOaEwHLJ1BKy_agdMiep7HFhIgsUhOVz49DsyA,2728
29
+ fenix_mcp/infrastructure/fenix_api/client.py,sha256=rKezcAalo4_qKrXvayK62FO95vuxinn5r6R7eLIDjuc,29389
30
+ fenix_mcp/interface/mcp_server.py,sha256=pEG1xQF0apw1YkVYx-lA0PsLZ8nrGTJrYj_RyX45SdE,6699
25
31
  fenix_mcp/interface/transports.py,sha256=2zJtc-L73zasyiwQoZbvFJ0yT1bggL5WAa7Nm7zID3k,8502
26
- fenix_mcp-2.0.0.dist-info/METADATA,sha256=6oZbIkzpboBC9i1IzXjedPEo_mSECyo_9tbZ36oHQFA,7714
27
- fenix_mcp-2.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
28
- fenix_mcp-2.0.0.dist-info/entry_points.txt,sha256=o52x_YHBupEd-1Z1GSfUjv3gJrx5_I-EkHhCgt1WBaE,49
29
- fenix_mcp-2.0.0.dist-info/top_level.txt,sha256=2G1UtKpwjaIGQyE7sRoHecxaGLeuexfjrOUjv9DDKh4,10
30
- fenix_mcp-2.0.0.dist-info/RECORD,,
32
+ fenix_mcp-2.2.0.dist-info/METADATA,sha256=RyNU85E6X0qAI4rnBWq5qmPL76L9FVLlMkmZGv7Mehc,7714
33
+ fenix_mcp-2.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
34
+ fenix_mcp-2.2.0.dist-info/entry_points.txt,sha256=o52x_YHBupEd-1Z1GSfUjv3gJrx5_I-EkHhCgt1WBaE,49
35
+ fenix_mcp-2.2.0.dist-info/top_level.txt,sha256=2G1UtKpwjaIGQyE7sRoHecxaGLeuexfjrOUjv9DDKh4,10
36
+ fenix_mcp-2.2.0.dist-info/RECORD,,