kg-mcp 0.1.8__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.
- kg_mcp/__init__.py +5 -0
- kg_mcp/__main__.py +8 -0
- kg_mcp/cli/__init__.py +3 -0
- kg_mcp/cli/setup.py +1100 -0
- kg_mcp/cli/status.py +344 -0
- kg_mcp/codegraph/__init__.py +3 -0
- kg_mcp/codegraph/indexer.py +296 -0
- kg_mcp/codegraph/model.py +170 -0
- kg_mcp/config.py +83 -0
- kg_mcp/kg/__init__.py +3 -0
- kg_mcp/kg/apply_schema.py +93 -0
- kg_mcp/kg/ingest.py +253 -0
- kg_mcp/kg/neo4j.py +155 -0
- kg_mcp/kg/repo.py +756 -0
- kg_mcp/kg/retrieval.py +225 -0
- kg_mcp/kg/schema.cypher +176 -0
- kg_mcp/llm/__init__.py +4 -0
- kg_mcp/llm/client.py +291 -0
- kg_mcp/llm/prompts/__init__.py +8 -0
- kg_mcp/llm/prompts/extractor.py +84 -0
- kg_mcp/llm/prompts/linker.py +117 -0
- kg_mcp/llm/schemas.py +248 -0
- kg_mcp/main.py +195 -0
- kg_mcp/mcp/__init__.py +3 -0
- kg_mcp/mcp/change_schemas.py +140 -0
- kg_mcp/mcp/prompts.py +223 -0
- kg_mcp/mcp/resources.py +218 -0
- kg_mcp/mcp/tools.py +537 -0
- kg_mcp/security/__init__.py +3 -0
- kg_mcp/security/auth.py +121 -0
- kg_mcp/security/origin.py +112 -0
- kg_mcp/utils.py +100 -0
- kg_mcp-0.1.8.dist-info/METADATA +86 -0
- kg_mcp-0.1.8.dist-info/RECORD +36 -0
- kg_mcp-0.1.8.dist-info/WHEEL +4 -0
- kg_mcp-0.1.8.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic schemas for structured code change tracking.
|
|
3
|
+
|
|
4
|
+
These models define the format for kg_track_changes input,
|
|
5
|
+
enabling detailed tracking of file and symbol modifications.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Literal, Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SymbolChange(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
A modified symbol (function, class, method, variable).
|
|
16
|
+
|
|
17
|
+
This represents a single code symbol that was added, modified, or deleted
|
|
18
|
+
within a file. The agent should provide this information after making
|
|
19
|
+
changes to enable precise tracking in the knowledge graph.
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
{
|
|
23
|
+
"name": "calculate_tax",
|
|
24
|
+
"kind": "function",
|
|
25
|
+
"line_start": 10,
|
|
26
|
+
"line_end": 25,
|
|
27
|
+
"signature": "def calculate_tax(income: float) -> float",
|
|
28
|
+
"change_type": "modified"
|
|
29
|
+
}
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
name: str = Field(
|
|
33
|
+
...,
|
|
34
|
+
description="Symbol name. For methods, use 'ClassName.method_name' format. "
|
|
35
|
+
"Examples: 'calculate_tax', 'UserService.authenticate', 'CONFIG'"
|
|
36
|
+
)
|
|
37
|
+
kind: Literal["function", "method", "class", "variable"] = Field(
|
|
38
|
+
...,
|
|
39
|
+
description="Type of symbol: 'function' for standalone functions, "
|
|
40
|
+
"'method' for class methods, 'class' for class definitions, "
|
|
41
|
+
"'variable' for module-level constants/variables"
|
|
42
|
+
)
|
|
43
|
+
line_start: int = Field(
|
|
44
|
+
...,
|
|
45
|
+
ge=1,
|
|
46
|
+
description="First line number of the symbol definition (1-indexed)"
|
|
47
|
+
)
|
|
48
|
+
line_end: int = Field(
|
|
49
|
+
...,
|
|
50
|
+
ge=1,
|
|
51
|
+
description="Last line number of the symbol definition (1-indexed)"
|
|
52
|
+
)
|
|
53
|
+
signature: Optional[str] = Field(
|
|
54
|
+
None,
|
|
55
|
+
description="Full signature including parameters and return type. "
|
|
56
|
+
"Example: 'def calculate_tax(income: float, rate: float = 0.2) -> float'"
|
|
57
|
+
)
|
|
58
|
+
change_type: Literal["added", "modified", "deleted", "renamed"] = Field(
|
|
59
|
+
...,
|
|
60
|
+
description="What happened to this symbol: 'added' for new symbols, "
|
|
61
|
+
"'modified' for changed implementations, 'deleted' for removed symbols, "
|
|
62
|
+
"'renamed' for renamed symbols (old name)"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class FileChange(BaseModel):
|
|
67
|
+
"""
|
|
68
|
+
A single file modification with its symbols.
|
|
69
|
+
|
|
70
|
+
This represents a file that was created, modified, or deleted,
|
|
71
|
+
along with the specific symbols that changed within it.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
{
|
|
75
|
+
"path": "/project/src/utils.py",
|
|
76
|
+
"change_type": "modified",
|
|
77
|
+
"language": "python",
|
|
78
|
+
"symbols": [
|
|
79
|
+
{
|
|
80
|
+
"name": "format_currency",
|
|
81
|
+
"kind": "function",
|
|
82
|
+
"line_start": 45,
|
|
83
|
+
"line_end": 52,
|
|
84
|
+
"signature": "def format_currency(amount: float) -> str",
|
|
85
|
+
"change_type": "added"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
path: str = Field(
|
|
92
|
+
...,
|
|
93
|
+
description="Absolute or project-relative path to the file. "
|
|
94
|
+
"Examples: '/Users/dev/project/src/auth.py', 'src/auth.py'"
|
|
95
|
+
)
|
|
96
|
+
change_type: Literal["created", "modified", "deleted", "renamed"] = Field(
|
|
97
|
+
...,
|
|
98
|
+
description="What happened to this file: 'created' for new files, "
|
|
99
|
+
"'modified' for changed files, 'deleted' for removed files, "
|
|
100
|
+
"'renamed' for renamed files"
|
|
101
|
+
)
|
|
102
|
+
language: Optional[str] = Field(
|
|
103
|
+
None,
|
|
104
|
+
description="Programming language. Auto-detected from extension if not provided. "
|
|
105
|
+
"Examples: 'python', 'typescript', 'javascript', 'go', 'rust'"
|
|
106
|
+
)
|
|
107
|
+
symbols: List[SymbolChange] = Field(
|
|
108
|
+
default_factory=list,
|
|
109
|
+
description="List of symbols that changed in this file. "
|
|
110
|
+
"RECOMMENDED: Always provide this for better tracking. "
|
|
111
|
+
"Include ALL functions/classes/methods that were added, modified, or deleted."
|
|
112
|
+
)
|
|
113
|
+
content_hash: Optional[str] = Field(
|
|
114
|
+
None,
|
|
115
|
+
description="SHA256 hash of file content for change detection. "
|
|
116
|
+
"Optional but useful for deduplication."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TrackChangesInput(BaseModel):
|
|
121
|
+
"""
|
|
122
|
+
Complete input for kg_track_changes tool.
|
|
123
|
+
|
|
124
|
+
This is the structured format the agent should use when calling
|
|
125
|
+
kg_track_changes after making file modifications.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
project_id: str = Field(
|
|
129
|
+
...,
|
|
130
|
+
description="Project identifier. Use the workspace/repository folder name."
|
|
131
|
+
)
|
|
132
|
+
changes: List[FileChange] = Field(
|
|
133
|
+
...,
|
|
134
|
+
min_length=1,
|
|
135
|
+
description="List of file changes. Must include at least one file."
|
|
136
|
+
)
|
|
137
|
+
check_impact: bool = Field(
|
|
138
|
+
default=True,
|
|
139
|
+
description="Whether to run impact analysis to find affected goals/tests."
|
|
140
|
+
)
|
kg_mcp/mcp/prompts.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Prompt definitions for the Knowledge Graph Memory Server.
|
|
3
|
+
Prompts are reusable templates that instruct agents on workflows.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from mcp.server.fastmcp import FastMCP
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register_prompts(mcp: FastMCP) -> None:
|
|
14
|
+
"""Register all MCP prompts with the server."""
|
|
15
|
+
|
|
16
|
+
@mcp.prompt()
|
|
17
|
+
def StartCodingWithKG(project_id: str) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Instructs an IDE agent to use the Knowledge Graph for context-aware coding.
|
|
20
|
+
|
|
21
|
+
This prompt template should be invoked at the start of a coding session.
|
|
22
|
+
It guides the agent through the simplified 2-tool workflow.
|
|
23
|
+
"""
|
|
24
|
+
return f"""# Knowledge Graph-Powered Coding Assistant
|
|
25
|
+
|
|
26
|
+
You are a coding assistant enhanced with a persistent Knowledge Graph memory.
|
|
27
|
+
**Project ID:** `{project_id}`
|
|
28
|
+
|
|
29
|
+
## Your Workflow (Simplified)
|
|
30
|
+
|
|
31
|
+
### š Step 1: Start Every Task with kg_autopilot
|
|
32
|
+
|
|
33
|
+
**ALWAYS** call this at the START of every task:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
kg_autopilot(
|
|
37
|
+
project_id="{project_id}",
|
|
38
|
+
user_text="<paste the user's request here>",
|
|
39
|
+
files=["<relevant", "files>"],
|
|
40
|
+
search_query="<optional: keywords to search>"
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This single call:
|
|
45
|
+
- ā
Ingests and analyzes the request
|
|
46
|
+
- ā
Extracts goals, constraints, preferences
|
|
47
|
+
- ā
Returns the full context pack
|
|
48
|
+
- ā
Optionally searches existing knowledge
|
|
49
|
+
|
|
50
|
+
**Read the returned markdown context carefully!**
|
|
51
|
+
|
|
52
|
+
### š Step 2: Track Changes with kg_track_changes
|
|
53
|
+
|
|
54
|
+
**ALWAYS** call this AFTER modifying files:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
kg_track_changes(
|
|
58
|
+
project_id="{project_id}",
|
|
59
|
+
changed_paths=["path/to/modified/file.py"],
|
|
60
|
+
related_goal_ids=["<goal_id from context>"]
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This tracks your changes and returns impact analysis.
|
|
65
|
+
|
|
66
|
+
## Quick Reference
|
|
67
|
+
|
|
68
|
+
| When | Tool |
|
|
69
|
+
|------|------|
|
|
70
|
+
| START of every task | `kg_autopilot` |
|
|
71
|
+
| AFTER file changes | `kg_track_changes` |
|
|
72
|
+
|
|
73
|
+
## Important Rules
|
|
74
|
+
|
|
75
|
+
1. **Never skip kg_autopilot** at task start - it's your persistent memory
|
|
76
|
+
2. **Never skip kg_track_changes** after modifying code - it maintains traceability
|
|
77
|
+
3. **Read the context pack** - it contains goals, preferences, and pain points to avoid
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
Now, let's begin! What would you like to work on?
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@mcp.prompt()
|
|
86
|
+
def ReviewGoals(project_id: str) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Prompt for reviewing and managing project goals.
|
|
89
|
+
"""
|
|
90
|
+
return f"""# Goal Review Session
|
|
91
|
+
|
|
92
|
+
**Project:** `{project_id}`
|
|
93
|
+
|
|
94
|
+
Let's review the current goals for this project.
|
|
95
|
+
|
|
96
|
+
## To Get Started
|
|
97
|
+
|
|
98
|
+
1. First, let me fetch the active goals:
|
|
99
|
+
```
|
|
100
|
+
Use resource: kg://projects/{project_id}/active-goals
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
2. Then we can:
|
|
104
|
+
- Mark goals as complete
|
|
105
|
+
- Reprioritize goals
|
|
106
|
+
- Break down large goals into subgoals
|
|
107
|
+
- Identify blockers (pain points)
|
|
108
|
+
- Update strategies
|
|
109
|
+
|
|
110
|
+
## Questions to Consider
|
|
111
|
+
|
|
112
|
+
- Are all active goals still relevant?
|
|
113
|
+
- Are priorities correctly assigned?
|
|
114
|
+
- Are there any blocked goals that need attention?
|
|
115
|
+
- Should any goals be decomposed into smaller tasks?
|
|
116
|
+
|
|
117
|
+
Let me retrieve the current goals and we'll discuss.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
@mcp.prompt()
|
|
121
|
+
def DebugWithContext(project_id: str, error_message: str = "") -> str:
|
|
122
|
+
"""
|
|
123
|
+
Prompt for debugging with knowledge graph context.
|
|
124
|
+
"""
|
|
125
|
+
error_section = f"\n**Error:**\n```\n{error_message}\n```\n" if error_message else ""
|
|
126
|
+
|
|
127
|
+
return f"""# Debugging Session
|
|
128
|
+
|
|
129
|
+
**Project:** `{project_id}`
|
|
130
|
+
{error_section}
|
|
131
|
+
|
|
132
|
+
## Debugging Workflow
|
|
133
|
+
|
|
134
|
+
1. **Ingest the error context:**
|
|
135
|
+
```
|
|
136
|
+
kg_ingest_message(
|
|
137
|
+
project_id="{project_id}",
|
|
138
|
+
user_text="Debugging error: {error_message[:100] if error_message else 'describe the issue'}",
|
|
139
|
+
tags=["debug", "error"]
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
2. **Get relevant context:**
|
|
144
|
+
```
|
|
145
|
+
kg_context_pack(
|
|
146
|
+
project_id="{project_id}",
|
|
147
|
+
query="error debugging"
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
3. **Check for known pain points:**
|
|
152
|
+
- The context pack will include open pain points
|
|
153
|
+
- Check if this error relates to known issues
|
|
154
|
+
|
|
155
|
+
4. **Search for related code:**
|
|
156
|
+
```
|
|
157
|
+
kg_search(
|
|
158
|
+
project_id="{project_id}",
|
|
159
|
+
query="relevant keywords from error",
|
|
160
|
+
filters=["CodeArtifact"]
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
5. **If you find the fix**, remember to:
|
|
165
|
+
- Log it as a resolved pain point
|
|
166
|
+
- Link the fixed code to the goal
|
|
167
|
+
|
|
168
|
+
Let's start debugging!
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
@mcp.prompt()
|
|
172
|
+
def DocumentPreferences(project_id: str) -> str:
|
|
173
|
+
"""
|
|
174
|
+
Prompt for documenting user preferences.
|
|
175
|
+
"""
|
|
176
|
+
return f"""# Preference Documentation
|
|
177
|
+
|
|
178
|
+
**Project:** `{project_id}`
|
|
179
|
+
|
|
180
|
+
I'll help you document your coding preferences. These will be remembered
|
|
181
|
+
and applied to all future coding tasks.
|
|
182
|
+
|
|
183
|
+
## Categories to Consider
|
|
184
|
+
|
|
185
|
+
### 1. Coding Style
|
|
186
|
+
- Formatting preferences (tabs vs spaces, line length)
|
|
187
|
+
- Naming conventions (camelCase, snake_case)
|
|
188
|
+
- Comment style
|
|
189
|
+
- Import organization
|
|
190
|
+
|
|
191
|
+
### 2. Architecture
|
|
192
|
+
- Design patterns preferred (SOLID, DDD, etc.)
|
|
193
|
+
- Architectural style (monolith, microservices)
|
|
194
|
+
- Error handling approach
|
|
195
|
+
- Logging strategy
|
|
196
|
+
|
|
197
|
+
### 3. Testing
|
|
198
|
+
- Test framework preference
|
|
199
|
+
- Coverage requirements
|
|
200
|
+
- Test naming conventions
|
|
201
|
+
- Mock/stub strategy
|
|
202
|
+
|
|
203
|
+
### 4. Tools & Technologies
|
|
204
|
+
- Preferred libraries
|
|
205
|
+
- Build tools
|
|
206
|
+
- Linting/formatting tools
|
|
207
|
+
- CI/CD preferences
|
|
208
|
+
|
|
209
|
+
### 5. Output Format
|
|
210
|
+
- How you like explanations formatted
|
|
211
|
+
- Level of detail in comments
|
|
212
|
+
- Documentation style
|
|
213
|
+
|
|
214
|
+
## To Record a Preference
|
|
215
|
+
|
|
216
|
+
When you tell me a preference, I'll save it using `kg_ingest_message`.
|
|
217
|
+
For example, if you say "I prefer functional programming patterns",
|
|
218
|
+
I'll extract and store that preference.
|
|
219
|
+
|
|
220
|
+
What preferences would you like to document?
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
logger.info("MCP prompts registered successfully")
|
kg_mcp/mcp/resources.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Resource definitions for the Knowledge Graph Memory Server.
|
|
3
|
+
Resources provide read-only access to graph data.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
from mcp.server.fastmcp import FastMCP
|
|
10
|
+
|
|
11
|
+
from kg_mcp.kg.repo import get_repository
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_resources(mcp: FastMCP) -> None:
|
|
17
|
+
"""Register all MCP resources with the server."""
|
|
18
|
+
|
|
19
|
+
@mcp.resource("kg://projects/{project_id}/active-goals")
|
|
20
|
+
async def get_active_goals(project_id: str) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Get all active goals for a project.
|
|
23
|
+
|
|
24
|
+
Returns a Markdown-formatted list of active goals with their
|
|
25
|
+
acceptance criteria, constraints, and strategies.
|
|
26
|
+
"""
|
|
27
|
+
logger.info(f"Resource requested: active-goals for project {project_id}")
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
repo = get_repository()
|
|
31
|
+
goals = await repo.get_active_goals(project_id)
|
|
32
|
+
|
|
33
|
+
if not goals:
|
|
34
|
+
return f"# Active Goals for {project_id}\n\nNo active goals found."
|
|
35
|
+
|
|
36
|
+
lines = [f"# Active Goals for {project_id}\n"]
|
|
37
|
+
|
|
38
|
+
for i, goal in enumerate(goals, 1):
|
|
39
|
+
priority = goal.get("priority", 3)
|
|
40
|
+
emoji = {1: "š“", 2: "š ", 3: "š”", 4: "š¢", 5: "āŖ"}.get(priority, "āŖ")
|
|
41
|
+
|
|
42
|
+
lines.append(f"## {i}. {emoji} {goal.get('title', 'Untitled')}")
|
|
43
|
+
lines.append(f"**ID:** `{goal.get('id', 'N/A')}`")
|
|
44
|
+
lines.append(f"**Priority:** {priority}")
|
|
45
|
+
|
|
46
|
+
if goal.get("description"):
|
|
47
|
+
lines.append(f"\n{goal['description']}")
|
|
48
|
+
|
|
49
|
+
if goal.get("acceptance_criteria"):
|
|
50
|
+
lines.append("\n**Acceptance Criteria:**")
|
|
51
|
+
for ac in goal["acceptance_criteria"]:
|
|
52
|
+
criterion = ac.get("criterion", ac) if isinstance(ac, dict) else ac
|
|
53
|
+
lines.append(f"- [ ] {criterion}")
|
|
54
|
+
|
|
55
|
+
if goal.get("constraints"):
|
|
56
|
+
lines.append("\n**Constraints:**")
|
|
57
|
+
for c in goal["constraints"]:
|
|
58
|
+
if isinstance(c, dict) and c:
|
|
59
|
+
lines.append(f"- [{c.get('severity', 'must')}] {c.get('description', '')}")
|
|
60
|
+
|
|
61
|
+
if goal.get("strategies"):
|
|
62
|
+
lines.append("\n**Strategies:**")
|
|
63
|
+
for s in goal["strategies"]:
|
|
64
|
+
if isinstance(s, dict) and s:
|
|
65
|
+
lines.append(f"- {s.get('title', 'Strategy')}: {s.get('approach', '')}")
|
|
66
|
+
|
|
67
|
+
lines.append("")
|
|
68
|
+
|
|
69
|
+
return "\n".join(lines)
|
|
70
|
+
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"Failed to get active goals: {e}")
|
|
73
|
+
return f"# Error\n\nFailed to retrieve active goals: {e}"
|
|
74
|
+
|
|
75
|
+
@mcp.resource("kg://projects/{project_id}/preferences")
|
|
76
|
+
async def get_preferences(project_id: str) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Get user preferences for a project.
|
|
79
|
+
|
|
80
|
+
Returns coding style preferences, architectural preferences,
|
|
81
|
+
tool preferences, and output format preferences.
|
|
82
|
+
"""
|
|
83
|
+
logger.info(f"Resource requested: preferences for project {project_id}")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
repo = get_repository()
|
|
87
|
+
# Use default_user for now; could be parameterized
|
|
88
|
+
preferences = await repo.get_preferences("default_user")
|
|
89
|
+
|
|
90
|
+
if not preferences:
|
|
91
|
+
return f"# Preferences for {project_id}\n\nNo preferences configured."
|
|
92
|
+
|
|
93
|
+
lines = [f"# Preferences for {project_id}\n"]
|
|
94
|
+
|
|
95
|
+
# Group by category
|
|
96
|
+
by_category: Dict[str, List[Dict[str, Any]]] = {}
|
|
97
|
+
for pref in preferences:
|
|
98
|
+
cat = pref.get("category", "other")
|
|
99
|
+
if cat not in by_category:
|
|
100
|
+
by_category[cat] = []
|
|
101
|
+
by_category[cat].append(pref)
|
|
102
|
+
|
|
103
|
+
for category, prefs in by_category.items():
|
|
104
|
+
lines.append(f"## {category.replace('_', ' ').title()}\n")
|
|
105
|
+
for p in prefs:
|
|
106
|
+
strength = p.get("strength", "prefer")
|
|
107
|
+
prefix = {
|
|
108
|
+
"require": "ā
REQUIRE",
|
|
109
|
+
"avoid": "ā AVOID",
|
|
110
|
+
"prefer": "š” PREFER",
|
|
111
|
+
}.get(strength, "š”")
|
|
112
|
+
lines.append(f"- {prefix}: {p.get('preference', '')}")
|
|
113
|
+
lines.append("")
|
|
114
|
+
|
|
115
|
+
return "\n".join(lines)
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.error(f"Failed to get preferences: {e}")
|
|
119
|
+
return f"# Error\n\nFailed to retrieve preferences: {e}"
|
|
120
|
+
|
|
121
|
+
@mcp.resource("kg://projects/{project_id}/goal/{goal_id}/subgraph")
|
|
122
|
+
async def get_goal_subgraph(project_id: str, goal_id: str) -> str:
|
|
123
|
+
"""
|
|
124
|
+
Get the subgraph around a specific goal.
|
|
125
|
+
|
|
126
|
+
Returns the goal and all entities connected within 2 hops:
|
|
127
|
+
constraints, strategies, pain points, code artifacts, tests.
|
|
128
|
+
"""
|
|
129
|
+
logger.info(f"Resource requested: subgraph for goal {goal_id}")
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
repo = get_repository()
|
|
133
|
+
subgraph = await repo.get_goal_subgraph(goal_id, k_hops=2)
|
|
134
|
+
|
|
135
|
+
if not subgraph.get("goal"):
|
|
136
|
+
return f"# Goal Subgraph\n\nGoal `{goal_id}` not found."
|
|
137
|
+
|
|
138
|
+
goal = subgraph["goal"]
|
|
139
|
+
connected = subgraph.get("connected", [])
|
|
140
|
+
|
|
141
|
+
lines = [
|
|
142
|
+
f"# Goal: {goal.get('title', 'Untitled')}",
|
|
143
|
+
f"**ID:** `{goal_id}`",
|
|
144
|
+
f"**Status:** {goal.get('status', 'unknown')}",
|
|
145
|
+
f"**Priority:** {goal.get('priority', '-')}",
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
if goal.get("description"):
|
|
149
|
+
lines.append(f"\n{goal['description']}")
|
|
150
|
+
|
|
151
|
+
if connected:
|
|
152
|
+
lines.append(f"\n## Connected Entities ({len(connected)} total)\n")
|
|
153
|
+
|
|
154
|
+
# Categorize connected nodes
|
|
155
|
+
for node in connected:
|
|
156
|
+
if isinstance(node, dict):
|
|
157
|
+
# Try to identify node type by properties
|
|
158
|
+
if "approach" in node:
|
|
159
|
+
lines.append(f"- **Strategy:** {node.get('title', 'Untitled')}")
|
|
160
|
+
elif "severity" in node and "description" in node:
|
|
161
|
+
lines.append(f"- **PainPoint:** {node.get('description', '')[:50]}...")
|
|
162
|
+
elif "path" in node:
|
|
163
|
+
lines.append(f"- **CodeArtifact:** `{node.get('path', '')}`")
|
|
164
|
+
elif "criterion" in node:
|
|
165
|
+
lines.append(f"- **AcceptanceCriteria:** {node.get('criterion', '')}")
|
|
166
|
+
else:
|
|
167
|
+
lines.append(f"- **Entity:** {node}")
|
|
168
|
+
|
|
169
|
+
return "\n".join(lines)
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"Failed to get goal subgraph: {e}")
|
|
173
|
+
return f"# Error\n\nFailed to retrieve goal subgraph: {e}"
|
|
174
|
+
|
|
175
|
+
@mcp.resource("kg://projects/{project_id}/pain-points")
|
|
176
|
+
async def get_pain_points(project_id: str) -> str:
|
|
177
|
+
"""
|
|
178
|
+
Get open pain points for a project.
|
|
179
|
+
|
|
180
|
+
Returns unresolved pain points sorted by severity.
|
|
181
|
+
"""
|
|
182
|
+
logger.info(f"Resource requested: pain-points for project {project_id}")
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
repo = get_repository()
|
|
186
|
+
pain_points = await repo.get_open_painpoints(project_id)
|
|
187
|
+
|
|
188
|
+
if not pain_points:
|
|
189
|
+
return f"# Open Pain Points for {project_id}\n\nā
No open pain points!"
|
|
190
|
+
|
|
191
|
+
lines = [f"# Open Pain Points for {project_id}\n"]
|
|
192
|
+
|
|
193
|
+
severity_emoji = {
|
|
194
|
+
"critical": "š“",
|
|
195
|
+
"high": "š ",
|
|
196
|
+
"medium": "š”",
|
|
197
|
+
"low": "š¢",
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for pp in pain_points:
|
|
201
|
+
severity = pp.get("severity", "medium")
|
|
202
|
+
emoji = severity_emoji.get(severity, "āŖ")
|
|
203
|
+
lines.append(f"## {emoji} [{severity.upper()}] Pain Point")
|
|
204
|
+
lines.append(f"**ID:** `{pp.get('id', 'N/A')}`")
|
|
205
|
+
lines.append(f"\n{pp.get('description', 'No description')}")
|
|
206
|
+
|
|
207
|
+
if pp.get("blocking_goals"):
|
|
208
|
+
lines.append(f"\n**Blocking Goals:** {', '.join(pp['blocking_goals'])}")
|
|
209
|
+
|
|
210
|
+
lines.append("")
|
|
211
|
+
|
|
212
|
+
return "\n".join(lines)
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.error(f"Failed to get pain points: {e}")
|
|
216
|
+
return f"# Error\n\nFailed to retrieve pain points: {e}"
|
|
217
|
+
|
|
218
|
+
logger.info("MCP resources registered successfully")
|