09. MCP 연동 — Neo4j MCP 서버를 Bolt 게이트웨이에 붙이기
이 문서가 답하는 질문
- 전용 MCP 서버가 없는데 어떻게 MCP 클라이언트를 붙이는가?
- 정확한 설정 파일과 실행 절차는? (복사-붙여넣기)
--database는 무엇을 가리키는가? 인증은 어떻게 되는가?- 무엇이 동작하고 무엇이 동작하지 않는가?
1. 사실 — 전용 MCP 서버는 없다. 대신 Neo4j의 것이 그대로 붙는다
spec 008 FR-032는 MCP 호환 서버를 요구하지만, 전용 바이너리는 미구현이다 (T020 미체크, specs/008-agent-native-interface/tasks.md:37; docs/agents.md:158-163 도 "not built yet"이라고 명시).
대신 성립하는 경로는 이것이다:
MCP 클라이언트 ──stdio──▶ mcp-neo4j-cypher (PyPI, 수정 없음)
│ Bolt 4.4
▼
ontological-bolt (spec 011)
│ postgres wire
▼
PostgreSQL + ontological
examples/meeting-rooms/ 전체가 이 경로를 테스트로 검증한다 (examples/meeting-rooms/README.md:10-12). verify_mcp.py 는 11개 검사 매트릭스를 출력하고, README는 11/11 통과를 보고한다 (README.md:73).
2. 사실 — 사전 조건
| 필요한 것 | 확인 방법 | 근거 |
|---|---|---|
PostgreSQL + ontological 확장 |
SELECT ontological_version(); |
— |
| Bolt 게이트웨이 실행 중 | ss -ltn | grep 7687 |
bolt/src/main.rs:36-57 |
| Python ≥ 3.10 | python3 --version |
examples/meeting-rooms/README.md:66 |
mcp-neo4j-cypher |
pip install mcp-neo4j-cypher |
같은 파일 66행 |
| (선택) 임베딩 모델 | Ollama 등 | 같은 파일 61-63행 |
2.1 Bolt 게이트웨이 실행
환경변수 전용이다 (bolt/src/main.rs:9-15, 37-44):
| 변수 | 기본값 |
|---|---|
OG_BOLT_LISTEN |
0.0.0.0:7687 |
OG_BOLT_PGHOST |
localhost |
OG_BOLT_PGPORT |
5432 |
OG_BOLT_PGDATABASE |
og |
OG_BOLT_GRAPH |
default — 세션이 database를 지정하지 않았을 때 쓰는 그래프 |
OG_BOLT_ADVERTISED |
OG_BOLT_LISTEN 과 동일 |
OG_BOLT_PGHOST=localhost \
OG_BOLT_PGPORT=5432 \
OG_BOLT_PGDATABASE=ogstudio \
OG_BOLT_GRAPH=meeting \
./bolt/target/release/ontological-bolt
start.sh 가 자동으로 띄우는 구성도 있다 (start.sh:54-65) — 호스트 포트는 OG_BOLTPORT(기본 28687)이고 컨테이너 내부 7687로 매핑된다(start.sh:9, 27). OG_BOLT=0 이면 게이트웨이를 띄우지 않는다(start.sh:56).
2.2 인증은 PostgreSQL의 것이다
// bolt/src/session.rs:165-184
// Authentication is PostgreSQL's: the credentials in HELLO are the role's
// credentials, and failing to connect is what "unauthorized" means here.
let user = extra["principal"];
let password = extra["credentials"];
cfg.host(...).port(...).dbname(...).user(&user).application_name("ontological-bolt");
MCP 설정의 --username / --password 는 PostgreSQL 역할의 자격증명이다. 별도 사용자 저장소가 없다(spec 011 FR-015). 연결 실패는 Neo.ClientError.Security.Unauthorized 로 번역된다 (session.rs:182-184).
2.3 --database 는 그래프 이름이다
// bolt/src/session.rs:221-238
fn select_graph(&mut self, extra: &Value) {
if let Some(db) = extra["db"] {
// "neo4j"와 "system"은 드라이버가 "지정 안 함"을 뜻할 때 보내는 값
if !db.is_empty() && db != "neo4j" && db != "system" { self.graph = db; return; }
}
if self.in_tx { return; } // 트랜잭션 안에서는 BEGIN 때의 그래프 유지
self.graph = self.config.default_graph.clone();
}
--database meeting→ 그래프meeting.neo4j/system은 "기본 그래프"로 해석된다 →OG_BOLT_GRAPH값.- PostgreSQL 데이터베이스가 아니다. 그것은
OG_BOLT_PGDATABASE로 고정된다.
3. 절차 (복사-붙여넣기)
3.1 온톨로지 선언 (SQL — 이 단계만 SQL이다)
이유: 타입·프로퍼티 타입·role(관계 양 끝의 이름 붙은 타입 제약)은 Neo4j에 대응 문법이 없다(examples/meeting-rooms/README.md:81-93).
psql -d ogstudio -f examples/meeting-rooms/schema.sql
내용 요약 (examples/meeting-rooms/schema.sql):
SELECT og_create_graph('meeting');
SELECT og_create_type('meeting', 'MeetingRoom', 'entity');
SELECT og_add_property('meeting', 'MeetingRoom', 'name', 'string', true, true);
-- …
SELECT og_create_type('meeting', 'FOR_ROOM', 'relation');
SELECT og_add_role('meeting', 'FOR_ROOM', 'reservation', 'Reservation', 0); -- ordinal 0 = source
SELECT og_add_role('meeting', 'FOR_ROOM', 'room', 'MeetingRoom', 1); -- ordinal 1 = target
role의 ordinal 이 og_schema 의 position (source/target)으로 노출되고 (engine/src/agent/mod.rs:170-174), apoc.meta.schema() 의 관계 방향도 여기서 나온다 (examples/meeting-rooms/README.md:89-93).
3.2 MCP 클라이언트 설정 파일
examples/meeting-rooms/mcp.json 을 그대로 쓴다. Claude Code면 프로젝트 루트에 .mcp.json 으로, Claude Desktop이면 claude_desktop_config.json 의 mcpServers 에 병합한다.
{
"mcpServers": {
"ontological": {
"command": "uvx",
"args": [
"mcp-neo4j-cypher",
"--db-url", "bolt://localhost:7687",
"--username", "dev",
"--password", "dev",
"--database", "meeting",
"--schema-sample-size", "1000"
]
}
}
}
--schema-sample-size 는 선택이 아니다. mcp-neo4j-cypher 의 help는 기본값 1000이라고 하지만 argparse 기본값이 None 이고, None 이 그대로 apoc.meta.schema({sample: None}) 로 도달해 None 이 변수 이름으로 파싱된다. Neo4j 상대로도 똑같이 실패하는 업스트림 버그이며, 플래그를 넘기는 것이 두 DB 모두에서의 회피책이다 (examples/meeting-rooms/mcp.json:14-17, README.md:75-79, og_mcp.py:41-47).
uvx 대신 pip install 한 실행 파일을 쓰려면 "command": "mcp-neo4j-cypher" 로 바꾸고 args 에서 첫 항목을 제거한다 (og_mcp.py:34-50 가 그 형태다).
3.3 데이터 적재와 검증
pip install mcp-neo4j-cypher # 진짜 Neo4j 서버, 수정 없음
psql -d ogstudio -f examples/meeting-rooms/schema.sql
OG_GRAPH=meeting python3 examples/meeting-rooms/load.py # 모든 쓰기를 MCP 서버로
OG_GRAPH=meeting python3 examples/meeting-rooms/verify_mcp.py # 호환성 매트릭스
OG_GRAPH=meeting python3 examples/meeting-rooms/scenario.py # 질문 종단 실행
og_mcp.py 가 읽는 환경변수 (examples/meeting-rooms/og_mcp.py:21-28):
| 변수 | 기본값 |
|---|---|
OG_BOLT_URI |
bolt://localhost:7687 |
OG_BOLT_USER |
dev |
OG_BOLT_PASSWORD |
dev |
OG_GRAPH |
meeting |
OG_EMBED_URL |
http://localhost:11434/api/embed |
OG_EMBED_MODEL |
qwen3-embedding:latest |
OG_EMBED_DIMS |
1024 |
3.4 벡터 인덱스는 Neo4j DDL로 만든다
CREATE VECTOR INDEX room_name IF NOT EXISTS
FOR (m:MeetingRoom) ON (m.name_vec)
OPTIONS {indexConfig: {
`vector.dimensions`: 1024,
`vector.similarity_function`: 'cosine'
}}
write_neo4j_cypher 로 보내면 된다 (examples/meeting-rooms/load.py:48-65). 내부에서는 og_add_embedding(graph, label, prop, dims, metric) 로 변환된다 (engine/src/compat/ddl.rs:211-230).
⚠️ 이 경로는 source_prop 를 넘기지 않는다 → 이 슬롯은 og_stale_embeddings 에서 영원히 제외된다. 상세는 05_embedding_pipeline.md 2.3절.
3.5 DB 안에서 임베딩하기 (선택)
이걸 켜야 에이전트가 한 문장으로 시맨틱 조회를 할 수 있다. mcp-neo4j-cypher 가 노출하는 도구 3개 중에 임베딩 도구가 없기 때문이다 (examples/meeting-rooms/README.md:95-104).
SELECT og_set_setting('genai.enabled', 'on');
SELECT og_set_setting('genai.endpoint', 'http://localhost:11434/api/embed');
SELECT og_set_setting('genai.provider', 'ollama'); -- 또는 OpenAI 호환
SELECT og_set_setting('genai.model', 'qwen3-embedding:latest');
SELECT og_set_setting('genai.dimensions', '1024');
그러면 이 한 문장이 성립한다:
CALL db.index.vector.queryNodes('room_name', 3, genai.vector.encode($text))
YIELD node, score
RETURN node.name AS canonical, score
scenario.py 는 이 형태를 먼저 시도하고, DB가 거절하면 클라이언트 임베딩으로 폴백하며 어느 경로를 탔는지 출력한다 (examples/meeting-rooms/scenario.py:98-106).
보안 주의: 엔드포인트는 의도적으로 인자가 아니라 설정이다 — Cypher를 쓸 수 있는 호출자가 서버로 하여금 임의 URL을 가져오게 만들 수 없다 (engine/src/compat/genai.rs:21-24). 다만 평문 SQL로 og_set_setting 을 호출할 수 있으면 이 보증은 우회된다 (05_embedding_pipeline.md 6절).
4. 사실 — verify_mcp.py 가 검증하는 11개 항목
examples/meeting-rooms/verify_mcp.py:44-133
| # | 검사 |
|---|---|
| 1 | MCP 핸드셰이크 + 도구 발견 |
| 2 | get_neo4j_schema (apoc.meta.schema) |
| 3 | … 관계 방향을 보고하는가 |
| 4 | read_neo4j_cypher (EXPLAIN 이 'r' 을 보고) |
| 5 | 읽기 도구가 쓰기를 거부하는가 |
| 6 | 쓰기 도구가 읽기를 거부하는가 |
| 7 | 파라미터가 바인딩되는가 (보간 아님) |
| 8 | db.index.vector.queryNodes + score |
| 9 | 구문 오류가 삼켜지지 않고 보고되는가 |
| 10 | genai.vector.encode (DB 안에서 임베딩) |
| 11 | write_neo4j_cypher 가 변경 카운터를 반환하는가 |
11번은 생성과 삭제 양쪽을 확인한다 — 한쪽만 배선된 카운터도 단일 호출에서는 정상으로 보이기 때문이다(verify_mcp.py:147-160).
5. 사실 — 지원되는 Neo4j 프로시저
engine/src/compat/procs.rs:154-159 의 오류 메시지가 목록 그 자체다:
db.index.vector.queryNodes, db.index.fulltext.queryNodes,
apoc.meta.schema, apoc.neighbors.tohop, db.labels, db.relationshipTypes,
db.propertyKeys, dbms.components
db.index.vector.queryNodes(indexName, k, vector) 의 제약 (procs.rs:163-201):
indexName은 리터럴 문자열이어야 한다. 컴파일 시점에 해소되므로 파라미터로 줄 수 없다(procs.rs:168-171).- 인덱스 이름은
og_catalog.compat_index에서 조회된다. 없으면 알려진 인덱스 목록이 오류에 포함된다(procs.rs:275-291). - 질의 벡터는 파라미터(jsonb 배열) /
genai.vector.encode출력(float8[]) / 리터럴 텍스트 셋 다 받는다 —translate(x::text, '{}', '[]')로 통일된다 (procs.rs:182-187). filter를 넘기지 않는다 (procs.rs:188-194) — 이것이 Bolt 경로에서 SQL 인젝션이 불가능한 이유다 (06_retrieval_and_rrf.md 1.3절).
db.index.fulltext.queryNodes 는 PostgreSQL의 simple 사전을 쓴다 — 어간 분석도 CJK 분절도 하지 않으므로 한국어 재현율이 Neo4j와 다르다. 문서화된 차이다 (engine/src/compat/ddl.rs:253-258).
6. 사실 — 알려진 한계
| 한계 | 근거 |
|---|---|
| Bolt 5.x 미지원, Path 타입 미지원, TLS 미지원 | README 스펙 상태표 (011 항목) |
서버 문자열은 Neo4j/4.4.0 (ontological-bolt) 고정 |
bolt/src/session.rs:189 |
| MCP 연결에 역할·리소스 상한이 자동 적용되지 않음 (FR-033) | og_apply_role 을 호출하는 코드가 게이트웨이에 없다 (08_guardrails_and_roles.md 2절) |
Cypher UNION 미구현 |
README 스펙 상태표 (003 항목) |
| 오타 레이블의 교정 후보가 MCP 응답에 오는지 미확인 | PostgreSQL NOTICE로만 나가며, Bolt notification 매핑을 확인하는 테스트가 없다 (03_correctable_errors.md 1.2절) |
7. 필수(Required) / 금지(Forbidden)
필수
--schema-sample-size를 반드시 넘길 것 (3.2절).--database에는 그래프 이름을 넣을 것. PostgreSQL 데이터베이스는OG_BOLT_PGDATABASE로 지정한다 (2.3절).- MCP 세션의
--username/--password는 PostgreSQL 역할이므로, 에이전트 전용 역할을 만들고 권한을 좁힐 것 (08_guardrails_and_roles.md 6.1절). og_add_role로 관계의 양 끝을 선언할 것. 이것이apoc.meta.schema의 방향 정보이자 생성 Cypher의 화살표 오류를 줄이는 근거다 (3.1절).
금지
- 그래프 이름을
neo4j또는system으로 짓지 말 것 — 드라이버의 "지정 안 함"과 구분되지 않는다 (session.rs:224-229). - Bolt 게이트웨이를 TLS 없이 신뢰 경계 밖에 노출하지 말 것. 자격증명이 평문으로 오간다.
db.index.vector.queryNodes의 인덱스 이름을 파라미터로 전달하지 말 것 (5절).CREATE VECTOR INDEX로 만든 슬롯에 staleness 추적을 기대하지 말 것 (3.4절).
8. 참고
- 예제 전체: examples/meeting-rooms/README.md
- 설정 파일: examples/meeting-rooms/mcp.json
- 게이트웨이: bolt/src/main.rs, bolt/src/session.rs
- 프로시저 호환: engine/src/compat/procs.rs
- 스펙: FR-032~FR-034 (specs/008-agent-native-interface/spec.md:276-281)