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,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extractor prompt template for entity extraction from user text.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
EXTRACTOR_SYSTEM_PROMPT = """You are an expert at analyzing developer requests and extracting structured information.
|
|
9
|
+
Your task is to analyze the user's message and extract:
|
|
10
|
+
1. GOALS: What the user wants to achieve (objectives, features, fixes)
|
|
11
|
+
2. CONSTRAINTS: Limitations or requirements (budget, technology stack, performance, time)
|
|
12
|
+
3. PREFERENCES: User's coding/architecture preferences (patterns, styles, tools)
|
|
13
|
+
4. PAIN_POINTS: Problems or frustrations mentioned
|
|
14
|
+
5. STRATEGIES: Approaches or plans mentioned for solving problems (track Success/Failure outcomes!)
|
|
15
|
+
6. ACCEPTANCE_CRITERIA: Success conditions mentioned
|
|
16
|
+
7. CODE_REFERENCES: Any files, functions, classes, or code snippets referenced
|
|
17
|
+
8. NEXT_ACTIONS: Immediate next steps implied by the request
|
|
18
|
+
|
|
19
|
+
IMPORTANT RULES:
|
|
20
|
+
- Only extract information that is explicitly stated or strongly implied
|
|
21
|
+
- Be precise and concise in descriptions
|
|
22
|
+
- For code_references, extract paths exactly as mentioned
|
|
23
|
+
- Set confidence based on how clear the extraction is (0.0 to 1.0)
|
|
24
|
+
- If the message is just a greeting or acknowledgment, return empty arrays
|
|
25
|
+
- **CRITICAL**: If a strategy is mentioned as successful or failed, mark the `outcome` field!
|
|
26
|
+
|
|
27
|
+
OUTPUT FORMAT (JSON):
|
|
28
|
+
{
|
|
29
|
+
"goals": [{"title": "...", "description": "...", "priority": 1-5, "status": "active", "parent_goal_title": null}],
|
|
30
|
+
"constraints": [{"type": "budget|stack|style|performance|time", "description": "...", "severity": "must|should|nice_to_have"}],
|
|
31
|
+
"preferences": [{"category": "coding_style|architecture|testing|tools|output_format", "preference": "...", "strength": "prefer|avoid|require"}],
|
|
32
|
+
"pain_points": [{"description": "...", "severity": "low|medium|high|critical", "related_goal": null}],
|
|
33
|
+
"strategies": [{"title": "...", "approach": "...", "rationale": "...", "outcome": "success|failure|pending", "outcome_reason": "...", "related_goal": null}],
|
|
34
|
+
"acceptance_criteria": [{"criterion": "...", "related_goal": "...", "testable": true}],
|
|
35
|
+
"code_references": [{"path": "...", "symbol": null, "start_line": null, "end_line": null, "action": "reference|create|modify|delete"}],
|
|
36
|
+
"next_actions": ["action 1", "action 2"],
|
|
37
|
+
"confidence": 0.85
|
|
38
|
+
}"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_extractor_prompt(
|
|
42
|
+
user_text: str,
|
|
43
|
+
files: Optional[List[str]] = None,
|
|
44
|
+
diff: Optional[str] = None,
|
|
45
|
+
symbols: Optional[List[str]] = None,
|
|
46
|
+
context: Optional[str] = None,
|
|
47
|
+
) -> Tuple[str, str]:
|
|
48
|
+
"""
|
|
49
|
+
Build the extractor prompt for entity extraction.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
user_text: The user's message
|
|
53
|
+
files: Optional list of files involved
|
|
54
|
+
diff: Optional code diff
|
|
55
|
+
symbols: Optional list of symbols
|
|
56
|
+
context: Optional additional context
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Tuple of (system_prompt, user_prompt)
|
|
60
|
+
"""
|
|
61
|
+
user_prompt_parts = [f"USER MESSAGE:\n{user_text}"]
|
|
62
|
+
|
|
63
|
+
if files:
|
|
64
|
+
files_str = "\n".join(f"- {f}" for f in files)
|
|
65
|
+
user_prompt_parts.append(f"\nFILES INVOLVED:\n{files_str}")
|
|
66
|
+
|
|
67
|
+
if diff:
|
|
68
|
+
# Truncate large diffs
|
|
69
|
+
truncated_diff = diff[:2000] + "..." if len(diff) > 2000 else diff
|
|
70
|
+
user_prompt_parts.append(f"\nCODE DIFF:\n```\n{truncated_diff}\n```")
|
|
71
|
+
|
|
72
|
+
if symbols:
|
|
73
|
+
symbols_str = ", ".join(symbols)
|
|
74
|
+
user_prompt_parts.append(f"\nSYMBOLS REFERENCED: {symbols_str}")
|
|
75
|
+
|
|
76
|
+
if context:
|
|
77
|
+
user_prompt_parts.append(f"\nADDITIONAL CONTEXT:\n{context}")
|
|
78
|
+
|
|
79
|
+
user_prompt_parts.append(
|
|
80
|
+
"\n\nAnalyze the above and extract structured information. "
|
|
81
|
+
"Return a JSON object following the specified format."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return EXTRACTOR_SYSTEM_PROMPT, "\n".join(user_prompt_parts)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Linker prompt template for entity deduplication and relationship inference.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, List, Tuple
|
|
7
|
+
|
|
8
|
+
from kg_mcp.llm.schemas import ExtractionResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
LINKER_SYSTEM_PROMPT = """You are an expert at analyzing knowledge graphs and detecting duplicates/relationships.
|
|
12
|
+
|
|
13
|
+
Your task is to:
|
|
14
|
+
1. DETECT DUPLICATES: Check if any newly extracted entities are duplicates of existing ones
|
|
15
|
+
2. SUGGEST MERGES: If entities are the same or very similar, suggest merging them
|
|
16
|
+
3. INFER RELATIONSHIPS: Based on context, suggest relationships between entities
|
|
17
|
+
|
|
18
|
+
RELATIONSHIP TYPES:
|
|
19
|
+
- DECOMPOSES_INTO: A goal breaks down into subgoals
|
|
20
|
+
- HAS_CONSTRAINT: A goal has a constraint
|
|
21
|
+
- HAS_STRATEGY: A goal has a strategy for implementation
|
|
22
|
+
- BLOCKED_BY: An entity is blocked by a pain point
|
|
23
|
+
- IMPLEMENTED_BY: A goal is implemented by code artifacts
|
|
24
|
+
- VERIFIED_BY: A goal is verified by tests
|
|
25
|
+
- RELATED_TO: Generic relationship
|
|
26
|
+
|
|
27
|
+
RULES:
|
|
28
|
+
- Only suggest merges with high confidence (> 0.7)
|
|
29
|
+
- Consider semantic similarity, not just exact matches
|
|
30
|
+
- For relationships, provide clear reasoning
|
|
31
|
+
- Use existing entity IDs when available
|
|
32
|
+
|
|
33
|
+
OUTPUT FORMAT (JSON):
|
|
34
|
+
{
|
|
35
|
+
"merge_suggestions": [
|
|
36
|
+
{
|
|
37
|
+
"new_entity_type": "Goal|Preference|etc",
|
|
38
|
+
"new_entity_title": "...",
|
|
39
|
+
"existing_entity_id": "uuid",
|
|
40
|
+
"existing_entity_title": "...",
|
|
41
|
+
"confidence": 0.85,
|
|
42
|
+
"reason": "Why they should be merged"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"relationships": [
|
|
46
|
+
{
|
|
47
|
+
"source_type": "Goal",
|
|
48
|
+
"source_id": "uuid or null",
|
|
49
|
+
"source_title": "...",
|
|
50
|
+
"relationship_type": "DECOMPOSES_INTO|HAS_CONSTRAINT|etc",
|
|
51
|
+
"target_type": "SubGoal",
|
|
52
|
+
"target_id": "uuid or null",
|
|
53
|
+
"target_title": "...",
|
|
54
|
+
"confidence": 0.8
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_linker_prompt(
|
|
61
|
+
extraction: ExtractionResult,
|
|
62
|
+
existing_goals: List[Dict[str, Any]],
|
|
63
|
+
existing_preferences: List[Dict[str, Any]],
|
|
64
|
+
recent_interactions: List[Dict[str, Any]],
|
|
65
|
+
) -> Tuple[str, str]:
|
|
66
|
+
"""
|
|
67
|
+
Build the linker prompt for entity linking and relationship inference.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
extraction: The extraction result to link
|
|
71
|
+
existing_goals: List of existing goals from the graph
|
|
72
|
+
existing_preferences: List of existing preferences
|
|
73
|
+
recent_interactions: Recent interactions for context
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Tuple of (system_prompt, user_prompt)
|
|
77
|
+
"""
|
|
78
|
+
user_prompt_parts = []
|
|
79
|
+
|
|
80
|
+
# Add extracted entities
|
|
81
|
+
user_prompt_parts.append("NEWLY EXTRACTED ENTITIES:")
|
|
82
|
+
user_prompt_parts.append(f"```json\n{extraction.model_dump_json(indent=2)}\n```")
|
|
83
|
+
|
|
84
|
+
# Add existing goals
|
|
85
|
+
if existing_goals:
|
|
86
|
+
user_prompt_parts.append("\nEXISTING GOALS IN GRAPH:")
|
|
87
|
+
for goal in existing_goals[:20]: # Limit to 20
|
|
88
|
+
user_prompt_parts.append(
|
|
89
|
+
f"- ID: {goal.get('id')}, Title: {goal.get('title')}, "
|
|
90
|
+
f"Status: {goal.get('status')}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Add existing preferences
|
|
94
|
+
if existing_preferences:
|
|
95
|
+
user_prompt_parts.append("\nEXISTING PREFERENCES:")
|
|
96
|
+
for pref in existing_preferences[:10]:
|
|
97
|
+
user_prompt_parts.append(
|
|
98
|
+
f"- ID: {pref.get('id')}, Category: {pref.get('category')}, "
|
|
99
|
+
f"Preference: {pref.get('preference')}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Add recent interaction context
|
|
103
|
+
if recent_interactions:
|
|
104
|
+
user_prompt_parts.append("\nRECENT INTERACTIONS (for context):")
|
|
105
|
+
for interaction in recent_interactions[:5]:
|
|
106
|
+
user_prompt_parts.append(
|
|
107
|
+
f"- {interaction.get('timestamp', 'N/A')}: "
|
|
108
|
+
f"{interaction.get('user_text', '')[:100]}..."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
user_prompt_parts.append(
|
|
112
|
+
"\n\nAnalyze the newly extracted entities against the existing graph. "
|
|
113
|
+
"Suggest merges for duplicates and infer relationships. "
|
|
114
|
+
"Return a JSON object following the specified format."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return LINKER_SYSTEM_PROMPT, "\n".join(user_prompt_parts)
|
kg_mcp/llm/schemas.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic schemas for LLM input/output validation.
|
|
3
|
+
These define the structured format for entity extraction and linking.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# =============================================================================
|
|
14
|
+
# Extraction Schemas
|
|
15
|
+
# =============================================================================
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GoalExtract(BaseModel):
|
|
19
|
+
"""Extracted goal from user text."""
|
|
20
|
+
|
|
21
|
+
title: str = Field(..., description="Short title of the goal")
|
|
22
|
+
description: Optional[str] = Field(None, description="Detailed description")
|
|
23
|
+
priority: int = Field(default=2, ge=1, le=5, description="Priority 1-5 (1=highest)")
|
|
24
|
+
status: str = Field(default="active", description="Status: active, paused, done")
|
|
25
|
+
parent_goal_title: Optional[str] = Field(
|
|
26
|
+
None, description="Title of parent goal if this is a subgoal"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConstraintExtract(BaseModel):
|
|
31
|
+
"""Extracted constraint from user text."""
|
|
32
|
+
|
|
33
|
+
type: str = Field(..., description="Constraint type: budget, stack, style, performance, time")
|
|
34
|
+
description: str = Field(..., description="Description of the constraint")
|
|
35
|
+
severity: str = Field(default="must", description="Severity: must, should, nice_to_have")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PreferenceExtract(BaseModel):
|
|
39
|
+
"""Extracted preference from user text."""
|
|
40
|
+
|
|
41
|
+
category: str = Field(
|
|
42
|
+
...,
|
|
43
|
+
description="Category: coding_style, architecture, testing, tools, output_format",
|
|
44
|
+
)
|
|
45
|
+
preference: str = Field(..., description="The preference itself")
|
|
46
|
+
strength: str = Field(default="prefer", description="Strength: prefer, avoid, require")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PainPointExtract(BaseModel):
|
|
50
|
+
"""Extracted pain point from user text."""
|
|
51
|
+
|
|
52
|
+
description: str = Field(..., description="Description of the pain point")
|
|
53
|
+
severity: str = Field(default="medium", description="Severity: low, medium, high, critical")
|
|
54
|
+
related_goal: Optional[str] = Field(None, description="Related goal title if any")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class StrategyExtract(BaseModel):
|
|
58
|
+
"""Extracted strategy from user text."""
|
|
59
|
+
|
|
60
|
+
title: str = Field(..., description="Short title of the strategy")
|
|
61
|
+
approach: str = Field(..., description="Description of the approach")
|
|
62
|
+
rationale: Optional[str] = Field(None, description="Why this strategy was chosen")
|
|
63
|
+
outcome: Optional[str] = Field(None, description="Outcome of the strategy: success, failure, pending")
|
|
64
|
+
outcome_reason: Optional[str] = Field(None, description="Reason for the outcome")
|
|
65
|
+
related_goal: Optional[str] = Field(None, description="Related goal title if any")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AcceptanceCriteriaExtract(BaseModel):
|
|
69
|
+
"""Extracted acceptance criteria from user text."""
|
|
70
|
+
|
|
71
|
+
criterion: str = Field(..., description="The acceptance criterion")
|
|
72
|
+
related_goal: Optional[str] = Field(None, description="Related goal title")
|
|
73
|
+
testable: bool = Field(default=True, description="Whether it's testable")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CodeReference(BaseModel):
|
|
77
|
+
"""Reference to code in the message."""
|
|
78
|
+
|
|
79
|
+
path: str = Field(..., description="File path")
|
|
80
|
+
symbol: Optional[str] = Field(None, description="Symbol name (function/class)")
|
|
81
|
+
start_line: Optional[int] = Field(None, description="Start line number")
|
|
82
|
+
end_line: Optional[int] = Field(None, description="End line number")
|
|
83
|
+
action: str = Field(
|
|
84
|
+
default="reference", description="Action: reference, create, modify, delete"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ExtractionResult(BaseModel):
|
|
89
|
+
"""Complete result of entity extraction from user text."""
|
|
90
|
+
|
|
91
|
+
goals: List[GoalExtract] = Field(default_factory=list)
|
|
92
|
+
constraints: List[ConstraintExtract] = Field(default_factory=list)
|
|
93
|
+
preferences: List[PreferenceExtract] = Field(default_factory=list)
|
|
94
|
+
pain_points: List[PainPointExtract] = Field(default_factory=list)
|
|
95
|
+
strategies: List[StrategyExtract] = Field(default_factory=list)
|
|
96
|
+
acceptance_criteria: List[AcceptanceCriteriaExtract] = Field(default_factory=list)
|
|
97
|
+
code_references: List[CodeReference] = Field(default_factory=list)
|
|
98
|
+
next_actions: List[str] = Field(default_factory=list)
|
|
99
|
+
confidence: float = Field(default=0.8, ge=0.0, le=1.0)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# =============================================================================
|
|
103
|
+
# Linking Schemas
|
|
104
|
+
# =============================================================================
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class MergeSuggestion(BaseModel):
|
|
108
|
+
"""Suggestion to merge a new entity with an existing one."""
|
|
109
|
+
|
|
110
|
+
new_entity_type: str = Field(..., description="Type of the new entity")
|
|
111
|
+
new_entity_title: str = Field(..., description="Title of the new entity")
|
|
112
|
+
existing_entity_id: str = Field(..., description="ID of the existing entity to merge with")
|
|
113
|
+
existing_entity_title: str = Field(..., description="Title of existing entity")
|
|
114
|
+
confidence: float = Field(default=0.8, ge=0.0, le=1.0)
|
|
115
|
+
reason: str = Field(..., description="Why these should be merged")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class RelationshipSuggestion(BaseModel):
|
|
119
|
+
"""Suggestion for a new relationship between entities."""
|
|
120
|
+
|
|
121
|
+
source_type: str = Field(..., description="Type of source entity")
|
|
122
|
+
source_id: Optional[str] = Field(None, description="ID of source entity (if existing)")
|
|
123
|
+
source_title: str = Field(..., description="Title of source entity")
|
|
124
|
+
relationship_type: str = Field(..., description="Type of relationship (e.g., IMPLEMENTED_BY)")
|
|
125
|
+
target_type: str = Field(..., description="Type of target entity")
|
|
126
|
+
target_id: Optional[str] = Field(None, description="ID of target entity (if existing)")
|
|
127
|
+
target_title: str = Field(..., description="Title of target entity")
|
|
128
|
+
confidence: float = Field(default=0.8, ge=0.0, le=1.0)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class LinkingResult(BaseModel):
|
|
132
|
+
"""Result of entity linking analysis."""
|
|
133
|
+
|
|
134
|
+
merge_suggestions: List[MergeSuggestion] = Field(default_factory=list)
|
|
135
|
+
relationships: List[RelationshipSuggestion] = Field(default_factory=list)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# =============================================================================
|
|
139
|
+
# Graph Node Schemas (for Neo4j)
|
|
140
|
+
# =============================================================================
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class BaseNode(BaseModel):
|
|
144
|
+
"""Base class for all graph nodes."""
|
|
145
|
+
|
|
146
|
+
id: str = Field(default_factory=lambda: str(uuid4()))
|
|
147
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
148
|
+
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class InteractionNode(BaseNode):
|
|
152
|
+
"""Represents a user interaction/request."""
|
|
153
|
+
|
|
154
|
+
user_text: str
|
|
155
|
+
assistant_text: Optional[str] = None
|
|
156
|
+
tags: List[str] = Field(default_factory=list)
|
|
157
|
+
project_id: str
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class GoalNode(BaseNode):
|
|
161
|
+
"""Represents a goal or objective."""
|
|
162
|
+
|
|
163
|
+
title: str
|
|
164
|
+
description: Optional[str] = None
|
|
165
|
+
status: str = "active"
|
|
166
|
+
priority: int = 2
|
|
167
|
+
project_id: str
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ConstraintNode(BaseNode):
|
|
171
|
+
"""Represents a constraint."""
|
|
172
|
+
|
|
173
|
+
type: str
|
|
174
|
+
description: str
|
|
175
|
+
severity: str = "must"
|
|
176
|
+
project_id: str
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class PreferenceNode(BaseNode):
|
|
180
|
+
"""Represents a user preference."""
|
|
181
|
+
|
|
182
|
+
category: str
|
|
183
|
+
preference: str
|
|
184
|
+
strength: str = "prefer"
|
|
185
|
+
user_id: str
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class PainPointNode(BaseNode):
|
|
189
|
+
"""Represents a pain point."""
|
|
190
|
+
|
|
191
|
+
description: str
|
|
192
|
+
severity: str = "medium"
|
|
193
|
+
resolved: bool = False
|
|
194
|
+
project_id: str
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class StrategyNode(BaseNode):
|
|
198
|
+
"""Represents a strategy or approach."""
|
|
199
|
+
|
|
200
|
+
title: str
|
|
201
|
+
approach: str
|
|
202
|
+
rationale: Optional[str] = None
|
|
203
|
+
outcome: Optional[str] = None
|
|
204
|
+
outcome_reason: Optional[str] = None
|
|
205
|
+
project_id: str
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class DecisionNode(BaseNode):
|
|
209
|
+
"""Represents an ADR-lite decision."""
|
|
210
|
+
|
|
211
|
+
title: str
|
|
212
|
+
decision: str
|
|
213
|
+
rationale: str
|
|
214
|
+
alternatives: List[str] = Field(default_factory=list)
|
|
215
|
+
project_id: str
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class CodeArtifactNode(BaseNode):
|
|
219
|
+
"""Represents a code artifact (file, function, class, snippet)."""
|
|
220
|
+
|
|
221
|
+
path: str
|
|
222
|
+
language: Optional[str] = None
|
|
223
|
+
kind: str = "file" # file, function, class, snippet
|
|
224
|
+
git_commit: Optional[str] = None
|
|
225
|
+
content_hash: Optional[str] = None
|
|
226
|
+
start_line: Optional[int] = None
|
|
227
|
+
end_line: Optional[int] = None
|
|
228
|
+
project_id: str
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class SymbolNode(BaseNode):
|
|
232
|
+
"""Represents a code symbol (function, class, method)."""
|
|
233
|
+
|
|
234
|
+
fqn: str # fully qualified name
|
|
235
|
+
name: str
|
|
236
|
+
kind: str # function, class, method, variable
|
|
237
|
+
signature: Optional[str] = None
|
|
238
|
+
artifact_id: str
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class TestCaseNode(BaseNode):
|
|
242
|
+
"""Represents a test case."""
|
|
243
|
+
|
|
244
|
+
name: str
|
|
245
|
+
kind: str = "unit" # unit, integration, e2e, manual
|
|
246
|
+
path: Optional[str] = None
|
|
247
|
+
status: str = "pending" # pending, passed, failed
|
|
248
|
+
project_id: str
|
kg_mcp/main.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main entry point for the MCP-KG-Memory server.
|
|
3
|
+
Supports both STDIO and Streamable HTTP transports for Antigravity compatibility.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
# STDIO mode (for Antigravity command/args config)
|
|
7
|
+
python -m kg_mcp --transport stdio
|
|
8
|
+
|
|
9
|
+
# HTTP mode (for Antigravity serverUrl config or standalone)
|
|
10
|
+
python -m kg_mcp --transport http --host 127.0.0.1 --port 8000
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import signal
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from mcp.server.fastmcp import FastMCP
|
|
21
|
+
|
|
22
|
+
from kg_mcp.config import get_settings
|
|
23
|
+
from kg_mcp.mcp.tools import register_tools
|
|
24
|
+
from kg_mcp.mcp.resources import register_resources
|
|
25
|
+
from kg_mcp.mcp.prompts import register_prompts
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def setup_logging(transport: str) -> logging.Logger:
|
|
29
|
+
"""
|
|
30
|
+
Configure logging based on transport mode.
|
|
31
|
+
|
|
32
|
+
For STDIO: Log ONLY to stderr (stdout is reserved for MCP protocol)
|
|
33
|
+
For HTTP: Log to stdout
|
|
34
|
+
"""
|
|
35
|
+
settings = get_settings()
|
|
36
|
+
log_level = getattr(logging, settings.log_level.upper(), logging.INFO)
|
|
37
|
+
|
|
38
|
+
# Clear any existing handlers
|
|
39
|
+
root_logger = logging.getLogger()
|
|
40
|
+
root_logger.handlers.clear()
|
|
41
|
+
|
|
42
|
+
if transport == "stdio":
|
|
43
|
+
# CRITICAL: In stdio mode, stdout is for MCP protocol ONLY
|
|
44
|
+
# All logs must go to stderr
|
|
45
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
46
|
+
else:
|
|
47
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
48
|
+
|
|
49
|
+
handler.setFormatter(
|
|
50
|
+
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
51
|
+
)
|
|
52
|
+
root_logger.addHandler(handler)
|
|
53
|
+
root_logger.setLevel(log_level)
|
|
54
|
+
|
|
55
|
+
return logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def create_mcp_server(json_response: bool = True, stateless: bool = True) -> FastMCP:
|
|
59
|
+
"""Create and configure the MCP server instance."""
|
|
60
|
+
logger = logging.getLogger(__name__)
|
|
61
|
+
logger.info("Initializing MCP-KG-Memory Server...")
|
|
62
|
+
|
|
63
|
+
# Create FastMCP instance
|
|
64
|
+
mcp = FastMCP(
|
|
65
|
+
"KG Memory Server",
|
|
66
|
+
json_response=json_response,
|
|
67
|
+
stateless_http=stateless,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Register all MCP components
|
|
71
|
+
register_tools(mcp)
|
|
72
|
+
register_resources(mcp)
|
|
73
|
+
register_prompts(mcp)
|
|
74
|
+
|
|
75
|
+
logger.info("MCP server components registered successfully")
|
|
76
|
+
return mcp
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def handle_shutdown(signum, frame):
|
|
80
|
+
"""Handle graceful shutdown on SIGTERM/SIGINT."""
|
|
81
|
+
logger = logging.getLogger(__name__)
|
|
82
|
+
logger.info(f"Received signal {signum}, shutting down gracefully...")
|
|
83
|
+
sys.exit(0)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def run_stdio():
|
|
87
|
+
"""Run server in STDIO mode (for Antigravity command/args config)."""
|
|
88
|
+
logger = setup_logging("stdio")
|
|
89
|
+
logger.info("Starting MCP-KG-Memory Server in STDIO mode")
|
|
90
|
+
|
|
91
|
+
# Register signal handlers for graceful shutdown
|
|
92
|
+
signal.signal(signal.SIGTERM, handle_shutdown)
|
|
93
|
+
signal.signal(signal.SIGINT, handle_shutdown)
|
|
94
|
+
|
|
95
|
+
settings = get_settings()
|
|
96
|
+
if settings.kg_mcp_token:
|
|
97
|
+
logger.info("Token authentication configured")
|
|
98
|
+
else:
|
|
99
|
+
logger.warning("No authentication token configured")
|
|
100
|
+
|
|
101
|
+
logger.info(f"LLM Model: {settings.llm_model}")
|
|
102
|
+
logger.info(f"Neo4j URI: {settings.neo4j_uri}")
|
|
103
|
+
|
|
104
|
+
# Create and run server
|
|
105
|
+
mcp = create_mcp_server(json_response=True, stateless=True)
|
|
106
|
+
|
|
107
|
+
# Run with stdio transport
|
|
108
|
+
mcp.run(transport="stdio")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def run_http(host: str = "127.0.0.1", port: int = 8000, path: str = "/mcp"):
|
|
112
|
+
"""Run server in HTTP mode (for Antigravity serverUrl config or standalone)."""
|
|
113
|
+
logger = setup_logging("http")
|
|
114
|
+
logger.info(f"Starting MCP-KG-Memory Server in HTTP mode on {host}:{port}{path}")
|
|
115
|
+
|
|
116
|
+
# Register signal handlers
|
|
117
|
+
signal.signal(signal.SIGTERM, handle_shutdown)
|
|
118
|
+
signal.signal(signal.SIGINT, handle_shutdown)
|
|
119
|
+
|
|
120
|
+
settings = get_settings()
|
|
121
|
+
if settings.kg_mcp_token:
|
|
122
|
+
logger.info("Bearer token authentication enabled")
|
|
123
|
+
else:
|
|
124
|
+
logger.warning("⚠️ No authentication token configured! Set KG_MCP_TOKEN in .env")
|
|
125
|
+
|
|
126
|
+
logger.info(f"LLM Model: {settings.llm_model}")
|
|
127
|
+
logger.info(f"Neo4j URI: {settings.neo4j_uri}")
|
|
128
|
+
|
|
129
|
+
# Create and run server
|
|
130
|
+
mcp = create_mcp_server(json_response=True, stateless=True)
|
|
131
|
+
|
|
132
|
+
# Run with streamable-http transport
|
|
133
|
+
mcp.run(
|
|
134
|
+
transport="streamable-http",
|
|
135
|
+
host=host,
|
|
136
|
+
port=port,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main():
|
|
141
|
+
"""Main entry point with CLI argument parsing."""
|
|
142
|
+
parser = argparse.ArgumentParser(
|
|
143
|
+
description="MCP-KG-Memory Server - Knowledge Graph Memory for IDE Agents",
|
|
144
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
145
|
+
epilog="""
|
|
146
|
+
Examples:
|
|
147
|
+
# STDIO mode (for Antigravity command config)
|
|
148
|
+
python -m kg_mcp --transport stdio
|
|
149
|
+
|
|
150
|
+
# HTTP mode (for Antigravity serverUrl config)
|
|
151
|
+
python -m kg_mcp --transport http --host 127.0.0.1 --port 8000
|
|
152
|
+
|
|
153
|
+
# Using console script
|
|
154
|
+
kg-mcp --transport stdio
|
|
155
|
+
""",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"--transport",
|
|
160
|
+
"-t",
|
|
161
|
+
choices=["stdio", "http"],
|
|
162
|
+
default="http",
|
|
163
|
+
help="Transport mode: 'stdio' for command-based, 'http' for serverUrl-based (default: http)",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--host",
|
|
168
|
+
default="127.0.0.1",
|
|
169
|
+
help="Host to bind to in HTTP mode (default: 127.0.0.1)",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
parser.add_argument(
|
|
173
|
+
"--port",
|
|
174
|
+
"-p",
|
|
175
|
+
type=int,
|
|
176
|
+
default=8000,
|
|
177
|
+
help="Port to listen on in HTTP mode (default: 8000)",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"--path",
|
|
182
|
+
default="/mcp",
|
|
183
|
+
help="MCP endpoint path in HTTP mode (default: /mcp)",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
args = parser.parse_args()
|
|
187
|
+
|
|
188
|
+
if args.transport == "stdio":
|
|
189
|
+
run_stdio()
|
|
190
|
+
else:
|
|
191
|
+
run_http(host=args.host, port=args.port, path=args.path)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
main()
|
kg_mcp/mcp/__init__.py
ADDED