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.
- mcp_knowledge_graph-0.1.0.dist-info/METADATA +196 -0
- mcp_knowledge_graph-0.1.0.dist-info/RECORD +21 -0
- mcp_knowledge_graph-0.1.0.dist-info/WHEEL +4 -0
- mcp_knowledge_graph-0.1.0.dist-info/entry_points.txt +2 -0
- src/__init__.py +1 -0
- src/cli.py +233 -0
- src/config.py +63 -0
- src/neo4j_client.py +229 -0
- src/ontology.py +58 -0
- src/prompts/__init__.py +1 -0
- src/prompts/cypher_generation.py +115 -0
- src/resources/__init__.py +1 -0
- src/resources/bundle.py +92 -0
- src/resources/dynamic.py +37 -0
- src/resources/static.py +194 -0
- src/server.py +483 -0
- src/tools/__init__.py +1 -0
- src/tools/context.py +56 -0
- src/tools/execute.py +89 -0
- src/tools/resolve.py +45 -0
- src/tools/validate.py +153 -0
|
@@ -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,,
|
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()
|