fenix-mcp 2.0.0__tar.gz → 2.2.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.2.0}/PKG-INFO +1 -1
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/__init__.py +1 -1
- fenix_mcp-2.2.0/fenix_mcp/application/prompt_base.py +76 -0
- fenix_mcp-2.2.0/fenix_mcp/application/prompt_registry.py +39 -0
- fenix_mcp-2.2.0/fenix_mcp/application/prompts/__init__.py +28 -0
- fenix_mcp-2.2.0/fenix_mcp/application/prompts/memory.py +60 -0
- fenix_mcp-2.2.0/fenix_mcp/application/prompts/mine.py +37 -0
- fenix_mcp-2.2.0/fenix_mcp/application/prompts/rules.py +37 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/infrastructure/fenix_api/client.py +6 -2
- fenix_mcp-2.2.0/fenix_mcp/interface/mcp_server.py +196 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp.egg-info/PKG-INFO +1 -1
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp.egg-info/SOURCES.txt +6 -0
- fenix_mcp-2.0.0/fenix_mcp/interface/mcp_server.py +0 -87
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/README.md +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/application/presenters.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/application/tool_base.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/application/tool_registry.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/application/tools/__init__.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/application/tools/health.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/application/tools/initialize.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/application/tools/intelligence.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/application/tools/knowledge.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/application/tools/productivity.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/application/tools/user_config.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/domain/initialization.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/domain/intelligence.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/domain/knowledge.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/domain/productivity.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/domain/user_config.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/infrastructure/config.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/infrastructure/context.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/infrastructure/http_client.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/infrastructure/logging.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/infrastructure/request_context.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/interface/transports.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp/main.py +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp.egg-info/dependency_links.txt +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp.egg-info/entry_points.txt +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp.egg-info/requires.txt +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/fenix_mcp.egg-info/top_level.txt +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/pyproject.toml +0 -0
- {fenix_mcp-2.0.0 → fenix_mcp-2.2.0}/setup.cfg +0 -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
|
-
|
|
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)
|
|
@@ -0,0 +1,196 @@
|
|
|
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, Optional
|
|
10
|
+
|
|
11
|
+
from fenix_mcp.application.prompt_registry import (
|
|
12
|
+
PromptRegistry,
|
|
13
|
+
build_default_prompt_registry,
|
|
14
|
+
)
|
|
15
|
+
from fenix_mcp.application.tool_registry import ToolRegistry, build_default_registry
|
|
16
|
+
from fenix_mcp.infrastructure.context import AppContext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class McpServerError(RuntimeError):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SimpleMcpServer:
|
|
25
|
+
context: AppContext
|
|
26
|
+
registry: ToolRegistry
|
|
27
|
+
prompt_registry: PromptRegistry
|
|
28
|
+
session_id: str
|
|
29
|
+
_init_instructions: Optional[str] = field(default=None, repr=False)
|
|
30
|
+
|
|
31
|
+
def set_personal_access_token(self, token: Optional[str]) -> None:
|
|
32
|
+
self.context.api_client.update_token(token)
|
|
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
|
+
|
|
99
|
+
async def handle(self, request: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
100
|
+
method = request.get("method")
|
|
101
|
+
request_id = request.get("id")
|
|
102
|
+
|
|
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
|
+
|
|
121
|
+
return {
|
|
122
|
+
"jsonrpc": "2.0",
|
|
123
|
+
"id": request_id,
|
|
124
|
+
"result": result,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if method == "tools/list":
|
|
128
|
+
return {
|
|
129
|
+
"jsonrpc": "2.0",
|
|
130
|
+
"id": request_id,
|
|
131
|
+
"result": {"tools": self.registry.list_definitions()},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if method == "tools/call":
|
|
135
|
+
params = request.get("params") or {}
|
|
136
|
+
name = params.get("name")
|
|
137
|
+
arguments = params.get("arguments") or {}
|
|
138
|
+
|
|
139
|
+
if not name:
|
|
140
|
+
raise McpServerError("Missing tool name in tools/call payload")
|
|
141
|
+
|
|
142
|
+
result = await self.registry.execute(name, arguments, self.context)
|
|
143
|
+
|
|
144
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
|
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
|
+
|
|
169
|
+
if method == "notifications/initialized":
|
|
170
|
+
# Notifications do not require a response
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
if method == "notifications/cancelled":
|
|
174
|
+
# Client cancelled a request - no response needed
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
if method == "logging/setLevel":
|
|
178
|
+
# Acknowledge log level change request (we don't actually change anything)
|
|
179
|
+
return {
|
|
180
|
+
"jsonrpc": "2.0",
|
|
181
|
+
"id": request_id,
|
|
182
|
+
"result": {},
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
raise McpServerError(f"Unsupported method: {method}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def build_server(context: AppContext) -> SimpleMcpServer:
|
|
189
|
+
registry = build_default_registry(context)
|
|
190
|
+
prompt_registry = build_default_prompt_registry()
|
|
191
|
+
return SimpleMcpServer(
|
|
192
|
+
context=context,
|
|
193
|
+
registry=registry,
|
|
194
|
+
prompt_registry=prompt_registry,
|
|
195
|
+
session_id=str(uuid.uuid4()),
|
|
196
|
+
)
|
|
@@ -9,8 +9,14 @@ fenix_mcp.egg-info/entry_points.txt
|
|
|
9
9
|
fenix_mcp.egg-info/requires.txt
|
|
10
10
|
fenix_mcp.egg-info/top_level.txt
|
|
11
11
|
fenix_mcp/application/presenters.py
|
|
12
|
+
fenix_mcp/application/prompt_base.py
|
|
13
|
+
fenix_mcp/application/prompt_registry.py
|
|
12
14
|
fenix_mcp/application/tool_base.py
|
|
13
15
|
fenix_mcp/application/tool_registry.py
|
|
16
|
+
fenix_mcp/application/prompts/__init__.py
|
|
17
|
+
fenix_mcp/application/prompts/memory.py
|
|
18
|
+
fenix_mcp/application/prompts/mine.py
|
|
19
|
+
fenix_mcp/application/prompts/rules.py
|
|
14
20
|
fenix_mcp/application/tools/__init__.py
|
|
15
21
|
fenix_mcp/application/tools/health.py
|
|
16
22
|
fenix_mcp/application/tools/initialize.py
|
|
@@ -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
|