Files
ExperionCrawler/mcp-server/server.py
windpacer 77bdcf1f7f feat: ExperionCrawler IIoT OPC UA Data Bridge Infrastructure
Major project initialization and feature implementation:

**Core Features:**
- OPC UA client for Honeywell Experion HS R530 integration
- Real-time data streaming and history data retrieval
- Text-to-SQL query engine with TimeScaleDB
- JSON-based node configuration system
- SQLite database with migration support

**Architecture:**
- Clean architecture with Domain, Application, Infrastructure layers
- ASP.NET Core Web API frontend
- Web UI with real-time visualization
- PKI-based OPC UA authentication (TLS)

**Infrastructure Components:**
- ExperionOpcClient: OPC UA connection management
- ExperionRealtimeService: Real-time data streaming
- ExperionHistoryService: Historical data queries
- TextToSqlService: Natural language to SQL queries
- SqlValidator: SQL injection prevention

**Database:**
- TimescaleDB integration (recommended) or SQLite fallback
- Entity Framework Core with Extenstion methods
- OPCTag, KeyValue tables for data storage

**Security:**
- Certificate-based OPC UA endpoint security
- SSL/TLS encryption for database connections
- Output param binding injection prevention

**Testing:**
- Unit tests for TextToSqlService and SqlValidator
- Integration tests for Korean time range extraction

See REVIEW_REQUEST.md for detailed code review information.
2026-04-26 19:28:56 +09:00

170 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""
ExperionCrawler RAG MCP Server
- 임베딩: Ollama nomic-embed-text (768-dim) — Roo Code 인덱스와 동일 모델
- 벡터 DB: Qdrant localhost:6333
- LLM: vLLM GLM-4.7-Flash localhost:8000/v1
- 사용처: Claude Code MCP / Roo Code MCP (동일 서버)
"""
from __future__ import annotations
import sys
import logging
import httpx
from functools import lru_cache
from mcp.server.fastmcp import FastMCP
logging.basicConfig(level=logging.WARNING, stream=sys.stderr)
# ── 설정 ──────────────────────────────────────────────────────────────────────
QDRANT_URL = "http://localhost:6333"
OLLAMA_URL = "http://localhost:11434"
EMBED_MODEL = "nomic-embed-text" # 768-dim, Roo Code 인덱스와 동일
VLLM_BASE_URL = "http://localhost:8000/v1"
VLLM_MODEL = "glm-4.7-flash"
# Qdrant 컬렉션
COL_CODEBASE = "ws-65f457145aee80b2" # ExperionCrawler 소스코드
COL_OPC_DOCS = "experion-opc-docs" # Experion HS R530 OPC UA 공식 문서 (266 chunks)
mcp = FastMCP("iiot-rag")
# ── 임베딩 (Ollama) ───────────────────────────────────────────────────────────
def _embed(text: str) -> list[float]:
"""Ollama nomic-embed-text로 768-dim 벡터 생성."""
with httpx.Client(timeout=30) as client:
resp = client.post(
f"{OLLAMA_URL}/api/embeddings",
json={"model": EMBED_MODEL, "prompt": text},
)
resp.raise_for_status()
return resp.json()["embedding"]
# ── LLM (vLLM / GLM-4.7-Flash) ───────────────────────────────────────────────
@lru_cache(maxsize=1)
def _llm():
from openai import OpenAI
return OpenAI(base_url=VLLM_BASE_URL, api_key="dummy")
# ── Qdrant 검색 헬퍼 ──────────────────────────────────────────────────────────
def _search(collection: str, query: str, top_k: int, threshold: float = 0.25) -> str:
vec = _embed(query)
with httpx.Client(timeout=20) as client:
resp = client.post(
f"{QDRANT_URL}/collections/{collection}/points/search",
json={
"vector": vec,
"limit": top_k,
"with_payload": True,
"score_threshold": threshold,
},
)
resp.raise_for_status()
hits = resp.json().get("result", [])
if not hits:
return "관련 결과 없음."
parts = []
for h in hits:
p = h.get("payload", {})
file_path = p.get("filePath", p.get("path", "unknown"))
chunk = p.get("codeChunk", p.get("content", p.get("text", "")))
start_line = p.get("startLine", "")
loc = f"{file_path}:{start_line}" if start_line else file_path
parts.append(f"[score={h['score']:.3f}] {loc}\n```\n{chunk[:700]}\n```")
return "\n\n---\n\n".join(parts)
# ── MCP 도구 ─────────────────────────────────────────────────────────────────
@mcp.tool()
def search_codebase(query: str, top_k: int = 6) -> str:
"""ExperionCrawler 프로젝트 소스코드 검색 (우리가 개발한 .NET 8 C# 코드).
Experion HS R530 공식 문서가 아닌, ExperionCrawler 구현 코드를 검색함.
사용 시점: ExperionCrawler 코드의 구현 방법, 버그, 구조를 알고 싶을 때.
⚠️ Experion HS R530 제품 동작/설정/스펙을 알고 싶으면 search_r530_docs 사용.
Args:
query: 검색어 (예: "OPC UA 구독 시작", "히스토리 스냅샷", "TextToSql 서비스")
top_k: 반환 결과 수 (기본 6)
"""
return _search(COL_CODEBASE, query, top_k)
@mcp.tool()
def search_r530_docs(query: str, top_k: int = 5) -> str:
"""Honeywell Experion HS R530 공식 제품 문서 검색.
ExperionCrawler 코드가 아닌, Honeywell 공식 HTM 문서를 검색함.
사용 시점: Experion HS R530의 OPC UA 설정, 인증서, 보안 정책, 포인트 주소 형식,
채널/컨트롤러 속성, 문제해결 등 제품 스펙과 동작을 알고 싶을 때.
⚠️ ExperionCrawler 구현 코드를 찾으려면 search_codebase 사용.
Args:
query: 검색어 (예: "certificate configuration", "endpoint security policy", "point address syntax")
top_k: 반환 결과 수 (기본 5)
"""
return _search(COL_OPC_DOCS, query, top_k)
@mcp.tool()
def ask_iiot_llm(question: str, context: str = "") -> str:
"""GLM-4.7-Flash에게 IIoT/OPC UA 질문 (컨텍스트 없이 LLM 직접 질문).
사용 시점: search_codebase 또는 search_r530_docs 결과를 context로 넘겨
종합 분석·답변이 필요할 때. 또는 일반 IIoT/OPC UA 개념 질문.
Args:
question: 질문 내용
context: (선택) search_codebase 또는 search_r530_docs 검색 결과
"""
system = (
"당신은 IIoT(산업용 IoT), OPC UA, Honeywell Experion PKS/HS R530 전문가입니다.\n"
"컨텍스트가 제공된 경우 컨텍스트를 우선 근거로 삼아 한국어로 답변합니다.\n"
"컨텍스트 출처가 'Experion HS R530 공식 문서'인지 'ExperionCrawler 코드'인지 명확히 구분하여 설명합니다."
)
user_msg = f"컨텍스트:\n{context}\n\n질문: {question}" if context else question
resp = _llm().chat.completions.create(
model=VLLM_MODEL,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user_msg},
],
max_tokens=2048,
temperature=0.1,
)
return resp.choices[0].message.content or "(응답 없음)"
@mcp.tool()
def rag_query(question: str, search_code: bool = False, search_docs: bool = True) -> str:
"""검색 → GLM-4.7-Flash 답변 생성 (통합 RAG).
기본값: Experion HS R530 공식 문서만 검색 (search_docs=True, search_code=False).
ExperionCrawler 코드도 함께 보려면 search_code=True 추가.
사용 시점: Experion HS R530 제품 질문이나 ExperionCrawler 코드 질문에
검색+LLM 답변을 한 번에 얻고 싶을 때.
Args:
question: 질문
search_docs: Experion HS R530 공식 문서 검색 여부 (기본 True)
search_code: ExperionCrawler 소스코드 검색 여부 (기본 False)
"""
context_parts: list[str] = []
if search_docs:
context_parts.append(f"=== Experion HS R530 공식 문서 ===\n{_search(COL_OPC_DOCS, question, 4)}")
if search_code:
context_parts.append(f"=== ExperionCrawler 구현 코드 ===\n{_search(COL_CODEBASE, question, 3)}")
return ask_iiot_llm(question, "\n\n".join(context_parts))
if __name__ == "__main__":
mcp.run(transport="stdio")