mcp-knowledge-graph 0.1.0__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,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-knowledge-graph
3
+ Version: 0.1.0
4
+ Summary: Neo4j knowledge graph retrieval MCP server
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: mcp>=1.6.0
7
+ Requires-Dist: neo4j>=5.0.0
8
+ Requires-Dist: pydantic-settings>=2.0
9
+ Requires-Dist: pydantic>=2.0
10
+ Requires-Dist: pyyaml>=6.0
11
+ Requires-Dist: twine>=6.2.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # Neo4j Knowledge Graph MCP Server
15
+
16
+ MCP server for enterprise knowledge graph retrieval over Neo4j. Client LLM generates Cypher; this server provides schema resources, validation, and read-only execution.
17
+
18
+ > 中文详细说明请参阅:[docs/知识图谱检索MCP服务说明.md](docs/知识图谱检索MCP服务说明.md)
19
+
20
+ ## Architecture
21
+
22
+ - **10 Resources**: 7 static (schema, constraints, conventions, semantics, glossary, patterns, safety) + 3 dynamic (stats, samples, indexes)
23
+ - **7 Tools**: `get_graph_context`, `get_cypher_prompt`, `validate_cypher`, `execute_cypher`, `explain_cypher`, `resolve_entity`, `refresh_graph_stats`
24
+ - **4 Prompts**: `generate_cypher_query`, `refine_cypher_query`, `explain_query_result`, `decompose_complex_question`
25
+
26
+ Context Tools (`get_graph_context_tool`, `get_cypher_prompt_tool`) provide the same schema context as Resources/Prompts for **Tools-only** MCP clients that do not support `resources/read` or `prompts/get`.
27
+
28
+ ## Setup
29
+
30
+ ```bash
31
+ python3 -m venv .venv
32
+ source .venv/bin/activate
33
+ pip install mcp neo4j pyyaml pydantic pydantic-settings
34
+ ```
35
+
36
+ Environment variables:
37
+
38
+ | Variable | Default | Description |
39
+ |----------|---------|-------------|
40
+ | `NEO4J_URI` | `bolt://localhost:7687` | Neo4j Bolt URI |
41
+ | `NEO4J_USER` | `neo4j` | Neo4j user |
42
+ | `NEO4J_PASSWORD` | `password` | Neo4j password |
43
+ | `NEO4J_DATABASE` | `neo4j` | Neo4j database |
44
+ | `MCP_TRANSPORT` | `stdio` | `stdio` \| `streamable-http` \| `sse` |
45
+ | `MCP_HOST` | `127.0.0.1` | HTTP/SSE bind host |
46
+ | `MCP_PORT` | `8000` | HTTP/SSE bind port |
47
+ | `MCP_SSE_PATH` | `/sse` | SSE endpoint path |
48
+ | `MCP_STREAMABLE_HTTP_PATH` | `/mcp` | Streamable HTTP endpoint path |
49
+
50
+ Copy `.env.example` to `.env` and adjust as needed.
51
+
52
+ ## Import graph data
53
+
54
+ From ontology_system:
55
+
56
+ ```bash
57
+ curl -o graph-import.cypher http://localhost:8183/graph/export/cypher
58
+ cypher-shell -u neo4j -p password -f graph-import.cypher
59
+ ```
60
+
61
+ ## Run MCP server
62
+
63
+ ### stdio(默认,Cursor 子进程模式)
64
+
65
+ ```bash
66
+ PYTHONPATH=. python -m src.server
67
+ ```
68
+
69
+ ### streamable-http(推荐 HTTP 模式)
70
+
71
+ ```bash
72
+ PYTHONPATH=. python -m src.server --transport streamable-http --host 127.0.0.1 --port 8000
73
+ ```
74
+
75
+ Endpoint: `http://127.0.0.1:8000/mcp`
76
+
77
+ ### SSE
78
+
79
+ ```bash
80
+ PYTHONPATH=. python -m src.server --transport sse --host 127.0.0.1 --port 8000
81
+ ```
82
+
83
+ Endpoint: `http://127.0.0.1:8000/sse`
84
+
85
+ ### 局域网访问(Cherry Studio 等远程客户端)
86
+
87
+ 绑定 `0.0.0.0` 时,必须配置允许的 Host 头,否则会出现 `421 Misdirected Request`:
88
+
89
+ ```bash
90
+ PYTHONPATH=. python -m src.server \
91
+ --transport streamable-http \
92
+ --host 0.0.0.0 \
93
+ --port 7688 \
94
+ --allowed-host 192.168.0.18:7688
95
+ ```
96
+
97
+ 或使用环境变量:
98
+
99
+ ```bash
100
+ export MCP_ALLOWED_HOSTS=192.168.0.18:7688
101
+ PYTHONPATH=. python -m src.server --transport streamable-http --host 0.0.0.0 --port 7688
102
+ ```
103
+
104
+ 局域网临时调试可加 `--disable-transport-security`(不推荐生产环境)。
105
+
106
+ > 绑定 `0.0.0.0` 前请确认网络访问控制;生产环境建议置于反向代理之后。
107
+
108
+ ## Cursor MCP configuration
109
+
110
+ ### stdio(command)
111
+
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "knowledge-graph": {
116
+ "command": "/path/to/mcp_knowledge_graph/.venv/bin/python",
117
+ "args": ["-m", "src.server"],
118
+ "cwd": "/path/to/mcp_knowledge_graph",
119
+ "env": {
120
+ "PYTHONPATH": "/path/to/mcp_knowledge_graph",
121
+ "NEO4J_URI": "bolt://192.168.0.18:7687",
122
+ "NEO4J_PASSWORD": "password"
123
+ }
124
+ }
125
+ }
126
+ }
127
+ ```
128
+
129
+ ### streamable-http(url,含 Cherry Studio)
130
+
131
+ 先启动服务(局域网需 `--allowed-host`):
132
+
133
+ ```bash
134
+ PYTHONPATH=. python -m src.server \
135
+ --transport streamable-http \
136
+ --host 0.0.0.0 \
137
+ --port 7688 \
138
+ --allowed-host 192.168.0.18:7688
139
+ ```
140
+
141
+ Cherry Studio JSON 配置:
142
+
143
+ ```json
144
+ {
145
+ "mcpServers": {
146
+ "knowledge-graph": {
147
+ "url": "http://192.168.0.18:7688/mcp"
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ 也可通过 args 指定 transport:
154
+
155
+ ```json
156
+ {
157
+ "mcpServers": {
158
+ "knowledge-graph": {
159
+ "command": "/path/to/mcp_knowledge_graph/.venv/bin/python",
160
+ "args": ["-m", "src.server", "--transport", "streamable-http", "--port", "8000"],
161
+ "cwd": "/path/to/mcp_knowledge_graph",
162
+ "env": {
163
+ "PYTHONPATH": "/path/to/mcp_knowledge_graph"
164
+ }
165
+ }
166
+ }
167
+ }
168
+ ```
169
+
170
+ ## Verify pipeline
171
+
172
+ ```bash
173
+ PYTHONPATH=. python scripts/verify_import_pipeline.py
174
+ ```
175
+
176
+ ## Tests
177
+
178
+ ```bash
179
+ PYTHONPATH=. pytest tests/ -v
180
+ ```
181
+
182
+ ## Workflow
183
+
184
+ ### Full MCP client (Resources + Prompts)
185
+
186
+ 1. Client reads `graph://schema`, `graph://ontology-constraints`, `graph://query-patterns`
187
+ 2. Optional: `resolve_entity` for name disambiguation
188
+ 3. Client LLM uses `generate_cypher_query` prompt to produce Cypher JSON
189
+ 4. `validate_cypher` → `execute_cypher` → `explain_query_result`
190
+
191
+ ### Tools-only client
192
+
193
+ 1. `get_cypher_prompt_tool(question=...)` or `get_graph_context_tool(scope="core")`
194
+ 2. Optional: `resolve_entity` → pass result as `entity_hints`
195
+ 3. Platform LLM generates Cypher JSON from returned prompt fields
196
+ 4. `validate_cypher` → `execute_cypher` → platform LLM explains records
@@ -0,0 +1,21 @@
1
+ src/__init__.py,sha256=1XIYRnuNBChFD1Ma2mpbzzHeI7ppUhnNHhJmuR4EYyo,40
2
+ src/cli.py,sha256=ltIh_mxqchbKZxT7GcHWGD14hgaesM2QkRKlC5f-OAM,7794
3
+ src/config.py,sha256=6-AQ53eTwLYrqlZLep8hu4cmU8cawIN-pPzF-rKlMz0,1684
4
+ src/neo4j_client.py,sha256=qu7P-3YaxEJMpHl-AZaZh7owkO3iNjQVzCDncygDmQs,8726
5
+ src/ontology.py,sha256=trO4TXCTTxaYEP-cEXnvY7HVbczEtsbozcyeK7DOPtU,1785
6
+ src/server.py,sha256=UlGWGuV0c2nkE_Sb5FeFyWPsdojFNXfzZnnrG-HFEEs,16868
7
+ src/prompts/__init__.py,sha256=2mUG1nkJoFm0uNEQiW0rht3IWBVXLnSsnmMGcpooz50,25
8
+ src/prompts/cypher_generation.py,sha256=kz74xEnoX0-PWbp8cuomDNu_TIYWgbvOG151RagOLqs,3887
9
+ src/resources/__init__.py,sha256=ijnjmQlr8w6jwP1ijqKgpk4Ol__kueYpzj9VUc3J7JE,27
10
+ src/resources/bundle.py,sha256=30epFKkvij4X8K2JvEiRgnPSOUGG3adLO9un928nqJo,3266
11
+ src/resources/dynamic.py,sha256=ms9Y9Iu7hxonFN2HuX99Kh5HLdnXih7YI-u3UiaoFN8,1063
12
+ src/resources/static.py,sha256=JUip6K3MLh9ggbtaPxaVILkUPM4ehS38BBzqry_6vQI,8451
13
+ src/tools/__init__.py,sha256=Wl4h9y5FArRxkXUa-uLOBdI_d_WMjaBxJ-V0ttBcOgw,23
14
+ src/tools/context.py,sha256=uKI1XNVL31LbAm6hZp5GnygJo5ulrpfo10eSN_hoopo,1680
15
+ src/tools/execute.py,sha256=QgUfp47Y-P6SXPrwS2pm1n4Ialx5v-O5w9Rs1ueEMgA,2522
16
+ src/tools/resolve.py,sha256=0Rqw30BaGsGj6vWc-BhEBecjYDK8ekK5RPOO_s5k0rw,1280
17
+ src/tools/validate.py,sha256=IJXpZTq8B6YctKTFFl9Zb0NFreLOphwKjmbWVTU5OgU,5410
18
+ mcp_knowledge_graph-0.1.0.dist-info/METADATA,sha256=tRjEO8lZogiA6Poj2WLwLLmKTUVe7cbGXL8VPS8XhYM,5393
19
+ mcp_knowledge_graph-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
20
+ mcp_knowledge_graph-0.1.0.dist-info/entry_points.txt,sha256=pKYU88e_nkrP1DeEmITBQO0rWGB9tDTcqLIsgKuKMGg,56
21
+ mcp_knowledge_graph-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcp-knowledge-graph = src.server:main
src/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Neo4j knowledge graph MCP server."""
src/cli.py ADDED
@@ -0,0 +1,233 @@
1
+ """CLI 参数解析与 MCP 启动配置合并。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import sys
8
+ from dataclasses import dataclass
9
+ from typing import Literal
10
+
11
+ from mcp.server.fastmcp import FastMCP
12
+ from mcp.server.transport_security import TransportSecuritySettings
13
+
14
+ from src.config import VALID_TRANSPORTS, McpSettings, TransportType, mcp_settings
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ TransportName = Literal["stdio", "sse", "streamable-http"]
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class RunConfig:
23
+ transport: TransportType
24
+ host: str
25
+ port: int
26
+ sse_path: str
27
+ streamable_http_path: str
28
+ allowed_hosts: tuple[str, ...]
29
+ allowed_origins: tuple[str, ...]
30
+ disable_transport_security: bool
31
+
32
+
33
+ def _split_csv(value: str | None) -> tuple[str, ...]:
34
+ if not value or not value.strip():
35
+ return ()
36
+ return tuple(item.strip() for item in value.split(",") if item.strip())
37
+
38
+
39
+ def _build_parser() -> argparse.ArgumentParser:
40
+ parser = argparse.ArgumentParser(
41
+ description="Neo4j 知识图谱 MCP 服务",
42
+ )
43
+ parser.add_argument(
44
+ "--transport",
45
+ choices=sorted(VALID_TRANSPORTS),
46
+ default=None,
47
+ help="MCP 传输协议:stdio(默认)、streamable-http、sse",
48
+ )
49
+ parser.add_argument(
50
+ "--host",
51
+ default=None,
52
+ help="HTTP/SSE 监听地址(默认 127.0.0.1)",
53
+ )
54
+ parser.add_argument(
55
+ "--port",
56
+ type=int,
57
+ default=None,
58
+ help="HTTP/SSE 监听端口(默认 8000)",
59
+ )
60
+ parser.add_argument(
61
+ "--sse-path",
62
+ dest="sse_path",
63
+ default=None,
64
+ help="SSE 端点路径(默认 /sse)",
65
+ )
66
+ parser.add_argument(
67
+ "--streamable-http-path",
68
+ dest="streamable_http_path",
69
+ default=None,
70
+ help="Streamable HTTP 端点路径(默认 /mcp)",
71
+ )
72
+ parser.add_argument(
73
+ "--allowed-host",
74
+ dest="allowed_hosts",
75
+ default=None,
76
+ help="允许的 Host 头,逗号分隔(如 192.168.0.18:7688 或 192.168.0.18:*)",
77
+ )
78
+ parser.add_argument(
79
+ "--allowed-origin",
80
+ dest="allowed_origins",
81
+ default=None,
82
+ help="允许的 Origin 头,逗号分隔(如 http://192.168.0.18:*)",
83
+ )
84
+ parser.add_argument(
85
+ "--disable-transport-security",
86
+ action="store_true",
87
+ default=None,
88
+ help="关闭 DNS rebinding 校验(局域网调试,不推荐生产)",
89
+ )
90
+ return parser
91
+
92
+
93
+ def parse_cli_args(argv: list[str] | None = None) -> argparse.Namespace:
94
+ return _build_parser().parse_args(argv)
95
+
96
+
97
+ def validate_transport(transport: str) -> TransportType:
98
+ if transport not in VALID_TRANSPORTS:
99
+ valid = ", ".join(sorted(VALID_TRANSPORTS))
100
+ raise ValueError(f"Unknown transport '{transport}'. Valid transports: {valid}")
101
+ return transport # type: ignore[return-value]
102
+
103
+
104
+ def build_run_config(
105
+ args: argparse.Namespace,
106
+ env: McpSettings | None = None,
107
+ ) -> RunConfig:
108
+ """合并配置:CLI 显式参数 > 环境变量 > 默认值。"""
109
+ env = env or mcp_settings
110
+ transport = validate_transport(args.transport if args.transport is not None else env.transport)
111
+ disable_security = (
112
+ args.disable_transport_security
113
+ if args.disable_transport_security is not None
114
+ else env.disable_transport_security
115
+ )
116
+ allowed_hosts = _split_csv(
117
+ args.allowed_hosts if args.allowed_hosts is not None else env.allowed_hosts
118
+ )
119
+ allowed_origins = _split_csv(
120
+ args.allowed_origins if args.allowed_origins is not None else env.allowed_origins
121
+ )
122
+ return RunConfig(
123
+ transport=transport,
124
+ host=args.host if args.host is not None else env.host,
125
+ port=args.port if args.port is not None else env.port,
126
+ sse_path=args.sse_path if args.sse_path is not None else env.sse_path,
127
+ streamable_http_path=(
128
+ args.streamable_http_path
129
+ if args.streamable_http_path is not None
130
+ else env.streamable_http_path
131
+ ),
132
+ allowed_hosts=allowed_hosts,
133
+ allowed_origins=allowed_origins,
134
+ disable_transport_security=disable_security,
135
+ )
136
+
137
+
138
+ def _merge_allowed_hosts(hosts: tuple[str, ...], port: int) -> list[str]:
139
+ """合并用户配置与本地回环地址,供健康检查与容器内访问。"""
140
+ merged = list(hosts)
141
+ for item in (
142
+ f"127.0.0.1:{port}",
143
+ f"localhost:{port}",
144
+ "127.0.0.1:*",
145
+ "localhost:*",
146
+ "[::1]:*",
147
+ ):
148
+ if item not in merged:
149
+ merged.append(item)
150
+ return merged
151
+
152
+
153
+ def _merge_allowed_origins(origins: tuple[str, ...], hosts: list[str]) -> list[str]:
154
+ merged = list(origins)
155
+ for host in hosts:
156
+ if host.startswith("http"):
157
+ candidate = host
158
+ else:
159
+ candidate = f"http://{host}"
160
+ if candidate not in merged:
161
+ merged.append(candidate)
162
+ for item in ("http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"):
163
+ if item not in merged:
164
+ merged.append(item)
165
+ return merged
166
+
167
+
168
+ def build_transport_security(config: RunConfig) -> TransportSecuritySettings:
169
+ """按监听地址与 allowed_hosts 构建传输安全策略。"""
170
+ if config.disable_transport_security:
171
+ return TransportSecuritySettings(enable_dns_rebinding_protection=False)
172
+
173
+ if config.allowed_hosts:
174
+ allowed_hosts = _merge_allowed_hosts(config.allowed_hosts, config.port)
175
+ origins = _merge_allowed_origins(config.allowed_origins, allowed_hosts)
176
+ return TransportSecuritySettings(
177
+ enable_dns_rebinding_protection=True,
178
+ allowed_hosts=allowed_hosts,
179
+ allowed_origins=origins,
180
+ )
181
+
182
+ if config.host in ("127.0.0.1", "localhost", "::1"):
183
+ return TransportSecuritySettings(
184
+ enable_dns_rebinding_protection=True,
185
+ allowed_hosts=[
186
+ f"{config.host}:*",
187
+ "127.0.0.1:*",
188
+ "localhost:*",
189
+ "[::1]:*",
190
+ ],
191
+ allowed_origins=[
192
+ f"http://{config.host}:*",
193
+ "http://127.0.0.1:*",
194
+ "http://localhost:*",
195
+ "http://[::1]:*",
196
+ ],
197
+ )
198
+
199
+ # 绑定 0.0.0.0 等对外地址时,必须显式配置 allowed_hosts
200
+ raise ValueError(
201
+ f"绑定到 {config.host}:{config.port} 时需配置允许的 Host 头,例如:\n"
202
+ f" --allowed-host 192.168.0.18:{config.port}\n"
203
+ f" --allowed-host 192.168.0.18:*\n"
204
+ f"或设置环境变量 MCP_ALLOWED_HOSTS=192.168.0.18:{config.port}\n"
205
+ f"局域网临时调试可加 --disable-transport-security(不推荐生产环境)"
206
+ )
207
+
208
+
209
+ def apply_run_config(server: FastMCP, config: RunConfig) -> None:
210
+ """将网络相关设置写入 FastMCP 实例(启动前调用)。"""
211
+ server.settings.host = config.host
212
+ server.settings.port = config.port
213
+ server.settings.sse_path = config.sse_path
214
+ server.settings.streamable_http_path = config.streamable_http_path
215
+ if config.transport in ("streamable-http", "sse"):
216
+ server.settings.transport_security = build_transport_security(config)
217
+
218
+
219
+ def run_server(server: FastMCP, config: RunConfig) -> None:
220
+ """按配置启动 MCP 服务。"""
221
+ apply_run_config(server, config)
222
+ transport: TransportName = config.transport # type: ignore[assignment]
223
+ server.run(transport=transport)
224
+
225
+
226
+ def main_entry(server: FastMCP, argv: list[str] | None = None) -> None:
227
+ try:
228
+ args = parse_cli_args(argv)
229
+ config = build_run_config(args)
230
+ run_server(server, config)
231
+ except ValueError as exc:
232
+ print(str(exc), file=sys.stderr)
233
+ raise SystemExit(2) from exc
src/config.py ADDED
@@ -0,0 +1,63 @@
1
+ """MCP server configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Literal
7
+
8
+ from pydantic_settings import BaseSettings, SettingsConfigDict
9
+
10
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
11
+ DATA_DIR = PROJECT_ROOT / "data"
12
+
13
+ TransportType = Literal["stdio", "streamable-http", "sse"]
14
+ VALID_TRANSPORTS: frozenset[str] = frozenset({"stdio", "streamable-http", "sse"})
15
+
16
+
17
+ class Settings(BaseSettings):
18
+ model_config = SettingsConfigDict(
19
+ env_prefix="NEO4J_",
20
+ env_file=".env",
21
+ env_file_encoding="utf-8",
22
+ extra="ignore",
23
+ )
24
+
25
+ uri: str = "bolt://192.168.0.18:7687"
26
+ user: str = "neo4j"
27
+ password: str = "password"
28
+ database: str = "neo4j"
29
+
30
+ ontology_schema_path: Path = DATA_DIR / "ontology_schema.json"
31
+ query_patterns_path: Path = DATA_DIR / "query_patterns.yaml"
32
+
33
+ default_limit: int = 50
34
+ max_limit: int = 500
35
+ query_timeout_seconds: int = 30
36
+ max_traversal_depth: int = 4
37
+
38
+ stats_refresh_interval_seconds: int = 3600
39
+
40
+
41
+ class McpSettings(BaseSettings):
42
+ """MCP 传输协议与 HTTP/SSE 监听配置(环境变量前缀 MCP_)。"""
43
+
44
+ model_config = SettingsConfigDict(
45
+ env_prefix="MCP_",
46
+ env_file=".env",
47
+ env_file_encoding="utf-8",
48
+ extra="ignore",
49
+ )
50
+
51
+ transport: TransportType = "stdio"
52
+ host: str = "127.0.0.1"
53
+ port: int = 8000
54
+ sse_path: str = "/sse"
55
+ streamable_http_path: str = "/mcp"
56
+ # 逗号分隔,如 192.168.0.18:7688 或 192.168.0.18:*
57
+ allowed_hosts: str = ""
58
+ allowed_origins: str = ""
59
+ disable_transport_security: bool = False
60
+
61
+
62
+ settings = Settings()
63
+ mcp_settings = McpSettings()