NoteHarbor MCP
by kris-atelier
README.md
# NoteHarbor MCP
Obsidian 노트를 MCP와 GraphQL로 조회하고 PostgreSQL/pgvector로 확장하는 Python 예제입니다. 개인 vault, 실제 문서, API 키는 포함하지 않습니다.
NoteHarbor의 원본은 Markdown 파일입니다. 검색에 필요한 데이터만 별도로 만들고 원본은 그대로 보존합니다.
## 이 프로젝트의 기준
- **Local first**: 원본 Markdown은 로컬에 남기고, 외부 서비스는 연결 가능한 선택지로 둡니다.
- **Source preserving**: 검색·임베딩·동기화 결과가 원본을 대체하지 않도록 합니다.
- **Privacy by boundary**: 실제 vault, 개인 기록, API 키는 공개 프로젝트 밖에 둡니다.
- **Model neutral**: 임베딩 생성기와 Vector DB를 특정 업체에 고정하지 않습니다.
- **Small and replaceable**: 어댑터와 서비스 레이어를 작게 유지합니다.
## 공개 샘플 3가지
1. **MCP**: `upsert_vector`, `search_vectors` 도구
2. **GraphQL**: `indexChunk` Mutation, `vectorSearch` Query
3. **PostgreSQL/pgvector**: SQLAlchemy 모델, Repository 포트, Docker 개발 환경
운영 서비스가 아니라 구조를 확인하기 위한 예제입니다. 기본 Repository는 메모리에서 동작하고, PostgreSQL/pgvector로 옮길 지점과 SQL 예제를 함께 제공합니다.
## 빠른 시작
```bash
uv sync
```
MCP 샘플 실행:
```bash
uv run noteharbor
```
## Docker로 PostgreSQL 띄우기
```bash
docker compose up -d postgres
```
`docker-compose.yml`은 `pgvector/pgvector` 이미지와 Python MCP용 개발용 PostgreSQL을 정의합니다.
```bash
# PostgreSQL만
docker compose up -d postgres
# Python MCP까지
docker compose up --build python-mcp
```
`POSTGRES_URL`은 실제 DB 연결을 붙일 때 사용할 설정 자리입니다. 현재 기본 실행은 목업입니다.
## GraphQL 샘플
GraphQL 스키마는 `noteharbor/api/graphql_schema.py`에 있습니다. 외부 ASGI 서버에 `get_schema()` 결과를 마운트할 수 있습니다.
예시 작업:
```graphql
mutation {
indexChunk(
sourcePath: "sample.md"
content: "Obsidian knowledge"
embedding: [1.0, 0.0]
)
}
query {
vectorSearch(embedding: [0.9, 0.1], limit: 5) {
sourcePath
content
score
}
}
```
## CRUD와 ORM 스타일 경계
Repository는 검색뿐 아니라 NoteChunk의 생성부터 삭제까지 전체 수명주기를 담당합니다.
- **Create/Upsert**: MCP `upsert_vector`, GraphQL `indexChunk` → `VectorService.index_chunk()`
- **Read**: MCP `get_chunk`·`list_chunks`, GraphQL `noteChunk`·`noteChunks`
- **Update**: MCP·GraphQL `updateChunk` → 기존 레코드를 읽은 뒤 변경 필드만 반영
- **Delete**: MCP·GraphQL `deleteChunk`
- **Search**: MCP `search_vectors`, GraphQL `vectorSearch`
현재 어댑터는 동작 확인을 위한 in-memory 목업입니다. 실제 저장 방식은 SQLAlchemy ORM 경계 뒤에 둡니다. 실제 PostgreSQL adapter에서는 다음과 같은 세션 연산으로 `MockPostgresVectorRepository`를 교체합니다.
실제 ORM 흐름을 코드로 보고 싶다면 `noteharbor/infrastructure/postgres/sqlalchemy_repository.py`를 확인하면 됩니다. 이 adapter는 같은 `VectorRepository` 포트를 구현하면서 `Session.get`, `select`, `update`, `delete`를 사용합니다. 기본 실행은 여전히 목업이므로 PostgreSQL 연결 없이도 예제를 읽고 테스트할 수 있습니다.
```python
session.get(NoteChunkModel, chunk_id)
session.scalars(select(NoteChunkModel).limit(limit)).all()
session.execute(update(NoteChunkModel).where(NoteChunkModel.id == chunk_id).values(...))
session.execute(delete(NoteChunkModel).where(NoteChunkModel.id == chunk_id))
```
API는 SQL을 직접 다루지 않고 `MCP/GraphQL → VectorService → VectorRepository → SQLAlchemy/pgvector` 순서로 CRUD와 검색을 넘깁니다.
## 목적
이 저장소는 하나의 Markdown 원본을 MCP와 GraphQL 양쪽에서 같은 벡터 검색 서비스로 조회하는 Python 아키텍처 샘플입니다.
특정 embedding 업체나 벡터 DB를 정해 두기보다, 교체 가능한 지점을 분명히 두는 데 초점을 맞췄습니다.
```text
MCP / GraphQL API
→ VectorService
→ VectorRepository port
→ 현재: in-memory MockPostgresVectorRepository
→ 전환: PostgreSQL + pgvector
```
## 벡터화와 검색의 책임
현재 Python 샘플은 embedding을 직접 생성하지 않습니다. `indexChunk` mutation과 `upsert_vector` 도구가 외부에서 생성된 `list[float]` embedding을 받아 저장합니다.
embedding 생성을 분리한 이유는 다음과 같습니다.
- embedding 모델을 OpenAI·Voyage·로컬 모델 중 하나로 고정하지 않기 위해
- API 키와 개인 데이터를 공개 샘플에서 제외하기 위해
- API 계층과 검색 저장소의 책임을 분리하기 위해
- 실제 서비스에서 chunking·embedding 배치 파이프라인을 별도 worker로 교체하기 위해
검색은 다음 순서입니다.
```text
사용자 query embedding
→ GraphQL vectorSearch 또는 MCP search_vectors
→ VectorService.search()
→ Repository.search()
→ cosine similarity 계산
→ score가 높은 NoteChunk 반환
```
현재 `repository.py`의 `MockPostgresVectorRepository`는 이 흐름을 메모리에서 재현합니다. 실제 PostgreSQL adapter에서는 `embedding <=> :query_embedding`으로 cosine distance를 계산하고, `1 - distance`를 score로 반환합니다.
## 왜 PostgreSQL + pgvector인가
NoteChunk는 벡터만 갖는 값이 아니라 원본 경로·chunk 순번·본문·메타데이터를 함께 갖습니다. PostgreSQL과 pgvector를 조합하면 관계형 조건과 벡터 유사도 검색을 한 저장소와 SQL 경계 안에서 다룰 수 있습니다.
- PostgreSQL: 원문 메타데이터, 상태, 동기화 정보와 트랜잭션 관리
- pgvector: `vector` 컬럼, cosine distance(`<=>`), HNSW 인덱스
- Docker: 개발 환경에서 pgvector 실행 조건 고정
- SQLAlchemy Repository: 저장소 교체 지점
검색 규모가 커지면 전용 벡터 DB가 더 적합할 수 있습니다. 여기서는 문서 정보와 벡터 검색을 한 저장소에서 다루는 흐름을 보여줍니다.
## 양자화 범위
현재 Python 레포에는 양자화 구현을 넣지 않았습니다. 입력 embedding은 float 값 그대로 받아 검색하며, 이는 모델 중립적인 Repository 경계를 먼저 보여주기 위한 선택입니다.
양자화를 추가하는 경우에는 `EmbeddingQuantizer` 포트를 별도로 두고 `float32 → INT8/float16 → 저장·복원` 단계를 indexing worker와 Repository adapter 사이에 배치합니다. 양자화 방식은 검색 품질·메모리·지연시간을 측정한 뒤 결정해야 하므로, 이 샘플에서는 구현했다고 과장하지 않습니다.
## 두 NoteHarbor 샘플의 관계
- [noteharbor-mcp](https://github.com/kris-atelier/noteharbor-mcp): TypeScript MCP 도구와 임베딩·INT8 양자화 흐름
- `noteharbor-python`: Python MCP·GraphQL API와 VectorService/Repository 경계
두 레포는 같은 아이디어를 다른 언어와 API 방식으로 풀어 쓴 예제이며, 실제 개인 vault나 운영 데이터는 포함하지 않습니다.
## 추가 기술과 투입 이유
| 기술 | 투입 이유 |
| --- | --- |
| **uv** | Python 의존성·가상환경·lock 파일을 빠르게 재현하고 Docker에서도 같은 설치 경로를 사용하기 위해 |
| **FastMCP** | Python 함수와 MCP 도구의 연결을 짧고 명확하게 보여주기 위해 |
| **Pydantic** | API 경계의 입력 모델과 설정을 검증하기 위해 |
| **SQLAlchemy** | Repository가 특정 SQL 실행 방식에 고정되지 않도록 PostgreSQL adapter 경계를 두기 위해 |
| **psycopg** | Python에서 PostgreSQL 연결을 담당하기 위해 |
| **pgvector Python package** | SQLAlchemy 모델에서 PostgreSQL vector 타입을 표현하기 위해 |
| **Strawberry GraphQL** | 같은 VectorService를 GraphQL Query/Mutation으로 노출하는 예시를 만들기 위해 |
| **Docker Compose** | pgvector가 활성화된 PostgreSQL과 Python MCP의 개발 환경을 함께 재현하기 위해 |
각 기술은 API, 서비스, 저장소, 개발 환경의 역할을 나누어 설명하기 위해 선택했습니다.
## 프로젝트 구조
```text
.
├── noteharbor/
│ ├── mcp_server.py
│ ├── api/graphql_schema.py
│ ├── application/vector_service.py
│ ├── domain/
│ └── infrastructure/
│ ├── notion/mock_adapter.py
│ └── postgres/
│ ├── models.py
│ ├── repository.py
│ └── sqlalchemy_repository.py
├── tests/test_vector_repository.py
├── Dockerfile
├── docker-compose.yml
└── pyproject.toml
```
## 라이선스
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues