Local LLM : LM Studio + 시간 개념 + 검색 기능 MCP (260220)

WonTerry·2026년 2월 19일

MCP

목록 보기
18/23

🌐 Web Search MCP Server (Powered by DuckDuckGo)

이 MCP 서버는 LM Studio, Claude Desktop 등 MCP를 지원하는 AI 에이전트에게 실시간 웹 검색 능력을 부여한다. 별도의 API 키(Google, Bing 등) 없이도 DuckDuckGo를 통해 빠르고 안정적인 검색 결과를 제공하도록 설계되었다.

✨ 주요 특징 (Key Features)

  • Zero-Config 검색: 별도의 유료 API 키 등록 없이 duckduckgo_search 라이브러리를 활용해 즉시 검색이 가능하다.
  • 지능형 지역(Region) 판별: 검색어의 언어(ASCII 여부)를 감지하여 영어권(us-en)과 한국어권(kr-kr) 검색 결과를 최적화하여 반환한다.
  • 스팸 사이트 필터링: 검색 품질을 저해하는 특정 도메인(Baidu, Sogou 등)을 쿼리 수준에서 자동으로 제외하여 고품질의 결과만 수집한다.
  • 이중 검색(Fallback) 시스템: 1차 필터링 검색에서 결과가 부족할 경우, 범용적인 전 세계(wt-wt) 설정으로 자동 재검색을 시도하여 신뢰성을 높였다.
  • 투명한 로깅: 검색 시작, 인스턴스 생성, 파싱 단계별 상태를 stderr 로그로 출력하여 LM Studio 내에서 작동 과정을 실시간으로 모니터링할 수 있다.

Web_search_MCP 적용 후

Web_search_MCP 적용 전 (+ Time_aware_MCP OFF)


Web_search_MCP.py

import sys
from ddgs import DDGS
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("web-search-mcp")

def log(msg: str):
    """LM Studio stderr 로그에 출력"""
    print(f"[web-search] {msg}", file=sys.stderr, flush=True)

@mcp.tool()
def web_search(query: str, max_results: int = 5) -> str:
    """
    Perform a stable and optimized web search using DuckDuckGo.
    """
    log(f" 검색 시작: '{query}'")

    # 1 입력 검증
    if not isinstance(query, str) or not query.strip():
        return "Invalid query."
    try:
        max_results = int(max_results)
    except:
        max_results = 5
    max_results = max(1, min(max_results, 15))

    # 2 원본 쿼리 저장 후 region 판별
    original_query = query.strip()
    # DDGS 지역 코드는 보통 'kr-kr' 보다는 'kr-kr' 혹은 'wt-wt' 형식을 쓰지만 
    # 최신 버전 호환성을 위해 소문자 유지
    region = "us-en" if original_query.isascii() else "kr-kr"
    log(f" region: {region}, max_results: {max_results}")

    # 3 원치 않는 사이트 차단
    blocked_sites = ["baidu.com", "zhidao.baidu.com", "so.com", "sogou.com"]
    filtered_query = original_query
    for site in blocked_sites:
        filtered_query += f" -site:{site}"

    # 4 검색 실행
    results = []
    try:
        log(" DDGS 인스턴스 생성 중...")
        with DDGS() as ddgs:
            log(" ddgs.text() 호출 중...")
            # 수정 포인트: keywords= 대신 첫 번째 인자로 filtered_query를 전달하거나 
            # 라이브러리 버전에 따라 keywords 대신 q를 사용하기도 합니다. 
            # 가장 안전한 방법은 위치 인자로 첫 번째에 넣는 것입니다.
            raw_results = ddgs.text(
                filtered_query,  # keywords= 대신 직접 전달
                region=region,
                safesearch="moderate",
                timelimit=None, # 최신 버전에서 요구할 수 있는 인자
                max_results=max_results
            )
            
            log(f" raw_results 수신 완료, 파싱 시작")
            if raw_results:
                for r in raw_results:
                    title = r.get("title", "")
                    url = r.get("href", "")
                    body = r.get("body", "")
                    if not title or not url:
                        continue
                    results.append(f"Title: {title}\nURL: {url}\nSnippet: {body}")
            log(f" 파싱 완료: {len(results)}개 결과")
    except Exception as e:
        log(f" 검색 오류: {str(e)}")
        # 에러 발생 시 중단하지 않고 Fallback으로 넘어가기 위해 return 대신 pass 고려 가능

    # 5 중복 제거
    results = list(dict.fromkeys(results))

    # 6 Fallback (결과가 없을 경우)
    if not results:
        log(" 결과 없음, Fallback 검색 시도")
        try:
            with DDGS() as ddgs:
                raw_results = ddgs.text(
                    original_query, # 원본 쿼리로 재시도
                    region="wt-wt",
                    safesearch="moderate",
                    max_results=5
                )
                if raw_results:
                    for r in raw_results:
                        title = r.get("title", "")
                        url = r.get("href", "")
                        body = r.get("body", "")
                        if title and url:
                            results.append(f"Title: {title}\nURL: {url}\nSnippet: {body}")
                log(f" Fallback 결과: {len(results)}개")
        except Exception as e:
            log(f" Fallback 오류: {str(e)}")

    # 7 최종 결과 반환
    if not results:
        log(" 최종 결과 없음")
        return "Search completed but no results were returned."
    
    log(f" 검색 완료: {len(results)}개 반환")
    return "\n\n---\n\n".join(results)

if __name__ == "__main__":
    mcp.run(transport="stdio")
profile
Hello, I'm Terry! 👋 Enjoy every moment of your life! 🌱 My current interests are Signal processing, Machine learning, Python, Database, LLM & RAG, MCP & ADK, Multi-Agents, Physical AI, ROS2...

0개의 댓글