fenix-mcp 2.0.0__tar.gz → 2.1.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.
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/PKG-INFO +1 -1
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/__init__.py +1 -1
- fenix_mcp-2.1.0/fenix_mcp/interface/mcp_server.py +183 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp.egg-info/PKG-INFO +1 -1
- fenix_mcp-2.0.0/fenix_mcp/interface/mcp_server.py +0 -87
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/README.md +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/application/presenters.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/application/tool_base.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/application/tool_registry.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/application/tools/__init__.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/application/tools/health.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/application/tools/initialize.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/application/tools/intelligence.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/application/tools/knowledge.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/application/tools/productivity.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/application/tools/user_config.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/domain/initialization.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/domain/intelligence.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/domain/knowledge.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/domain/productivity.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/domain/user_config.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/infrastructure/config.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/infrastructure/context.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/infrastructure/fenix_api/client.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/infrastructure/http_client.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/infrastructure/logging.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/infrastructure/request_context.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/interface/transports.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp/main.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp.egg-info/SOURCES.txt +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp.egg-info/dependency_links.txt +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp.egg-info/entry_points.txt +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp.egg-info/requires.txt +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/fenix_mcp.egg-info/top_level.txt +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/pyproject.toml +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.1.0}/setup.cfg +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Lightweight MCP server implementation backed by the tool registry."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from fenix_mcp.application.tool_registry import ToolRegistry, build_default_registry
|
|
12
|
+
from fenix_mcp.infrastructure.context import AppContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class McpServerError(RuntimeError):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class SimpleMcpServer:
|
|
21
|
+
context: AppContext
|
|
22
|
+
registry: ToolRegistry
|
|
23
|
+
session_id: str
|
|
24
|
+
_init_instructions: Optional[str] = field(default=None, repr=False)
|
|
25
|
+
|
|
26
|
+
def set_personal_access_token(self, token: Optional[str]) -> None:
|
|
27
|
+
self.context.api_client.update_token(token)
|
|
28
|
+
|
|
29
|
+
async def _build_auto_init_instructions(self) -> str:
|
|
30
|
+
"""Load Fenix context automatically on MCP protocol initialize."""
|
|
31
|
+
api = self.context.api_client
|
|
32
|
+
logger = self.context.logger
|
|
33
|
+
|
|
34
|
+
async def safe_call(func, *args, **kwargs):
|
|
35
|
+
try:
|
|
36
|
+
return await asyncio.to_thread(func, *args, **kwargs)
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
logger.warning("Auto-init API call failed: %s", exc)
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def extract_items(payload: Any, key: str) -> List[Dict[str, Any]]:
|
|
42
|
+
if payload is None:
|
|
43
|
+
return []
|
|
44
|
+
if isinstance(payload, list):
|
|
45
|
+
return [item for item in payload if isinstance(item, dict)]
|
|
46
|
+
if isinstance(payload, dict):
|
|
47
|
+
value = payload.get(key) or payload.get("data")
|
|
48
|
+
if isinstance(value, list):
|
|
49
|
+
return [item for item in value if isinstance(item, dict)]
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
# Fetch profile, core docs, and user docs in parallel
|
|
53
|
+
profile_task = safe_call(api.get_profile)
|
|
54
|
+
core_docs_task = safe_call(api.list_core_documents, return_content=True)
|
|
55
|
+
user_docs_task = safe_call(api.list_user_core_documents, return_content=True)
|
|
56
|
+
|
|
57
|
+
profile, core_docs_raw, user_docs_raw = await asyncio.gather(
|
|
58
|
+
profile_task, core_docs_task, user_docs_task
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
core_documents = extract_items(core_docs_raw, "coreDocuments")
|
|
62
|
+
user_documents = extract_items(user_docs_raw, "userCoreDocuments")
|
|
63
|
+
|
|
64
|
+
# Build context summary
|
|
65
|
+
user_info = (profile or {}).get("user") or {}
|
|
66
|
+
tenant_info = (profile or {}).get("tenant") or {}
|
|
67
|
+
team_info = (profile or {}).get("team") or {}
|
|
68
|
+
|
|
69
|
+
lines = [
|
|
70
|
+
"# Fenix Cloud Context (Auto-initialized)",
|
|
71
|
+
"",
|
|
72
|
+
"## User Context",
|
|
73
|
+
f"- **User**: {user_info.get('name', 'Unknown')} (`{user_info.get('id', 'N/A')}`)",
|
|
74
|
+
f"- **Tenant**: {tenant_info.get('name', 'Unknown')} (`{tenant_info.get('id', 'N/A')}`)",
|
|
75
|
+
f"- **Team**: {team_info.get('name', 'Unknown')} (`{team_info.get('id', 'N/A')}`)",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
if core_documents:
|
|
79
|
+
lines.extend(["", "## Core Documents"])
|
|
80
|
+
for doc in core_documents:
|
|
81
|
+
name = doc.get("name", "untitled")
|
|
82
|
+
content = doc.get("content", "")
|
|
83
|
+
metadata = doc.get("metadata", "")
|
|
84
|
+
lines.append(f"\n### {name}")
|
|
85
|
+
if content:
|
|
86
|
+
lines.append(content)
|
|
87
|
+
if metadata:
|
|
88
|
+
lines.append(f"\n**Metadata:**\n{metadata}")
|
|
89
|
+
|
|
90
|
+
if user_documents:
|
|
91
|
+
lines.extend(["", "## User Documents"])
|
|
92
|
+
for doc in user_documents:
|
|
93
|
+
name = doc.get("name", "untitled")
|
|
94
|
+
content = doc.get("content", "")
|
|
95
|
+
lines.append(f"\n### {name}")
|
|
96
|
+
if content:
|
|
97
|
+
lines.append(content)
|
|
98
|
+
|
|
99
|
+
lines.extend(
|
|
100
|
+
[
|
|
101
|
+
"",
|
|
102
|
+
"## Available Tools",
|
|
103
|
+
"Use `mcp__fenix__knowledge` for work items, docs, sprints, rules.",
|
|
104
|
+
"Use `mcp__fenix__intelligence` for semantic memory search/save.",
|
|
105
|
+
"Use `mcp__fenix__productivity` for TODOs.",
|
|
106
|
+
]
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return "\n".join(lines)
|
|
110
|
+
|
|
111
|
+
async def handle(self, request: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
112
|
+
method = request.get("method")
|
|
113
|
+
request_id = request.get("id")
|
|
114
|
+
|
|
115
|
+
if method == "initialize":
|
|
116
|
+
# Auto-load Fenix context
|
|
117
|
+
try:
|
|
118
|
+
self._init_instructions = await self._build_auto_init_instructions()
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
self.context.logger.warning("Auto-init failed: %s", exc)
|
|
121
|
+
self._init_instructions = None
|
|
122
|
+
|
|
123
|
+
result = {
|
|
124
|
+
"protocolVersion": "2024-11-05",
|
|
125
|
+
"capabilities": {"tools": {}, "logging": {}},
|
|
126
|
+
"serverInfo": {"name": "fenix_cloud_mcp_py", "version": "0.1.0"},
|
|
127
|
+
"sessionId": self.session_id,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if self._init_instructions:
|
|
131
|
+
result["instructions"] = self._init_instructions
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"jsonrpc": "2.0",
|
|
135
|
+
"id": request_id,
|
|
136
|
+
"result": result,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if method == "tools/list":
|
|
140
|
+
return {
|
|
141
|
+
"jsonrpc": "2.0",
|
|
142
|
+
"id": request_id,
|
|
143
|
+
"result": {"tools": self.registry.list_definitions()},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if method == "tools/call":
|
|
147
|
+
params = request.get("params") or {}
|
|
148
|
+
name = params.get("name")
|
|
149
|
+
arguments = params.get("arguments") or {}
|
|
150
|
+
|
|
151
|
+
if not name:
|
|
152
|
+
raise McpServerError("Missing tool name in tools/call payload")
|
|
153
|
+
|
|
154
|
+
result = await self.registry.execute(name, arguments, self.context)
|
|
155
|
+
|
|
156
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
|
157
|
+
|
|
158
|
+
if method == "notifications/initialized":
|
|
159
|
+
# Notifications do not require a response
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
if method == "notifications/cancelled":
|
|
163
|
+
# Client cancelled a request - no response needed
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
if method == "logging/setLevel":
|
|
167
|
+
# Acknowledge log level change request (we don't actually change anything)
|
|
168
|
+
return {
|
|
169
|
+
"jsonrpc": "2.0",
|
|
170
|
+
"id": request_id,
|
|
171
|
+
"result": {},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
raise McpServerError(f"Unsupported method: {method}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def build_server(context: AppContext) -> SimpleMcpServer:
|
|
178
|
+
registry = build_default_registry(context)
|
|
179
|
+
return SimpleMcpServer(
|
|
180
|
+
context=context,
|
|
181
|
+
registry=registry,
|
|
182
|
+
session_id=str(uuid.uuid4()),
|
|
183
|
+
)
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
-
"""Lightweight MCP server implementation backed by the tool registry."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import uuid
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from typing import Any, Dict, Optional
|
|
9
|
-
|
|
10
|
-
from fenix_mcp.application.tool_registry import ToolRegistry, build_default_registry
|
|
11
|
-
from fenix_mcp.infrastructure.context import AppContext
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class McpServerError(RuntimeError):
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@dataclass(slots=True)
|
|
19
|
-
class SimpleMcpServer:
|
|
20
|
-
context: AppContext
|
|
21
|
-
registry: ToolRegistry
|
|
22
|
-
session_id: str
|
|
23
|
-
|
|
24
|
-
def set_personal_access_token(self, token: Optional[str]) -> None:
|
|
25
|
-
self.context.api_client.update_token(token)
|
|
26
|
-
|
|
27
|
-
async def handle(self, request: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
28
|
-
method = request.get("method")
|
|
29
|
-
request_id = request.get("id")
|
|
30
|
-
|
|
31
|
-
if method == "initialize":
|
|
32
|
-
return {
|
|
33
|
-
"jsonrpc": "2.0",
|
|
34
|
-
"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
|
-
},
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if method == "tools/list":
|
|
44
|
-
return {
|
|
45
|
-
"jsonrpc": "2.0",
|
|
46
|
-
"id": request_id,
|
|
47
|
-
"result": {"tools": self.registry.list_definitions()},
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if method == "tools/call":
|
|
51
|
-
params = request.get("params") or {}
|
|
52
|
-
name = params.get("name")
|
|
53
|
-
arguments = params.get("arguments") or {}
|
|
54
|
-
|
|
55
|
-
if not name:
|
|
56
|
-
raise McpServerError("Missing tool name in tools/call payload")
|
|
57
|
-
|
|
58
|
-
result = await self.registry.execute(name, arguments, self.context)
|
|
59
|
-
|
|
60
|
-
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
|
61
|
-
|
|
62
|
-
if method == "notifications/initialized":
|
|
63
|
-
# Notifications do not require a response
|
|
64
|
-
return None
|
|
65
|
-
|
|
66
|
-
if method == "notifications/cancelled":
|
|
67
|
-
# Client cancelled a request - no response needed
|
|
68
|
-
return None
|
|
69
|
-
|
|
70
|
-
if method == "logging/setLevel":
|
|
71
|
-
# Acknowledge log level change request (we don't actually change anything)
|
|
72
|
-
return {
|
|
73
|
-
"jsonrpc": "2.0",
|
|
74
|
-
"id": request_id,
|
|
75
|
-
"result": {},
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
raise McpServerError(f"Unsupported method: {method}")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def build_server(context: AppContext) -> SimpleMcpServer:
|
|
82
|
-
registry = build_default_registry(context)
|
|
83
|
-
return SimpleMcpServer(
|
|
84
|
-
context=context,
|
|
85
|
-
registry=registry,
|
|
86
|
-
session_id=str(uuid.uuid4()),
|
|
87
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|