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.
@@ -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")
@@ -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")