RAG文档检索与存储服务器

基于Spring AI MCP协议的RAG服务器,支持PDF/Markdown文档的向量化存储与检索。

  • by yongfire38
  • Java
  • 本地部署
  • 更新于
在 GitHub 查看 yongfire38/my-webflux-mcp-server

使用说明

my-webflux-mcp-server

Spring AI MCP(Model Context Protocol) 기반의 RAG(Retrieval-Augmented Generation) 서버입니다. PDF/마크다운 문서를 ONNX 임베딩 모델로 벡터화해 PostgreSQL(PGVector)에 저장하고, MCP 클라이언트의 요청에 따라 문서 검색·적재 도구를 노출합니다.


환경

항목 값
Java 17
eGovFrame Boot 5.0.0 (Spring Boot 3.5.6)
Spring AI 1.1.8
서버 포트 9090
MCP 타입 ASYNC / STREAMABLE HTTP
벡터 DB PostgreSQL + PGVector (localhost:5434, DB: mcpdb)
임베딩 모델 ONNX (ko-sroberta 계열, 768차원, jar 외부 경로)
Swagger UI http://localhost:9090/swagger-ui.html

사전 준비

1. 인프라 기동 (Docker Compose)

cd C:/workspace-team/my-webflux-mcp-server
docker-compose up -d

PostgreSQL(mcpdb:5434)과 pg_trgm 익스텐션이 자동으로 준비됩니다.

2. ONNX 임베딩 모델

모델 파일은 jar 외부 경로에 위치해야 합니다. 기본 경로:

  • Windows: %USERPROFILE%\spring-ai-Config\model\
  • Linux/macOS: ~/spring-ai-Config/model/

필요 파일: model.onnx, tokenizer.json, config.json, tokenizer_config.json, special_tokens_map.json

경로 변경: 환경변수 EMBEDDING_MODEL_PATH / EMBEDDING_TOKENIZER_PATH로 오버라이드 가능

3. 환경변수

변수 설명 기본값
MCP_API_KEY MCP ingestDocument 도구 인증 키 빈 문자열 (MCP 적재 전체 차단)
EMBEDDING_MODEL_PATH ONNX 모델 파일 전체 경로 ${user.home}/spring-ai-Config/model/model.onnx
EMBEDDING_TOKENIZER_PATH 토크나이저 파일 전체 경로 ${user.home}/spring-ai-Config/model/tokenizer.json

MCP_API_KEY를 설정하지 않으면 빈 문자열이 기본값이 되어 적재가 전체 차단됩니다(의도된 안전 기본값).

4. 문서 디렉토리 (REST 재인덱싱 경로)

C:/workspace-test/upload/data/

빌드 전 미리 생성하거나 application.yml의 app.document.path 경로를 변경하세요.


빌드 및 실행

cd C:/workspace-team/my-webflux-mcp-server
mvn clean compile
mvn spring-boot:run

MCP 노출 기능

도구 (Tool)

@McpTool 어노테이션으로 자동 등록됩니다. 모두 Mono<String> 반환(ASYNC 서버 필수).

도구 이름 구현 클래스 설명
searchDocuments(query) DocumentSearchServiceImpl 벡터 유사도 기반 문서 검색. RAG의 핵심 도구. hybrid.enabled: true 시 dense + pg_trgm lexical RRF 융합.
describeKnowledgeBase() DocumentSearchServiceImpl 인덱싱된 파일 목록과 청크 수 조회. searchDocuments 호출 전 사전 확인용.
ingestDocument(ctx, jobId, filename, content, mimeType) DocumentIngestServiceImpl 파일 수신 → 임베딩. X-MCP-API-Key 헤더 필수. Progress 4단계 + MCP Sampling(Ollama 요약 위임) 사용.
getCurrentDateTimeWithZone(zoneId) DateTimeServiceImpl 지정 타임존의 현재 날짜/시각 반환.

ingestDocument 상세

  • 인증: X-MCP-API-Key 헤더 값을 app.security.api-keys 목록과 대조. 불일치 시 도구가 거부 메시지를 반환.
  • 원본 파일 저장 없음: 클라이언트가 base64로 전송한 파일 내용을 메모리에서 처리 후 벡터 DB에만 적재. 서버 디스크에 원본 파일이 저장되지 않으므로 REST 재인덱싱 대상에서 제외됨 (의도된 동작).
  • 고아 정리 제외: document_metadata.source_client 가 non-null인 MCP 적재 문서는 REST reindex 시 고아 정리 대상에서 자동 제외됨(findAllDistinctFilenames() WHERE 조건).
  • Progress: 4단계(텍스트 추출 → 요약 → 청킹 → 임베딩) 진행률을 MCP 클라이언트에 실시간 전송.
  • Sampling: 문서 요약을 서버가 직접 생성하지 않고 MCP Sampling으로 클라이언트 Ollama에 위임.
  • 변경 감지: SHA-256 해시 비교로 변경된 페이지만 선별 재임베딩. 미변경 페이지 스킵.
  • Stale 벡터 정리: 재임베딩 전 기존 벡터를 페이지 단위로 선삭제.
  • 소유권 보호: 다른 클라이언트가 적재한 문서의 덮어쓰기 차단 (source_client 비교).
  • 프롬프트 인젝션 탐지: 텍스트 추출 직후 한/영 의심 패턴 검사 → 감지 시 적재 거부.

리소스 (Resource)

URI 설명
resource://documents/index 인덱싱된 파일 목록 (읽기 전용). describeKnowledgeBase 도구와 동일 내용.

프롬프트 (Prompt)

이름 설명
rag_assistant RAG 어시스턴트 시스템 프롬프트 템플릿. strict 파라미터(true/false)로 검색 강제 엄격도 조정. VS Code·Claude Desktop 등 외부 MCP 클라이언트용.

VS Code·Cline 등 범용 MCP 클라이언트 연결

http://localhost:9090/mcp를 Streamable HTTP로 등록하면 됩니다. ingestDocument는 인증 헤더가 필요하므로 X-MCP-API-Key를 같이 등록하세요.

VS Code Copilot — C:\Users\{사용자명}\AppData\Roaming\Code\User\mcp.json

{
  "servers": {
    "my-webflux-mcp-server": {
      "url": "http://localhost:9090/mcp",
      "type": "http",
      "headers": { "X-MCP-API-Key": "<발급받은 API 키>" }
    }
  }
}

Cline — cline_mcp_settings.json ("type"을 "streamableHttp"로 지정해야 함, "http" 아님)

{
  "mcpServers": {
    "my-webflux-mcp-server": {
      "url": "http://localhost:9090/mcp",
      "type": "streamableHttp",
      "headers": { "X-MCP-API-Key": "<발급받은 API 키>" },
      "disabled": false,
      "autoApprove": []
    }
  }
}

알아둘 점

  • searchDocuments·describeKnowledgeBase는 인증 없이 바로 호출 가능합니다. ingestDocument는 위 헤더가 없으면 거부됩니다.
  • ingestDocument로 파일을 적재하려면 클라이언트가 로컬 파일을 읽어 content 파라미터로 넘겨줘야 하는데, VS Code·Cline 자체에는 그 기능이 없습니다. @modelcontextprotocol/server-filesystem을 함께 등록해 read_file 결과를 넘기는 방식이 필요합니다 (filesystem 서버 필수).
  • 이 경로로는 텍스트 파일(.md, .txt)만 적재 가능합니다. PDF는 바이너리라 범용 클라이언트가 다룰 방법이 없어 지원되지 않습니다 — 커스텀 클라이언트나 REST /api/documents/reindex를 사용하세요.
  • 자세한 설정 예시·트러블슈팅(스키마 캐시, 인증 실패, base64 오디코딩 등)은 docs/vscode-mcp-client-troubleshooting.md 참고.

REST API

MCP 경로(/mcp/**)와 별개로 서버 파일시스템 문서를 관리하는 REST API입니다.

메서드 경로 인증 설명
GET /api/documents/status localhost 전용 ETL 진행 상태 조회
POST /api/documents/reindex localhost 전용 서버 로컬 문서 전체 재인덱싱 (202/409)

localhost 전용 경로는 외부 IP에서 호출 시 ApiSecurityFilter가 403을 반환합니다.

문서 파일 추가는 서버의 app.document.path 경로에 직접 복사 후 /api/documents/reindex 호출로 적재하거나, MCP ingestDocument 도구(base64 전송)를 사용합니다.


접속 URL

용도 URL
MCP 엔드포인트 http://localhost:9090/mcp
Swagger UI http://localhost:9090/swagger-ui.html
인덱싱 상태 http://localhost:9090/api/documents/status

보안 구조

계층 구현 대상
REST IP 제한 ApiSecurityFilter (WebFilter) /api/documents/reindex, /api/documents/status — localhost만 허용
MCP 적재 키 McpTransportConfig + Tool 내부 검증 ingestDocument — X-MCP-API-Key 헤더 추출 후 Tool 내부에서 검증

MCP 읽기 도구(searchDocuments, describeKnowledgeBase, getCurrentDateTimeWithZone)는 인증 없이 개방됩니다.


프로젝트 구조

src/main/java/com/example/webflux/
├── McpServerApplication.java
├── config/
│   ├── AsyncConfig.java                  # 비동기 스레드풀
│   ├── EgovCommonConfig.java
│   ├── HybridIndexInitializer.java       # pg_trgm GIN 인덱스 자동 생성 (hybrid.enabled=true 시)
│   ├── McpResourcePromptConfig.java      # @McpResource, @McpPrompt 등록
│   ├── McpTransportConfig.java           # WebFluxStreamableServerTransportProvider 오버라이드
│   │                                     #   → X-MCP-API-Key 헤더를 transportContext에 주입
│   └── SecurityProperties.java           # app.security.api-keys 바인딩
├── controller/
│   └── DocumentController.java           # REST /api/documents/**
├── dto/
│   └── DocumentStatusResponse.java
├── etl/
│   ├── ETLPipelineConfig.java
│   ├── readers/                          # MarkdownDocumentReader, PagePdfDocumentReader
│   ├── transformers/                     # ContentFormatTransformer, DocumentChunkTransformer, KoreanSentenceSplitter(선택)
│   └── writers/                          # VectorStoreDocumentWriter
├── model/
│   └── DocumentMetadata.java             # JPA 엔티티 (filename, hash, sourceClient 등)
├── repository/
│   └── DocumentMetadataRepository.java
├── security/
│   └── ApiSecurityFilter.java            # WebFilter: REST IP·API 키 검증
├── service/
│   ├── DateTimeService.java
│   ├── DocumentIngestService.java
│   ├── DocumentManagementService.java
│   ├── DocumentSearchService.java
│   └── impl/
│       ├── DateTimeServiceImpl.java               # @McpTool: getCurrentDateTimeWithZone
│       ├── DocumentIngestServiceImpl.java         # @McpTool: ingestDocument
│       ├── DocumentManagementServiceImpl.java     # REST reindex 처리 (ETL 파이프라인)
│       └── DocumentSearchServiceImpl.java         # @McpTool: searchDocuments, describeKnowledgeBase
└── util/
    ├── DocumentHashUtil.java              # SHA-256 해시 계산
    ├── DocumentOverwriteForbiddenException.java
    ├── KoreanSentenceSupport.java          # 한국어 문장 경계 판정 유틸 (KoreanSentenceSplitter가 사용)
    ├── PromptInjectionDetector.java       # 자연어 프롬프트 인젝션 패턴 탐지
    ├── PromptInjectionDetectedException.java
    ├── RrfFusion.java                     # Reciprocal Rank Fusion (하이브리드 검색)
    └── StaleVectorCleaner.java            # 재임베딩 전 기존 벡터 선삭제

application.yml 주요 설정

spring:
  ai:
    mcp:
      server:
        type: ASYNC
        protocol: STREAMABLE
        instructions: |
          이 서버는 RAG 지식 베이스 검색과 문서 적재를 제공합니다.
          - searchDocuments: 질문에 관련된 문서 청크를 벡터 유사도로 검색
          - ingestDocument: PDF/MD 적재 (전용 클라이언트에서만 호출)
        streamable-http:
          keep-alive-interval: 30s      # idle 연결 유지
    embedding.transformer.onnx:
      modelUri: file:${EMBEDDING_MODEL_PATH:${user.home}/spring-ai-Config/model/model.onnx}
    vectorstore.pgvector:
      dimensions: 768
      distance-type: COSINE_DISTANCE

server:
  port: 9090

app:
  security:
    api-keys:
      - "${MCP_API_KEY:}"               # 빈 문자열은 유효 키로 불인정 → 적재 차단
  rag:
    similarity-threshold: 0.30
    top-k: 5
    hybrid:
      enabled: false                    # true 시 dense + lexical RRF 융합 검색
      lexical.word-similarity-threshold: 0.30
  document:
    chunk-size: 4000
    min-chunk-size-chars: 350
    max-num-chunks: 500
    min-chunk-length-to-embed: 50
    chunking:
      korean-sentence:
        enabled: false                  # true 시 문장경계 인지 스플리터(KoreanSentenceSplitter) 사용
    normalization:
      normalize-newlines: true          # true면 문단 경계(빈 줄) 규칙 무력화 — 아래 "한국어 문장경계 청킹" 참고

springdoc:
  paths-to-exclude: /mcp/**            # Swagger에서 MCP 경로 제외

한국어 문장경계 청킹 (선택 기능)

기본은 Spring AI TokenTextSplitter(토큰 수 기준 균등 분할)입니다. app.document.chunking.korean-sentence.enabled: true로 켜면 KoreanSentenceSplitter가 한국어 종결어미·마크다운 블록·코드펜스 경계를 우선 보존하며 분할합니다.

항목 내용
기본값 false (기존 동작 유지)
적합한 문서 한국어 위주 문서
부적합할 수 있는 경우 영어 비중이 높은 문서 (영어 문장 끝 마침표는 경계로 보지 않아 구간이 길어질 수 있음)
오버랩 없음 (기존 TokenTextSplitter와의 정합을 위해 의도적으로 생략)

주의: app.document.normalization.normalize-newlines: true(기본값)이면 연속 개행이 정규화 단계에서 미리 1개로 축약되어 빈 줄 기준 문단 경계 규칙이 동작하지 않습니다. 마크다운 블록 시작·코드펜스 경계·문장 종결 경계는 이 설정과 무관하게 정상 동작하지만, 문단 경계까지 온전히 쓰려면 normalize-newlines: false로 내려야 합니다.

활성화 여부는 기동 로그의 TextSplitter 설정 - splitter: KoreanSentenceSplitter, ...로 확인할 수 있습니다.


트러블슈팅

증상 원인 해결
model.onnx not found ONNX 모델 파일 누락 %USERPROFILE%\spring-ai-Config\model\에 모델 파일 복사
PGVector 연결 오류 PostgreSQL 미기동 docker-compose up -d 실행
MCP 적재 인증 실패 MCP_API_KEY 미설정 또는 클라이언트 키 불일치 서버·클라이언트 양쪽 MCP_API_KEY 환경변수 일치 여부 확인 후 재기동
인덱싱 중 409 Conflict 재인덱싱 이미 진행 중 /api/documents/status 확인 후 완료 대기
Sampling 요약 미생성 연결된 클라이언트의 Ollama 미기동 또는 클라이언트가 Sampling 미지원 클라이언트 쪽 Ollama 기동 확인. 서버는 자체 Ollama가 없어 대체 생성하지 않음 — Sampling 미지원 클라이언트는 요약 없이 적재됨(정상 동작, 적재 자체는 성공).
하이브리드 검색 결과 수 < topK RRF 결과 중 lexical 전용 문서는 본문 없어 제외 정상 동작. topK 증가 또는 dense 가중치 조정으로 완화 가능.
Swagger에 MCP API 없음 의도된 동작 /mcp/**는 Swagger 제외. /api/documents/**만 표시.
MCP 적재 후 reindex 시 문서 사라짐 이전 버전 버그 (현행 수정됨) source_client IS NULL 조건으로 MCP 적재 문서는 고아 정리 제외. 최신 서버 사용.