Skip to main content
Glama
README.md
# NoteHarbor MCP — TypeScript

Obsidian Markdown을 MCP 도구로 연결하고 PostgreSQL/pgvector 검색으로 확장하는 TypeScript 예제입니다.

NoteHarbor는 **MCP 인터페이스와 벡터 저장소를 분리**합니다. 클라이언트는 MCP 도구를 호출하고, 서비스가 도메인 모델과 Repository를 통해 저장소를 사용합니다.

## 아키텍처

```text
MCP Client
    │
    ▼
MCP Tools
(upsert_vector / search_vectors)
    │
    ▼
Vector Service
(src/application/vectorService.ts)
    │
    ▼
VectorRepository port
(src/domain/knowledge.ts)
    │
    ├── 현재 실행 어댑터: in-memory Map 목업
    │
    └── 운영 전환 지점: PostgreSQL + pgvector
        (src/infrastructure/postgres/)
```

노트는 다음 순서로 검색 데이터가 됩니다.

```text
Obsidian Markdown
  → note chunk
  → externally generated embedding
  → note_chunks.embedding (pgvector)
  → cosine similarity search
  → MCP response
```

## 실제 벡터화 흐름

`upsert_vector`는 이미 만든 embedding을 저장하는 도구입니다. Markdown이 chunk와 벡터로 바뀌는 과정은 `index_note`에서 확인할 수 있습니다.

```text
Markdown text
  → splitMarkdownIntoChunks()
  → EmbeddingProvider.embed(chunk)
  → normalized number[]
  → NoteChunk
  → VectorService.indexChunk()
  → VectorRepository.save()
```

주요 코드는 다음 파일에 나뉘어 있습니다.

- [chunker.ts](src/application/chunker.ts): Markdown을 paragraph 기준으로 나누고 최대 길이를 적용
- [embeddingProvider.ts](src/application/embeddingProvider.ts): API 키 없이 동작하는 결정적 데모 임베딩 provider
- [indexingPipeline.ts](src/application/indexingPipeline.ts): chunk 생성·임베딩·저장을 연결
- [knowledge.ts](src/domain/knowledge.ts): `EmbeddingProvider`와 `VectorRepository` 포트
- [index.ts](index.ts): `index_note`, `upsert_vector`, `search_vectors` MCP 도구 등록

데모 provider는 흐름을 확인하기 위한 결정적 구현입니다. 의미 기반 검색 품질을 제공하는 모델은 아니며, 실제 서비스에서는 같은 `EmbeddingProvider` 포트에 외부나 로컬 모델을 연결합니다.

양자화는 임베딩 생성 뒤에 적용합니다.

```text
float embedding [-1, 1]
  → clamp
  → int8 = round(value / (1 / 127))
  → 저장: values + scale + zeroPoint
  → 복원: (int8 - zeroPoint) * scale
```

샘플은 대칭형 scalar INT8 양자화를 사용합니다.

- 값 범위: `[-1, 1]`
- 양자화 범위: `[-127, 127]`
- `scale`: `1 / 127`
- `zeroPoint`: `0`
- 원본 임베딩과 함께 `embedding_int8`, `embedding_scale`, `embedding_zero_point`를 저장하도록 스키마를 정의
- 현재 참조 검색은 복원 가능한 float vector를 사용하며, 양자화 ANN 인덱스는 실제 adapter 선택 시 추가

구현은 [`quantizer.ts`](src/application/quantizer.ts)와 [`indexingPipeline.ts`](src/application/indexingPipeline.ts)에 있습니다. `index_note` 응답에도 임베딩 차원과 양자화 비트 수가 포함됩니다.

## 왜 이렇게 구성했는가

NoteHarbor는 Obsidian Markdown을 검색 가능한 지식 단위로 바꾸고, 그 기능을 MCP 도구로 제공하는 예제입니다.

단순 문자열 검색만으로는 표현이 다른 관련 내용을 찾기 어렵습니다. 그래서 노트를 작은 chunk로 나누고, 각 chunk를 embedding으로 바꿔 의미가 가까운 내용을 검색할 수 있게 합니다.

양자화는 이 embedding을 더 작은 표현으로 보관하기 위한 선택입니다.

- 메모리와 저장 공간을 줄임
- 벡터 전송량을 줄임
- 대규모 지식베이스에서 캐시와 배치 처리에 유리
- 대신 원본 float보다 정밀도가 낮아질 수 있음

그래서 각 구성요소의 역할을 다음처럼 나눴습니다.

1. **Embedding**: 텍스트의 의미를 수치 벡터로 표현
2. **Quantization**: 벡터의 정밀도를 낮춰 저장 비용을 줄임
3. **Vector search**: 가까운 벡터를 찾아 관련 chunk를 반환
4. **MCP**: 이 기능을 LLM 클라이언트가 호출할 수 있는 도구로 노출

이 프로젝트에서 INT8을 선택한 이유는 양자화 원리와 저장 형태를 코드로 설명하기 쉽기 때문입니다. 실제 서비스에서는 검색 품질, 메모리 절감, 지연시간을 측정한 뒤 float32·float16·INT8·binary 중 하나를 선택해야 합니다.

현재 구현은 흐름을 확인하기 위한 목업입니다. 의미 기반 검색 품질이나 양자화 성능을 보장하지 않으며, 실제 운영에서는 embedding provider와 pgvector adapter를 연결해야 합니다.
## 벡터 DB 설계

벡터 저장 단위는 `NoteChunk`입니다.

| 필드 | 의미 |
| --- | --- |
| `id` | 원본 경로와 chunk 순번으로 만든 식별자 |
| `sourcePath` | Obsidian Markdown 원본 경로 |
| `chunkIndex` | 문서 안의 chunk 순번 |
| `content` | 검색 결과로 되돌릴 텍스트 |
| `embedding` | 외부 임베딩 모델이 생성한 벡터 |
| `metadata` | 태그·상태·원본 속성 등 확장 정보 |

실행 가능한 SQL 설계 예시는 [`src/infrastructure/postgres/schema.sql`](src/infrastructure/postgres/schema.sql)에 있습니다. 기본 예시는 1,536차원 임베딩과 cosine distance용 HNSW 인덱스를 사용하며, 실제 임베딩 모델의 차원에 맞춰 조정합니다.

검색은 PostgreSQL에서 다음 형태로 전환됩니다.

```sql
SELECT id, source_path, chunk_index, content, metadata,
       1 - (embedding <=> $1::vector) AS score
FROM note_chunks
ORDER BY embedding <=> $1::vector
LIMIT $2;
```

## CRUD와 ORM 스타일 경계

벡터 저장소는 검색 전용이 아니라 `NoteChunk`의 수명주기를 관리하는 Repository입니다.

- **Create/Upsert**: `upsert_vector`, `index_note` → `VectorService.indexChunk()`
- **Read**: `get_chunk`, `list_chunks`
- **Update**: `update_chunk` → 기존 chunk를 읽고 변경 필드와 양자화 표현을 함께 갱신
- **Delete**: `delete_chunk`
- **Search**: `search_vectors`, `search_knowledge`

현재 실행 어댑터는 Map입니다. 운영 환경에서는 Repository 안쪽 구현을 Drizzle ORM과 pg로 바꾸면 됩니다.

```ts
db.insert(noteChunks).values(row).onConflictDoUpdate(...)
db.select().from(noteChunks).where(eq(noteChunks.id, id)).limit(1)
db.update(noteChunks).set(values).where(eq(noteChunks.id, id))
db.delete(noteChunks).where(eq(noteChunks.id, id))
```

MCP 도구는 SQL을 직접 실행하지 않고 `MCP → VectorService → VectorRepository → Drizzle/pgvector` 순서로 CRUD와 검색을 넘깁니다.

## 포함된 샘플

- Obsidian Markdown 목록·읽기·검색
- `index_note`, `search_knowledge`, `upsert_vector`, `search_vectors` MCP 도구
- `MCP → Service → Repository → pgvector` 계층
- Drizzle ORM 기반 `note_chunks` 스키마 매핑
- PostgreSQL/pgvector 스키마와 검색 SQL
- Notion 동기화 경계
- Smithery 개발 서버와 Docker 실행 예시

현재 기본 실행은 개인 데이터와 외부 자격증명을 포함하지 않는 **메모리 목업**입니다. PostgreSQL 연결에서는 `src/infrastructure/postgres/postgresVectorRepository.ts`의 `Map` 구현을 Drizzle/pg 기반 어댑터로 교체합니다. README와 스키마는 그 전환 지점을 공개적으로 보여주기 위한 샘플입니다.

Python/GraphQL 버전은 [noteharbor-python](https://github.com/kris-atelier/noteharbor-python)에서 확인할 수 있습니다.

## 시작

```bash
npm install
npm run dev
```

Docker:

```bash
docker build -f Dockerfile.typescript -t noteharbor-mcp-ts .
docker run --rm -p 8081:8081 noteharbor-mcp-ts
```

## 왜 PostgreSQL + pgvector인가

NoteHarbor의 검색 대상은 벡터만 있는 데이터가 아닙니다. 원본 경로, chunk 순번, 태그, 상태, 동기화 정보 같은 관계형 메타데이터를 함께 다뤄야 합니다.

그래서 이 샘플에서는 별도 벡터 DB를 추가하기보다 PostgreSQL에 다음을 함께 둡니다.

- `content`: 검색 결과로 보여줄 원문 조각
- `metadata`: 태그·상태·원본 정보
- `embedding`: cosine 검색용 float vector
- `embedding_int8`: 저장·전송 비용을 줄이기 위한 양자화 표현

pgvector를 선택한 구체적인 이유는 다음과 같습니다.

- **데이터 결합**: 벡터 유사도와 `source_path`, 태그, 상태 조건을 한 SQL에서 조합할 수 있음
- **일관성**: 원문 메타데이터와 검색 인덱스를 같은 트랜잭션 경계에서 관리할 수 있음
- **운영 단순성**: 애플리케이션이 PostgreSQL과 별도 벡터 DB를 각각 운영하지 않아도 됨
- **검색 기능**: cosine distance(`<=>`)와 HNSW 인덱스를 PostgreSQL 확장으로 사용할 수 있음
- **확장 경로**: 초기에는 단일 저장소로 시작하고, 규모가 커질 때 검색 전용 adapter를 분리할 수 있음

검색 규모가 커지면 전용 벡터 DB가 더 적합할 수 있습니다. 여기서는 원문, metadata, 벡터 검색을 한 애플리케이션 경계에서 다루는 흐름을 보여주는 데 의미가 있습니다.
## 검색은 어떻게 동작하는가

검색은 원문 문자열을 직접 비교하지 않고, 질문과 노트 chunk를 같은 embedding 공간에 놓은 뒤 거리를 비교합니다.

```text
사용자 질문
  → query embedding 생성
  → INT8 양자화 후 복원
  → PostgreSQL/pgvector cosine distance 검색
  → 가까운 NoteChunk 반환
  → sourcePath·content·metadata와 함께 MCP 응답
```

실행 가능한 샘플은 `search_knowledge` MCP 도구입니다.

1. `index_note`가 Markdown을 chunk로 나누고 각 chunk의 embedding을 저장합니다.
2. 사용자가 자연어 `query`를 보냅니다.
3. 같은 `EmbeddingProvider`로 query embedding을 생성합니다.
4. query를 저장 벡터와 같은 방식으로 양자화·복원합니다.
5. `VectorRepository.search()`가 cosine similarity 기준으로 가까운 chunk를 정렬합니다.
6. 검색 결과에는 원본 경로, chunk 내용, metadata, score가 포함됩니다.

`search_vectors`는 이미 만들어진 embedding을 직접 받는 저수준 도구이고, `search_knowledge`는 자연어 질문부터 검색 결과까지 연결하는 응용 수준 도구입니다.

현재 목업 Repository는 메모리 `Map`에서 cosine similarity를 계산합니다. PostgreSQL adapter로 전환하면 같은 포트 뒤에서 pgvector의 `<=>` 연산과 `LIMIT` 검색을 사용하게 됩니다.
## 추가 기술과 투입 이유

| 기술 | 투입 이유 |
| --- | --- |
| **Node.js ESM** | TypeScript MCP 샘플을 현재 Node 런타임 방식으로 단순하게 실행하기 위해 |
| **MCP SDK** | `index_note`, `search_knowledge`, `upsert_vector`, `search_vectors` 도구를 표준 MCP 서버로 등록하기 위해 |
| **Zod** | MCP 입력값과 설정값을 런타임에서 검증하기 위해 |
| **Drizzle ORM** | PostgreSQL adapter를 연결할 때 타입 안전한 스키마·쿼리 경계를 제공하기 위해 |
| **pg** | 실제 PostgreSQL 연결 adapter로 전환할 때 사용할 드라이버 |
| **Smithery CLI** | MCP 서버를 개발·검증하는 실행 경로를 제공하기 위해 |
| **Docker** | 로컬과 배포 환경의 Node/MCP 실행 조건을 고정하기 위해 |
| **chokidar·fast-glob** | Markdown vault 변경 감지와 파일 탐색을 담당하기 위해 |
| **gray-matter·marked** | Markdown frontmatter와 본문을 지식 chunk로 다루기 위해 |
| **dotenv** | 로컬 환경 설정을 코드와 분리하기 위해 |

모든 의존성이 벡터 검색의 핵심은 아닙니다. 일부는 Obsidian·Notion 연동을 위한 보조 기술이고, 검색 경로의 중심은 MCP SDK → Vector Service → Repository → pgvector입니다.

## 기술을 선택한 이유

| 기술 | 선택 이유 |
| --- | --- |
| **Obsidian Markdown** | 원본이 평문 파일이라 소유권과 이동성이 높고, 특정 SaaS에 지식 원본을 종속시키지 않기 위해 선택 |
| **MCP** | LLM 클라이언트마다 별도 연동 코드를 만들지 않고, 동일한 지식 도구를 표준 인터페이스로 노출하기 위해 선택 |
| **TypeScript** | MCP SDK와의 연결이 자연스럽고, 도구 입력·출력 경계를 타입으로 관리하기 위해 선택 |
| **PostgreSQL** | 문서 메타데이터·상태·검색 결과를 한 저장소에서 일관되게 관리하고, 운영 전환 경로를 확보하기 위해 선택 |
| **pgvector** | 별도 벡터 DB를 추가하지 않고 PostgreSQL 안에서 원문 메타데이터와 벡터 검색을 함께 다루기 위해 선택 |
| **Notion adapter** | Notion을 원본 저장소로 삼기보다, 필요한 경우 지식 조각을 외부 워크스페이스로 동기화하는 경계를 보여주기 위해 선택 |
| **Docker** | 로컬 환경과 배포 환경에서 PostgreSQL·MCP 실행 조건을 일정하게 만들기 위해 선택 |

핵심은 도구를 많이 붙이는 것이 아닙니다. **원본은 Markdown으로 보존하고, 검색용 파생 데이터는 PostgreSQL/pgvector에 두며, LLM에는 MCP로 필요한 기능만 제공하는 것**입니다.

따라서 이 프로젝트는 Obsidian·Notion·PostgreSQL을 모두 원본으로 사용하는 시스템이 아닙니다.

- Obsidian Markdown: 원본 지식
- PostgreSQL/pgvector: 검색용 파생 인덱스
- Notion: 선택적 외부 동기화 대상
- MCP: LLM 접근 경계
## 설계 포인트

- 원본 Markdown 보존
- MCP 도구와 서비스 레이어 분리
- `NoteChunk` 도메인 모델과 `VectorRepository` 포트
- PostgreSQL/pgvector로 교체 가능한 저장소 경계
- 실제 개인 vault와 자격증명 제외

이 프로젝트는 MCP와 지식 검색 구조를 확인하기 위한 공개 예제입니다.

## 라이선스

MIT

Maintenance

ActivityMaintained
ResponsivenessNo issues