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,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Origin validation middleware for MCP server.
|
|
3
|
+
Prevents DNS rebinding attacks by validating Origin headers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import fnmatch
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Callable, List, Optional
|
|
9
|
+
|
|
10
|
+
from kg_mcp.config import get_settings
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OriginValidationError(Exception):
|
|
16
|
+
"""Raised when origin validation fails."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_origin(origin: Optional[str], allowed_origins: List[str]) -> bool:
|
|
22
|
+
"""
|
|
23
|
+
Validate if an origin is allowed.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
origin: The Origin header value
|
|
27
|
+
allowed_origins: List of allowed origin patterns (supports wildcards)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True if the origin is allowed
|
|
31
|
+
"""
|
|
32
|
+
if not origin:
|
|
33
|
+
# No origin header - might be a same-origin request or non-browser client
|
|
34
|
+
# For MCP servers, we typically allow this
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
if not allowed_origins:
|
|
38
|
+
# No allowlist configured - only allow localhost by default
|
|
39
|
+
allowed_origins = ["http://localhost:*", "http://127.0.0.1:*"]
|
|
40
|
+
|
|
41
|
+
for pattern in allowed_origins:
|
|
42
|
+
if fnmatch.fnmatch(origin, pattern):
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_origin_middleware() -> Callable:
|
|
49
|
+
"""
|
|
50
|
+
Create an origin validation middleware function.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A middleware function that validates Origin headers
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
async def origin_middleware(request, call_next):
|
|
57
|
+
"""Middleware to validate Origin headers."""
|
|
58
|
+
settings = get_settings()
|
|
59
|
+
allowed_origins = settings.allowed_origins_list
|
|
60
|
+
|
|
61
|
+
origin = request.headers.get("origin")
|
|
62
|
+
|
|
63
|
+
if not validate_origin(origin, allowed_origins):
|
|
64
|
+
logger.warning(
|
|
65
|
+
f"Rejected request with disallowed origin: {origin} "
|
|
66
|
+
f"(allowed: {allowed_origins})"
|
|
67
|
+
)
|
|
68
|
+
from starlette.responses import JSONResponse
|
|
69
|
+
|
|
70
|
+
return JSONResponse(
|
|
71
|
+
status_code=403,
|
|
72
|
+
content={"error": f"Origin '{origin}' is not allowed"},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Add CORS headers for allowed origins
|
|
76
|
+
response = await call_next(request)
|
|
77
|
+
|
|
78
|
+
if origin and validate_origin(origin, allowed_origins):
|
|
79
|
+
response.headers["Access-Control-Allow-Origin"] = origin
|
|
80
|
+
response.headers["Access-Control-Allow-Credentials"] = "true"
|
|
81
|
+
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
|
|
82
|
+
response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type"
|
|
83
|
+
|
|
84
|
+
return response
|
|
85
|
+
|
|
86
|
+
return origin_middleware
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def is_localhost(host: str) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
Check if a host is localhost.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
host: The host to check
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if localhost
|
|
98
|
+
"""
|
|
99
|
+
localhost_patterns = [
|
|
100
|
+
"localhost",
|
|
101
|
+
"127.0.0.1",
|
|
102
|
+
"::1",
|
|
103
|
+
"[::1]",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
# Strip port if present
|
|
107
|
+
if ":" in host and not host.startswith("["):
|
|
108
|
+
host = host.split(":")[0]
|
|
109
|
+
elif host.startswith("[") and "]:" in host:
|
|
110
|
+
host = host.split("]:")[0] + "]"
|
|
111
|
+
|
|
112
|
+
return host.lower() in localhost_patterns
|
kg_mcp/utils.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for the KG-MCP server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, date, time
|
|
7
|
+
from typing import Any, Dict, List, Union
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def serialize_neo4j_value(value: Any) -> Any:
|
|
11
|
+
"""
|
|
12
|
+
Recursively serialize Neo4j values to JSON-compatible types.
|
|
13
|
+
|
|
14
|
+
Handles:
|
|
15
|
+
- neo4j.time.DateTime -> ISO string
|
|
16
|
+
- neo4j.time.Date -> ISO string
|
|
17
|
+
- neo4j.time.Time -> ISO string
|
|
18
|
+
- neo4j.time.Duration -> dict
|
|
19
|
+
- neo4j.spatial.Point -> dict
|
|
20
|
+
- nested dicts and lists
|
|
21
|
+
"""
|
|
22
|
+
if value is None:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
# Handle Neo4j DateTime types
|
|
26
|
+
type_name = type(value).__name__
|
|
27
|
+
module_name = type(value).__module__
|
|
28
|
+
|
|
29
|
+
if module_name.startswith('neo4j'):
|
|
30
|
+
# Neo4j DateTime
|
|
31
|
+
if type_name == 'DateTime':
|
|
32
|
+
return value.isoformat()
|
|
33
|
+
# Neo4j Date
|
|
34
|
+
elif type_name == 'Date':
|
|
35
|
+
return value.isoformat()
|
|
36
|
+
# Neo4j Time
|
|
37
|
+
elif type_name == 'Time':
|
|
38
|
+
return value.isoformat()
|
|
39
|
+
# Neo4j Duration
|
|
40
|
+
elif type_name == 'Duration':
|
|
41
|
+
return {
|
|
42
|
+
'months': value.months,
|
|
43
|
+
'days': value.days,
|
|
44
|
+
'seconds': value.seconds,
|
|
45
|
+
'nanoseconds': value.nanoseconds,
|
|
46
|
+
}
|
|
47
|
+
# Neo4j Point
|
|
48
|
+
elif type_name == 'Point':
|
|
49
|
+
return {
|
|
50
|
+
'srid': value.srid,
|
|
51
|
+
'x': value.x,
|
|
52
|
+
'y': value.y,
|
|
53
|
+
'z': getattr(value, 'z', None),
|
|
54
|
+
}
|
|
55
|
+
# Other Neo4j types - convert to string
|
|
56
|
+
else:
|
|
57
|
+
return str(value)
|
|
58
|
+
|
|
59
|
+
# Handle Python datetime types
|
|
60
|
+
if isinstance(value, datetime):
|
|
61
|
+
return value.isoformat()
|
|
62
|
+
if isinstance(value, date):
|
|
63
|
+
return value.isoformat()
|
|
64
|
+
if isinstance(value, time):
|
|
65
|
+
return value.isoformat()
|
|
66
|
+
|
|
67
|
+
# Handle dict - recursively serialize
|
|
68
|
+
if isinstance(value, dict):
|
|
69
|
+
return {k: serialize_neo4j_value(v) for k, v in value.items()}
|
|
70
|
+
|
|
71
|
+
# Handle list - recursively serialize
|
|
72
|
+
if isinstance(value, (list, tuple)):
|
|
73
|
+
return [serialize_neo4j_value(v) for v in value]
|
|
74
|
+
|
|
75
|
+
# Handle sets
|
|
76
|
+
if isinstance(value, set):
|
|
77
|
+
return [serialize_neo4j_value(v) for v in value]
|
|
78
|
+
|
|
79
|
+
# Handle bytes
|
|
80
|
+
if isinstance(value, bytes):
|
|
81
|
+
return value.decode('utf-8', errors='replace')
|
|
82
|
+
|
|
83
|
+
# Return primitives as-is
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def serialize_response(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
88
|
+
"""
|
|
89
|
+
Serialize a complete response dictionary for JSON output.
|
|
90
|
+
|
|
91
|
+
Use this to wrap tool responses before returning.
|
|
92
|
+
"""
|
|
93
|
+
return serialize_neo4j_value(data)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Neo4jJSONEncoder(json.JSONEncoder):
|
|
97
|
+
"""Custom JSON encoder that handles Neo4j types."""
|
|
98
|
+
|
|
99
|
+
def default(self, obj):
|
|
100
|
+
return serialize_neo4j_value(obj)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kg-mcp
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Summary: Memory/Knowledge Graph MCP Server for IDE Assistants - Persistent context and knowledge for AI coding agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/Hexecu/mcp-neuralmemory
|
|
6
|
+
Project-URL: Documentation, https://github.com/Hexecu/mcp-neuralmemory#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/Hexecu/mcp-neuralmemory
|
|
8
|
+
Project-URL: Issues, https://github.com/Hexecu/mcp-neuralmemory/issues
|
|
9
|
+
Author: Davide Leopardi
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: ai-assistant,gemini,knowledge-graph,llm,mcp,model-context-protocol,neo4j
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: google-auth>=2.0.0
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: litellm>=1.40.0
|
|
23
|
+
Requires-Dist: mcp>=1.0.0
|
|
24
|
+
Requires-Dist: neo4j>=5.0.0
|
|
25
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
26
|
+
Requires-Dist: pydantic>=2.0.0
|
|
27
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
28
|
+
Requires-Dist: rich>=13.0.0
|
|
29
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: black>=24.0.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# MCP-KG-Memory Server
|
|
40
|
+
|
|
41
|
+
Python MCP server implementation with Neo4j backend.
|
|
42
|
+
|
|
43
|
+
## Development Setup
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Create virtual environment
|
|
47
|
+
python -m venv .venv
|
|
48
|
+
source .venv/bin/activate
|
|
49
|
+
|
|
50
|
+
# Install dependencies (including dev)
|
|
51
|
+
pip install -e ".[dev]"
|
|
52
|
+
|
|
53
|
+
# Run tests
|
|
54
|
+
pytest tests/ -v
|
|
55
|
+
|
|
56
|
+
# Run server
|
|
57
|
+
python -m kg_mcp.main
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Project Structure
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
src/kg_mcp/
|
|
64
|
+
├── main.py # Entry point
|
|
65
|
+
├── config.py # Settings management
|
|
66
|
+
├── llm/ # LLM integration
|
|
67
|
+
│ ├── client.py # LiteLLM wrapper
|
|
68
|
+
│ ├── schemas.py # Pydantic models
|
|
69
|
+
│ └── prompts/ # Prompt templates
|
|
70
|
+
├── kg/ # Knowledge graph
|
|
71
|
+
│ ├── neo4j.py # Driver/client
|
|
72
|
+
│ ├── schema.cypher # DB schema
|
|
73
|
+
│ ├── repo.py # Query repository
|
|
74
|
+
│ ├── ingest.py # Ingestion pipeline
|
|
75
|
+
│ └── retrieval.py # Context builder
|
|
76
|
+
├── mcp/ # MCP components
|
|
77
|
+
│ ├── tools.py # Tool definitions
|
|
78
|
+
│ ├── resources.py # Resource handlers
|
|
79
|
+
│ └── prompts.py # Prompt templates
|
|
80
|
+
├── codegraph/ # Code indexing (V1)
|
|
81
|
+
│ ├── model.py # Data models
|
|
82
|
+
│ └── indexer.py # File indexer
|
|
83
|
+
└── security/ # Auth/Origin
|
|
84
|
+
├── auth.py # Token validation
|
|
85
|
+
└── origin.py # Origin checking
|
|
86
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
kg_mcp/__init__.py,sha256=f35x5yllOVTBCMOsJcgesmoze7bM5UTiOlTSMZU61-U,80
|
|
2
|
+
kg_mcp/__main__.py,sha256=3djAWBOOTYfXvFPnAfwe2UWkKkYHBUVHlPJjaMXNWrY,135
|
|
3
|
+
kg_mcp/config.py,sha256=9ryFQLG28B_v-PGv-D_2ojm5oU2EhNFxbq4qAv_hOyM,3479
|
|
4
|
+
kg_mcp/main.py,sha256=oprRN_yyZ5QIHOivYr923xl-_BWzizKvXEAOgtin-hg,5614
|
|
5
|
+
kg_mcp/utils.py,sha256=_4DlV2PtftJjR5Ak4lFzxdNuIrbyczIVenWhqKkX5zk,2810
|
|
6
|
+
kg_mcp/cli/__init__.py,sha256=-i85AHO1gqjeqEYqIkSjfW9Ik0LWbhrTPuhUCIk7eD4,60
|
|
7
|
+
kg_mcp/cli/setup.py,sha256=1d4jmR6Tuaf9kGNQWOcmrCoIMqeH3rMYZGDdBLdH39s,44376
|
|
8
|
+
kg_mcp/cli/status.py,sha256=0-_CiISA5maBgliQXl6S7B5WfpEc8nUuQI6S1QE_mLk,12651
|
|
9
|
+
kg_mcp/codegraph/__init__.py,sha256=Erh3mMg5FlbN0kzMJAzCpizcrjrNX0LuZHawKOECskE,68
|
|
10
|
+
kg_mcp/codegraph/indexer.py,sha256=H-QTMmrLsgzL4jXzhgLUagFGUyDfkm9cX-B-9ip_7TA,9130
|
|
11
|
+
kg_mcp/codegraph/model.py,sha256=qupXdrkHC4FQZGYE0Haw93AWdDWPsyuTND2jlMOFjWM,3853
|
|
12
|
+
kg_mcp/kg/__init__.py,sha256=2lueX_bq3H6sjh5DgRzSVWOAuzftzq7vz8NlCLsY1BE,56
|
|
13
|
+
kg_mcp/kg/apply_schema.py,sha256=iJNiLmSqmhOXVeS5bgNO_tSFIZi38fZfhSchmDtc_Gs,2715
|
|
14
|
+
kg_mcp/kg/ingest.py,sha256=KdLUlweATYiwBVOuyxMjGmLh04fXUky9QSO1fkgtTr8,9239
|
|
15
|
+
kg_mcp/kg/neo4j.py,sha256=ZvD45aChmuFH3Ns7i8OmTz0mrnmbscvIaZlTRa_-7hw,4761
|
|
16
|
+
kg_mcp/kg/repo.py,sha256=ASuKwsBLTel3toshScsNFC0F-0_8wvkV2PJ9p7s_dac,27164
|
|
17
|
+
kg_mcp/kg/retrieval.py,sha256=oFxVvGFCCC2jto6EBDMcXenkJq_LIln_5P9q3Lk6X-Y,9032
|
|
18
|
+
kg_mcp/kg/schema.cypher,sha256=r6BMg-fKhJxHREqaeHnxp2X3MknbTY8vJ96e2UUVPn4,5887
|
|
19
|
+
kg_mcp/llm/__init__.py,sha256=ewfvU-0Mxw8P2s774DsI8t9PkFNh78WeO58FqwsTOwY,88
|
|
20
|
+
kg_mcp/llm/client.py,sha256=OEwig8uE_MZeUvsZPUKZUEvZFVetA5mPIffbx42PUQM,10535
|
|
21
|
+
kg_mcp/llm/schemas.py,sha256=F4LQM7a5PMBoL3LQEP80LlPgcB7bfHoDV3nK2mbBJbw,8554
|
|
22
|
+
kg_mcp/llm/prompts/__init__.py,sha256=rMw7FCXH-pK_X759NlDpghKLvIt5hGk6p_1bOIw8klg,221
|
|
23
|
+
kg_mcp/llm/prompts/extractor.py,sha256=3gBq0f3YLKtEHwjOLjMxQYDu_yUi1SEoleiYiZ_79Uc,3692
|
|
24
|
+
kg_mcp/llm/prompts/linker.py,sha256=Cv0sR2hFbOf58_C8bEbVOmZPDV8vOVUBwa01dU5mpiU,3931
|
|
25
|
+
kg_mcp/mcp/__init__.py,sha256=HIdA5ozqK0A6JDcQpItt4M73_nk52PkdqYAorlgaiUE,57
|
|
26
|
+
kg_mcp/mcp/change_schemas.py,sha256=mWso9c26556O-8VaFfHLc1lvibouyT6IxRSVLTuzUE8,4891
|
|
27
|
+
kg_mcp/mcp/prompts.py,sha256=qbRwyM834gKFxwJlcsXH9eSdCrGXVmlyPDaJPb6VR6c,5546
|
|
28
|
+
kg_mcp/mcp/resources.py,sha256=H7hVC8bMqYUKB7e8T-kaSVoVI4JlA5nKhmlQuxC3j2w,8502
|
|
29
|
+
kg_mcp/mcp/tools.py,sha256=8CpBsO-_y7rW6wRb00NQJ73ENqgj7rUwzi76QGmKqZA,20172
|
|
30
|
+
kg_mcp/security/__init__.py,sha256=zvxT3XvZQLqMaj1IiKOYXlQDmQrolXpEErn8-LSe-a8,65
|
|
31
|
+
kg_mcp/security/auth.py,sha256=4GLUguNnlvwA8G1cAjbQlMvndyJYxecEv1REQFnhk4s,3218
|
|
32
|
+
kg_mcp/security/origin.py,sha256=fRm-w_URkT7sF0D8aRnt3xXXXkfYREBAydHKdrTrbtg,3068
|
|
33
|
+
kg_mcp-0.1.8.dist-info/METADATA,sha256=Wkbq5TbXdVmHxZn81I0FfK3ddpxminN0RMMxZAY3E7o,3026
|
|
34
|
+
kg_mcp-0.1.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
35
|
+
kg_mcp-0.1.8.dist-info/entry_points.txt,sha256=rWKJ-LdGRIRnTAALNI0ZJ7H1Nclnn8C76OLwt3QjUiE,120
|
|
36
|
+
kg_mcp-0.1.8.dist-info/RECORD,,
|