├── src ├── schemas │ ├── time.schemas.ts │ ├── category.schemas.ts │ ├── search.schemas.ts │ └── datalab.schemas.ts ├── types │ ├── time.types.ts │ ├── global.d.ts │ ├── search.types.ts │ └── datalab.types.ts ├── handlers │ ├── time.handlers.ts │ ├── search.handlers.ts │ ├── datalab.handlers.ts │ └── category.handlers.ts ├── clients │ ├── naver-api-core.client.ts │ └── naver-search.client.ts ├── utils │ └── memory-manager.ts └── index.ts ├── data └── README.md ├── tsconfig.json ├── .gitignore ├── Dockerfile ├── package.json ├── LICENSE ├── smithery.yaml ├── README-ko.md └── README.md /src/schemas/time.schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | /** 4 | * Schema for Korean time tool - no parameters needed 5 | */ 6 | export const GetKoreanTimeSchema = z.object({ 7 | // No parameters required for getting current time 8 | }); 9 | 10 | export type GetKoreanTimeArgs = z.infer; -------------------------------------------------------------------------------- /src/types/time.types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Korean time response interface 3 | */ 4 | export interface KoreanTimeResponse { 5 | current_korean_time: { 6 | iso_string: string; 7 | korean_date: string; 8 | korean_time: string; 9 | formatted: string; 10 | timezone: string; 11 | timestamp: number; 12 | }; 13 | } -------------------------------------------------------------------------------- /src/schemas/category.schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | // Find category schema - Simple and powerful! 4 | export const FindCategorySchema = z.object({ 5 | query: z.string().describe("Korean category search query (한국어 카테고리 검색어, 예: '패션', '화장품', '가구', '스마트폰'). Supports exact match and fuzzy search with level-based priority"), 6 | max_results: z.number().optional().default(10).describe("Maximum number of results to return (반환할 최대 결과 수, 기본값: 10)") 7 | }); -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | # 카테고리 데이터 폴더 2 | 3 | 이 폴더에 `categories.xlsx` 파일을 배치하세요. 4 | 5 | ## 파일 구조 6 | 7 | ``` 8 | data/ 9 | └── categories.xlsx 10 | ``` 11 | 12 | ## Excel 파일 형식 13 | 14 | | 카테고리번호 | 대분류 | 중분류 | 소분류 | 세분류 | 15 | |-------------|--------|--------|--------|--------| 16 | | 50003307 | 가구/인테리어 | DIY자재/용품 | 가구부속품 | 가구다리 | 17 | 18 | - 첫 번째 행(헤더)은 자동으로 건너뜁니다 19 | - 카테고리번호는 필수입니다 20 | - 대분류는 필수, 나머지 분류는 선택사항입니다 21 | 22 | ## 사용법 23 | 24 | 1. `categories.xlsx` 파일을 이 폴더에 배치 25 | 2. MCP 서버 시작시 자동으로 로드됨 26 | 3. 파일 수정시 자동으로 캐시 갱신됨 -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./dist", 7 | "rootDir": ".", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "allowSyntheticDefaultImports": true, 14 | "declaration": true, 15 | "noImplicitAny": false 16 | }, 17 | "include": ["./**/*.ts"], 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | .pnpm-debug.log* 7 | 8 | # Build output 9 | build/ 10 | dist/ 11 | 12 | # Environment variables 13 | .env 14 | .env.local 15 | .env.*.local 16 | .env.test 17 | 18 | # IDE 19 | .vscode/ 20 | .idea/ 21 | *.swp 22 | *.swo 23 | 24 | # OS 25 | .DS_Store 26 | Thumbs.db 27 | 28 | # Logs 29 | logs/ 30 | *.log 31 | 32 | # Test coverage 33 | coverage/ 34 | 35 | # Temporary files 36 | *.tmp 37 | *.temp 38 | 39 | # Claude Code configuration 40 | .claude/ 41 | 42 | # Reference folders 43 | ref/ 44 | ef/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:lts-alpine 3 | 4 | # Create app directory 5 | WORKDIR /usr/src/app 6 | 7 | # Copy package.json and package-lock.json 8 | COPY package*.json ./ 9 | 10 | # Install dependencies without triggering any unwanted scripts 11 | RUN npm install --ignore-scripts 12 | 13 | # Copy all source code 14 | COPY . . 15 | 16 | # Build the application 17 | RUN npm run build 18 | 19 | # Set production environment 20 | ENV NODE_ENV=production 21 | 22 | # Command to run the server 23 | CMD [ "node", "dist/src/index.js" ] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@isnow890/naver-search-mcp", 3 | "version": "1.0.46", 4 | "description": "Naver Search MCP Server", 5 | "main": "dist/src/index.js", 6 | "module": "./src/index.ts", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "prebuild": "npx shx rm -rf dist", 13 | "build": "tsc && shx cp -r data dist/", 14 | "prepare": "npm run build", 15 | "watch": "tsc --watch", 16 | "start": "node dist/src/index.js" 17 | }, 18 | "engines": { 19 | "node": ">=18" 20 | }, 21 | "dependencies": { 22 | "@modelcontextprotocol/sdk": "1.17.1", 23 | "@smithery/sdk": "^1.6.6", 24 | "axios": "^1.6.7", 25 | "zod": "^3.23.0", 26 | "zod-to-json-schema": "^3.23.0", 27 | "sharp": "^0.33.2", 28 | "dotenv": "^16.4.5", 29 | "xlsx": "0.18.5" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^22", 33 | "shx": "^0.3.4", 34 | "typescript": "^5.3.3" 35 | }, 36 | "author": "isnow890", 37 | "license": "MIT", 38 | "bin": { 39 | "naver-search-mcp": "dist/src/index.js" 40 | }, 41 | "type": "module" 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 isnow890 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // Global type declarations for Node.js environment 2 | declare global { 3 | namespace NodeJS { 4 | interface Process { 5 | argv: string[]; 6 | env: { [key: string]: string | undefined }; 7 | cwd(): string; 8 | memoryUsage(): { 9 | rss: number; 10 | heapTotal: number; 11 | heapUsed: number; 12 | external: number; 13 | arrayBuffers: number; 14 | }; 15 | exit(code?: number): never; 16 | } 17 | 18 | interface Global { 19 | gc?: () => void; 20 | } 21 | } 22 | 23 | var process: NodeJS.Process; 24 | var global: NodeJS.Global; 25 | var console: { 26 | log(...args: any[]): void; 27 | error(...args: any[]): void; 28 | warn(...args: any[]): void; 29 | info(...args: any[]): void; 30 | }; 31 | 32 | function setTimeout(callback: (...args: any[]) => void, ms: number): any; 33 | function setInterval(callback: (...args: any[]) => void, ms: number): any; 34 | function clearInterval(id: any): void; 35 | function clearTimeout(id: any): void; 36 | 37 | interface ImportMeta { 38 | url: string; 39 | } 40 | } 41 | 42 | declare module "fs" { 43 | export function existsSync(path: string): boolean; 44 | export function readFileSync(path: string, encoding?: string): string | Buffer; 45 | } 46 | 47 | declare module "path" { 48 | export function join(...paths: string[]): string; 49 | } 50 | 51 | export {}; -------------------------------------------------------------------------------- /src/handlers/time.handlers.ts: -------------------------------------------------------------------------------- 1 | import { GetKoreanTimeArgs } from "../schemas/time.schemas.js"; 2 | import { KoreanTimeResponse } from "../types/time.types.js"; 3 | 4 | /** 5 | * Get current Korean time (KST/UTC+9) 6 | * @param args - No arguments required 7 | * @returns Current Korean time in multiple formats 8 | */ 9 | export async function getCurrentKoreanTime( 10 | args: GetKoreanTimeArgs 11 | ): Promise { 12 | const now = new Date(); 13 | const koreanTime = new Date(now.toLocaleString("en-US", {timeZone: "Asia/Seoul"})); 14 | 15 | return { 16 | current_korean_time: { 17 | iso_string: koreanTime.toISOString(), 18 | korean_date: koreanTime.toLocaleDateString("ko-KR", { 19 | year: "numeric", 20 | month: "long", 21 | day: "numeric", 22 | weekday: "long" 23 | }), 24 | korean_time: koreanTime.toLocaleTimeString("ko-KR", { 25 | hour: "2-digit", 26 | minute: "2-digit", 27 | second: "2-digit", 28 | hour12: false 29 | }), 30 | formatted: `${koreanTime.getFullYear()}년 ${koreanTime.getMonth() + 1}월 ${koreanTime.getDate()}일 ${koreanTime.toLocaleDateString("ko-KR", {weekday: "long"})} ${koreanTime.toLocaleTimeString("ko-KR", {hour12: false})}`, 31 | timezone: "Asia/Seoul (KST)", 32 | timestamp: koreanTime.getTime() 33 | } 34 | }; 35 | } 36 | 37 | export const timeToolHandlers = { 38 | getCurrentKoreanTime, 39 | }; -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | name: naver-search 2 | version: 1.0.30 3 | description: | 4 | [KOR] 5 | 네이버 검색 API를 활용한 MCP 서버입니다. 뉴스, 블로그, 쇼핑, 이미지, 지식iN, 책, 백과사전, 웹문서, 카페글 등 다양한 컨텐츠를 검색할 수 있으며, 데이터랩 API를 통해 검색어 트렌드와 쇼핑 인사이트를 분석할 수 있습니다. 검색 결과는 유사도 또는 날짜 기준으로 정렬이 가능하며, 페이지네이션을 지원합니다. 6 | 7 | [ENG] 8 | An MCP server leveraging the Naver Search API and Datalab API capabilities. Search across diverse content types including news, blogs, shopping items, images, Knowledge-iN, books, encyclopedia, web documents, and cafe articles. Analyze search trends and shopping insights through Datalab API. Features include relevance/date-based sorting and pagination support. 9 | 10 | 주요 기능: 11 | - 통합 검색: 모든 타입의 컨텐츠를 한 번에 검색 12 | - 카테고리별 검색: 뉴스, 블로그, 쇼핑, 이미지, 지식iN, 책, 백과사전, 웹문서, 카페글 등 개별 검색 13 | - 검색 결과 정렬: 유사도순 또는 날짜순 정렬 지원 14 | - 페이지네이션: 검색 결과 페이징 처리 지원 15 | - 데이터랩 분석: 16 | * 검색어 트렌드 분석 17 | * 쇼핑 카테고리별 트렌드 18 | * 디바이스별 쇼핑 트렌드 (PC/모바일) 19 | * 성별 쇼핑 트렌드 20 | * 연령대별 쇼핑 트렌드 21 | * 쇼핑 키워드 트렌드 22 | 23 | author: isnow890 24 | repository: https://github.com/isnow890/naver-search-mcp 25 | license: MIT 26 | 27 | # Smithery.ai configuration 28 | runtime: "typescript" 29 | 30 | startCommand: 31 | type: http 32 | configSchema: 33 | type: object 34 | required: ["NAVER_CLIENT_ID", "NAVER_CLIENT_SECRET"] 35 | properties: 36 | NAVER_CLIENT_ID: 37 | type: string 38 | title: "Naver API Client ID" 39 | description: "Naver API Client ID issued from Naver Developers Center" 40 | NAVER_CLIENT_SECRET: 41 | type: string 42 | title: "Naver API Client Secret" 43 | description: "Naver API Client Secret issued from Naver Developers Center" -------------------------------------------------------------------------------- /src/types/search.types.ts: -------------------------------------------------------------------------------- 1 | // 네이버 검색 API 공통 응답 타입 2 | export interface NaverSearchResponse { 3 | lastBuildDate: string; // 검색 결과 생성 시각 (RFC822) 4 | total: number; // 전체 검색 결과 수 5 | start: number; // 검색 시작 위치 6 | display: number; // 한 번에 표시할 검색 결과 수 7 | items: NaverSearchItem[]; // 검색 결과 아이템 목록 8 | isError?: boolean; // 에러 여부(선택) 9 | } 10 | 11 | // 네이버 검색 API 공통 아이템 타입 12 | export interface NaverSearchItem { 13 | title: string; // 아이템 제목 (검색어는 태그로 강조) 14 | link: string; // 아이템 URL 15 | description: string; // 아이템 설명 (검색어는 태그로 강조) 16 | } 17 | 18 | // 전문자료 검색 응답 타입 19 | export interface NaverDocumentSearchResponse extends NaverSearchResponse { 20 | items: NaverDocumentItem[]; // 전문자료 아이템 목록 21 | } 22 | 23 | // 전문자료 아이템 타입 24 | export interface NaverDocumentItem extends NaverSearchItem { 25 | title: string; // 문서 제목 ( 태그 강조) 26 | link: string; // 문서 URL 27 | description: string; // 문서 요약 ( 태그 강조) 28 | } 29 | 30 | // 지식백과 검색 응답 타입 31 | export interface NaverEncyclopediaSearchResponse extends NaverSearchResponse { 32 | items: NaverEncyclopediaItem[]; // 지식백과 아이템 목록 33 | } 34 | 35 | // 지식백과 아이템 타입 36 | export interface NaverEncyclopediaItem extends NaverSearchItem { 37 | title: string; // 백과사전 제목 ( 태그 강조) 38 | link: string; // 백과사전 문서 URL 39 | description: string; // 백과사전 요약 ( 태그 강조) 40 | thumbnail: string; // 썸네일 이미지 URL 41 | } 42 | 43 | // 지역 검색 응답 타입 44 | export interface NaverLocalSearchResponse extends NaverSearchResponse { 45 | items: NaverLocalItem[]; // 지역 아이템 목록 46 | } 47 | 48 | // 지역 아이템 타입 49 | export interface NaverLocalItem extends Omit { 50 | title: string; // 업체/기관명 51 | link: string; // 상세 정보 URL 52 | category: string; // 분류 정보 53 | description: string; // 설명 54 | telephone: string; // 전화번호(반환 안함) 55 | address: string; // 지번 주소 56 | roadAddress: string; // 도로명 주소 57 | mapx: number; // x좌표(KATECH) 58 | mapy: number; // y좌표(KATECH) 59 | } 60 | -------------------------------------------------------------------------------- /src/clients/naver-api-core.client.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosInstance } from "axios"; 2 | import { NaverSearchConfig } from "../schemas/search.schemas.js"; 3 | 4 | export abstract class NaverApiCoreClient { 5 | protected searchBaseUrl = "https://openapi.naver.com/v1/search"; 6 | protected datalabBaseUrl = "https://openapi.naver.com/v1/datalab"; 7 | protected config: NaverSearchConfig | null = null; 8 | protected axiosInstance: AxiosInstance; 9 | 10 | constructor() { 11 | // HTTP/HTTPS 에이전트 설정으로 연결 풀링 및 메모리 누수 방지 12 | this.axiosInstance = axios.create({ 13 | timeout: 30000, // 30초 타임아웃 14 | maxRedirects: 3, 15 | // HTTP 연결 풀링 설정은 운영체제에서 처리하도록 단순화 16 | }); 17 | } 18 | 19 | initialize(config: NaverSearchConfig) { 20 | this.config = config; 21 | } 22 | 23 | protected getHeaders( 24 | contentType: string = "application/json" 25 | ): AxiosRequestConfig { 26 | if (!this.config) throw new Error("NaverApiCoreClient is not initialized."); 27 | return { 28 | headers: { 29 | "X-Naver-Client-Id": this.config.clientId, 30 | "X-Naver-Client-Secret": this.config.clientSecret, 31 | "Content-Type": contentType, 32 | }, 33 | }; 34 | } 35 | 36 | protected async get(url: string, params: any): Promise { 37 | try { 38 | const response = await this.axiosInstance.get(url, { 39 | params, 40 | ...this.getHeaders() 41 | }); 42 | return response.data; 43 | } catch (error) { 44 | // 연결 정리는 axios가 자동으로 처리하지만 명시적으로 에러 처리 45 | throw error; 46 | } 47 | } 48 | 49 | protected async post(url: string, data: any): Promise { 50 | try { 51 | const response = await this.axiosInstance.post(url, data, this.getHeaders()); 52 | return response.data; 53 | } catch (error) { 54 | // 연결 정리는 axios가 자동으로 처리하지만 명시적으로 에러 처리 55 | throw error; 56 | } 57 | } 58 | 59 | /** 60 | * 리소스 정리 메서드 (메모리 누수 방지) 61 | */ 62 | protected cleanup(): void { 63 | // 연결 정리 - axios가 자동으로 처리 64 | this.config = null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/schemas/search.schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | // 네이버 검색 타입(카테고리) 3 | export const NaverSearchTypeSchema = z.enum([ 4 | "news", 5 | "encyc", 6 | "blog", 7 | "shop", 8 | "webkr", 9 | "image", 10 | "doc", 11 | "kin", 12 | "book", 13 | "cafearticle", 14 | "local", 15 | ]); 16 | 17 | export const NaverSearchParamsSchema = z.object({ 18 | query: z.string().describe("Search query"), 19 | display: z 20 | .number() 21 | .optional() 22 | .describe("Number of results to display (default: 10)"), 23 | start: z 24 | .number() 25 | .optional() 26 | .describe("Start position of search results (default: 1)"), 27 | sort: z 28 | .enum(["sim", "date"]) 29 | .optional() 30 | .describe("Sort method (sim: similarity, date: chronological)"), 31 | }); 32 | // 네이버 검색 공통 파라미터 33 | export const SearchArgsSchema = z.object({ 34 | query: z.string().describe("검색어"), 35 | display: z.number().optional().describe("한 번에 가져올 결과 수 (기본 10)"), 36 | start: z.number().optional().describe("검색 시작 위치 (기본 1)"), 37 | sort: z 38 | .enum(["sim", "date"]) 39 | .optional() 40 | .describe("정렬 방식 (sim: 유사도, date: 날짜순)"), 41 | }); 42 | 43 | // 네이버 API 인증 정보 44 | export const NaverSearchConfigSchema = z.object({ 45 | clientId: z.string().describe("네이버 개발자센터에서 발급받은 Client ID"), 46 | clientSecret: z 47 | .string() 48 | .describe("네이버 개발자센터에서 발급받은 Client Secret"), 49 | }); 50 | 51 | // 전문자료(논문 등) 검색 파라미터 52 | export const NaverDocumentSearchParamsSchema = z.object({ 53 | query: z.string().describe("검색어"), 54 | display: z.number().optional().describe("한 번에 가져올 결과 수 (최대 100)"), 55 | start: z.number().optional().describe("검색 시작 위치 (최대 1000)"), 56 | }); 57 | 58 | // 지역 검색 파라미터 59 | export const NaverLocalSearchParamsSchema = SearchArgsSchema.extend({ 60 | sort: z 61 | .enum(["random", "comment"]) 62 | .optional() 63 | .describe("정렬 방식 (random: 정확도순, comment: 리뷰 많은순)"), 64 | display: z.number().optional().describe("한 번에 가져올 결과 수 (최대 5)"), 65 | start: z.number().optional().describe("검색 시작 위치 (최대 1)"), 66 | }); 67 | export type NaverLocalSearchParams = z.infer< 68 | typeof NaverLocalSearchParamsSchema 69 | >; 70 | export type NaverSearchParams = z.infer; 71 | 72 | export type SearchArgs = z.infer; 73 | export type NaverSearchType = z.infer; 74 | export type NaverSearchConfig = z.infer; 75 | export type NaverDocumentSearchParams = z.infer< 76 | typeof NaverDocumentSearchParamsSchema 77 | >; 78 | -------------------------------------------------------------------------------- /src/types/datalab.types.ts: -------------------------------------------------------------------------------- 1 | // 데이터랩 분석 단위 타입 2 | export type DatalabTimeUnit = "date" | "week" | "month"; 3 | 4 | // 데이터랩 검색 요청 타입 5 | export interface DatalabSearchRequest { 6 | startDate: string; // 분석 시작일 (yyyy-mm-dd) 7 | endDate: string; // 분석 종료일 (yyyy-mm-dd) 8 | timeUnit: DatalabTimeUnit; // 분석 단위 9 | keywordGroups: Array<{ 10 | groupName: string; // 키워드 그룹명 11 | keywords: string[]; // 그룹 내 키워드 목록 12 | }>; 13 | } 14 | //checkpoint 15 | // 데이터랩 쇼핑 응답 타입 16 | export interface DatalabShoppingResponse { 17 | startDate: string; // 분석 시작일 18 | endDate: string; // 분석 종료일 19 | timeUnit: string; // 분석 단위 20 | results: { 21 | title: string; // 결과 제목 22 | category?: string[]; // 카테고리 정보 23 | keyword?: string[]; // 키워드 정보 24 | data: { 25 | period: string; // 기간 26 | group?: string; // 그룹 정보 27 | ratio: number; // 비율 값 28 | }[]; 29 | }[]; 30 | } 31 | 32 | // 데이터랩 쇼핑 카테고리 요청 타입 33 | export interface DatalabShoppingCategoryRequest { 34 | startDate: string; // 분석 시작일 (yyyy-mm-dd) 35 | endDate: string; // 분석 종료일 (yyyy-mm-dd) 36 | timeUnit: DatalabTimeUnit; // 분석 단위 37 | category: Array<{ 38 | name: string; // 카테고리명 39 | param: string[]; // 카테고리 파라미터 40 | }>; 41 | device?: "pc" | "mo"; // 기기 구분 (PC/모바일) 42 | gender?: "f" | "m"; // 성별 43 | ages?: string[]; // 연령대 44 | } 45 | 46 | // 데이터랩 쇼핑 기기별 요청 타입 47 | export interface DatalabShoppingDeviceRequest { 48 | startDate: string; // 분석 시작일 49 | endDate: string; // 분석 종료일 50 | timeUnit: DatalabTimeUnit; // 분석 단위 51 | category: string; // 카테고리 코드 52 | device: "pc" | "mo"; // 기기 구분 53 | } 54 | 55 | // 데이터랩 쇼핑 성별 요청 타입 56 | export interface DatalabShoppingGenderRequest { 57 | startDate: string; // 분석 시작일 58 | endDate: string; // 분석 종료일 59 | timeUnit: DatalabTimeUnit; // 분석 단위 60 | category: string; // 카테고리 코드 61 | gender: "f" | "m"; // 성별 62 | } 63 | 64 | // 데이터랩 쇼핑 연령별 요청 타입 65 | export interface DatalabShoppingAgeRequest { 66 | startDate: string; // 분석 시작일 67 | endDate: string; // 분석 종료일 68 | timeUnit: DatalabTimeUnit; // 분석 단위 69 | category: string; // 카테고리 코드 70 | ages: string[]; // 연령대 71 | } 72 | 73 | // 데이터랩 쇼핑 키워드 그룹 요청 타입 74 | export interface DatalabShoppingKeywordsRequest { 75 | startDate: string; // 분석 시작일 76 | endDate: string; // 분석 종료일 77 | timeUnit: DatalabTimeUnit; // 분석 단위 78 | category: string; // 카테고리 코드 79 | keyword: Array<{ 80 | name: string; // 키워드명 81 | param: string[]; // 키워드 파라미터 82 | }>; 83 | } 84 | 85 | // 데이터랩 쇼핑 키워드 단일 요청 타입 86 | export interface DatalabShoppingKeywordRequest { 87 | startDate: string; // 분석 시작일 88 | endDate: string; // 분석 종료일 89 | timeUnit: DatalabTimeUnit; // 분석 단위 90 | category: string; // 카테고리 코드 91 | keyword: string; // 검색 키워드 92 | device?: "pc" | "mo"; // 기기 구분 93 | gender?: "f" | "m"; // 성별 94 | ages?: string[]; // 연령대 95 | } 96 | -------------------------------------------------------------------------------- /src/handlers/search.handlers.ts: -------------------------------------------------------------------------------- 1 | import { NaverSearchClient } from "../clients/naver-search.client.js"; 2 | import { NaverLocalSearchParams } from "../schemas/search.schemas.js"; 3 | 4 | import { SearchArgs } from "../schemas/search.schemas.js"; 5 | import { SearchArgsSchema } from "../schemas/search.schemas.js"; 6 | 7 | // 클라이언트 인스턴스 8 | const client = NaverSearchClient.getInstance(); 9 | 10 | export const searchToolHandlers: Record Promise> = { 11 | search_webkr: (args) => { 12 | console.error("search_webkr called with args:", JSON.stringify(args, null, 2)); 13 | return handleWebKrSearch(SearchArgsSchema.parse(args)); 14 | }, 15 | search_news: (args) => { 16 | console.error("search_news called with args:", JSON.stringify(args, null, 2)); 17 | return handleNewsSearch(SearchArgsSchema.parse(args)); 18 | }, 19 | search_blog: (args) => { 20 | console.error("search_blog called with args:", JSON.stringify(args, null, 2)); 21 | return handleBlogSearch(SearchArgsSchema.parse(args)); 22 | }, 23 | search_shop: (args) => { 24 | console.error("search_shop called with args:", JSON.stringify(args, null, 2)); 25 | return handleShopSearch(SearchArgsSchema.parse(args)); 26 | }, 27 | search_image: (args) => { 28 | console.error("search_image called with args:", JSON.stringify(args, null, 2)); 29 | return handleImageSearch(SearchArgsSchema.parse(args)); 30 | }, 31 | search_kin: (args) => { 32 | console.error("search_kin called with args:", JSON.stringify(args, null, 2)); 33 | return handleKinSearch(SearchArgsSchema.parse(args)); 34 | }, 35 | search_book: (args) => { 36 | console.error("search_book called with args:", JSON.stringify(args, null, 2)); 37 | return handleBookSearch(SearchArgsSchema.parse(args)); 38 | }, 39 | search_encyc: (args) => { 40 | console.error("search_encyc called with args:", JSON.stringify(args, null, 2)); 41 | return handleEncycSearch(SearchArgsSchema.parse(args)); 42 | }, 43 | search_academic: (args) => { 44 | console.error("search_academic called with args:", JSON.stringify(args, null, 2)); 45 | return handleAcademicSearch(SearchArgsSchema.parse(args)); 46 | }, 47 | search_local: (args) => { 48 | console.error("search_local called with args:", JSON.stringify(args, null, 2)); 49 | return handleLocalSearch(args); 50 | }, 51 | search_cafearticle: (args) => { 52 | console.error("search_cafearticle called with args:", JSON.stringify(args, null, 2)); 53 | return handleCafeArticleSearch(SearchArgsSchema.parse(args)); 54 | }, 55 | }; 56 | 57 | /** 58 | * 전문자료 검색 핸들러 59 | */ 60 | export async function handleAcademicSearch(params: SearchArgs) { 61 | return client.searchAcademic(params); 62 | } 63 | 64 | /** 65 | * 도서 검색 핸들러 66 | */ 67 | export async function handleBookSearch(params: SearchArgs) { 68 | return client.search("book", params); 69 | } 70 | 71 | /** 72 | * 지식백과 검색 핸들러 73 | */ 74 | export async function handleEncycSearch(params: SearchArgs) { 75 | return client.search("encyc", params); 76 | } 77 | 78 | /** 79 | * 이미지 검색 핸들러 80 | */ 81 | export async function handleImageSearch(params: SearchArgs) { 82 | return client.search("image", params); 83 | } 84 | 85 | /** 86 | * 지식iN 검색 핸들러 87 | */ 88 | export async function handleKinSearch(params: SearchArgs) { 89 | return client.search("kin", params); 90 | } 91 | 92 | /** 93 | * 지역 검색 핸들러 94 | */ 95 | export async function handleLocalSearch(params: NaverLocalSearchParams) { 96 | return client.searchLocal(params); 97 | } 98 | 99 | /** 100 | * 뉴스 검색 핸들러 101 | */ 102 | export async function handleNewsSearch(params: SearchArgs) { 103 | return client.search("news", params); 104 | } 105 | 106 | /** 107 | * 블로그 검색 핸들러 108 | */ 109 | export async function handleBlogSearch(params: SearchArgs) { 110 | return client.search("blog", params); 111 | } 112 | 113 | /** 114 | * 쇼핑 검색 핸들러 115 | */ 116 | export async function handleShopSearch(params: SearchArgs) { 117 | return client.search("shop", params); 118 | } 119 | 120 | /** 121 | * 카페글 검색 핸들러 122 | */ 123 | export async function handleCafeArticleSearch(params: SearchArgs) { 124 | return client.search("cafearticle", params); 125 | } 126 | 127 | export async function handleWebKrSearch(args: SearchArgs) { 128 | const client = NaverSearchClient.getInstance(); 129 | return await client.search("webkr", args); 130 | } 131 | -------------------------------------------------------------------------------- /src/schemas/datalab.schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | // 기본 DataLab 스키마 4 | export const DatalabBaseSchema = z.object({ 5 | startDate: z.string().describe("Start date (yyyy-mm-dd)"), 6 | endDate: z.string().describe("End date (yyyy-mm-dd)"), 7 | timeUnit: z.enum(["date", "week", "month"]).describe("Time unit"), 8 | }); 9 | 10 | // 검색어 트렌드 스키마 11 | export const DatalabSearchSchema = DatalabBaseSchema.extend({ 12 | keywordGroups: z 13 | .array( 14 | z.object({ 15 | groupName: z.string().describe("Group name"), 16 | keywords: z.array(z.string()).describe("List of keywords"), 17 | }) 18 | ) 19 | .describe("Keyword groups"), 20 | }); 21 | 22 | // 쇼핑 카테고리 스키마 23 | export const DatalabShoppingSchema = DatalabBaseSchema.extend({ 24 | category: z 25 | .array( 26 | z.object({ 27 | name: z.string().describe("Category name"), 28 | param: z.array(z.string()).describe("Category codes"), 29 | }) 30 | ) 31 | .describe("Array of category name and code pairs"), 32 | }); 33 | 34 | // 기기별 트렌드 스키마 35 | export const DatalabShoppingDeviceSchema = DatalabBaseSchema.extend({ 36 | category: z.string().describe("Category code"), 37 | device: z.enum(["pc", "mo"]).describe("Device type"), 38 | }); 39 | 40 | // 성별 트렌드 스키마 41 | export const DatalabShoppingGenderSchema = DatalabBaseSchema.extend({ 42 | category: z.string().describe("Category code"), 43 | gender: z.enum(["f", "m"]).describe("Gender"), 44 | }); 45 | 46 | // 연령별 트렌드 스키마 47 | export const DatalabShoppingAgeSchema = DatalabBaseSchema.extend({ 48 | category: z.string().describe("Category code"), 49 | ages: z 50 | .array(z.enum(["10", "20", "30", "40", "50", "60"])) 51 | .describe("Age groups"), 52 | }); 53 | 54 | // 키워드 트렌드 스키마 55 | export const DatalabShoppingKeywordsSchema = DatalabBaseSchema.extend({ 56 | category: z.string().describe("Category code"), 57 | keyword: z 58 | .array( 59 | z.object({ 60 | name: z.string().describe("Keyword name"), 61 | param: z.array(z.string()).describe("Keyword values"), 62 | }) 63 | ) 64 | .describe("Array of keyword name and value pairs"), 65 | }); 66 | 67 | // 키워드 기기별 트렌드 스키마 68 | export const DatalabShoppingKeywordDeviceSchema = DatalabBaseSchema.extend({ 69 | category: z.string().describe("Category code"), 70 | keyword: z.string().describe("Search keyword"), 71 | device: z.enum(["pc", "mo"]).describe("Device type"), 72 | }); 73 | 74 | // 키워드 성별 트렌드 스키마 75 | export const DatalabShoppingKeywordGenderSchema = DatalabBaseSchema.extend({ 76 | category: z.string().describe("Category code"), 77 | keyword: z.string().describe("Search keyword"), 78 | gender: z.enum(["f", "m"]).describe("Gender"), 79 | }); 80 | 81 | // 키워드 연령별 트렌드 스키마 82 | export const DatalabShoppingKeywordAgeSchema = DatalabBaseSchema.extend({ 83 | category: z.string().describe("Category code"), 84 | keyword: z.string().describe("Search keyword"), 85 | ages: z 86 | .array(z.enum(["10", "20", "30", "40", "50", "60"])) 87 | .describe("Age groups"), 88 | }); 89 | 90 | // 카테고리 디바이스/성별/연령별 트렌드 스키마 91 | export const DatalabShoppingCategoryDeviceSchema = DatalabBaseSchema.extend({ 92 | category: z 93 | .array( 94 | z.object({ 95 | name: z.string().describe("Category name"), 96 | param: z.array(z.string()).describe("Category codes"), 97 | }) 98 | ) 99 | .describe("Array of category name and code pairs"), 100 | device: z.enum(["pc", "mo"]).optional().describe("Device type"), 101 | gender: z.enum(["f", "m"]).optional().describe("Gender"), 102 | ages: z 103 | .array(z.enum(["10", "20", "30", "40", "50", "60"])) 104 | .optional() 105 | .describe("Age groups"), 106 | }); 107 | 108 | // 키워드 디바이스/성별/연령별 트렌드 스키마 109 | export const DatalabShoppingKeywordTrendSchema = DatalabBaseSchema.extend({ 110 | category: z.string().describe("Category code"), 111 | keyword: z 112 | .array( 113 | z.object({ 114 | name: z.string().describe("Keyword name"), 115 | param: z.array(z.string()).describe("Keyword values"), 116 | }) 117 | ) 118 | .describe("Array of keyword name and value pairs"), 119 | device: z.enum(["pc", "mo"]).optional().describe("Device type"), 120 | gender: z.enum(["f", "m"]).optional().describe("Gender"), 121 | ages: z 122 | .array(z.enum(["10", "20", "30", "40", "50", "60"])) 123 | .optional() 124 | .describe("Age groups"), 125 | }); 126 | 127 | export type DatalabSearch = z.infer; 128 | export type DatalabShopping = z.infer; 129 | export type DatalabShoppingDevice = z.infer; 130 | export type DatalabShoppingGender = z.infer; 131 | export type DatalabShoppingAge = z.infer; 132 | export type DatalabShoppingKeywords = z.infer; 133 | export type DatalabShoppingKeywordDevice = z.infer; 134 | export type DatalabShoppingKeywordGender = z.infer; 135 | export type DatalabShoppingKeywordAge = z.infer; -------------------------------------------------------------------------------- /src/clients/naver-search.client.ts: -------------------------------------------------------------------------------- 1 | import { NaverApiCoreClient } from "./naver-api-core.client.js"; 2 | import { 3 | NaverSearchType, 4 | NaverSearchConfig, 5 | NaverLocalSearchParams, 6 | NaverDocumentSearchParams, 7 | SearchArgs, 8 | } from "../schemas/search.schemas.js"; 9 | import { 10 | NaverDocumentSearchResponse, 11 | NaverLocalSearchResponse, 12 | } from "../types/search.types.js"; 13 | import { 14 | DatalabSearchRequest, 15 | DatalabShoppingResponse, 16 | DatalabShoppingCategoryRequest, 17 | DatalabShoppingDeviceRequest, 18 | DatalabShoppingGenderRequest, 19 | DatalabShoppingAgeRequest, 20 | DatalabShoppingKeywordsRequest, 21 | DatalabShoppingKeywordRequest, 22 | } from "../types/datalab.types.js"; 23 | 24 | /** 25 | * NaverSearchClient - 네이버 API 서비스를 위한 싱글톤 클라이언트 26 | * 검색, 데이터랩 API 요청 처리 27 | */ 28 | export class NaverSearchClient extends NaverApiCoreClient { 29 | private static instance: NaverSearchClient | null = null; 30 | 31 | private constructor() { 32 | super(); 33 | } 34 | 35 | /** 36 | * 싱글톤 인스턴스 반환 37 | */ 38 | static getInstance(): NaverSearchClient { 39 | if (!NaverSearchClient.instance) { 40 | NaverSearchClient.instance = new NaverSearchClient(); 41 | } 42 | return NaverSearchClient.instance; 43 | } 44 | 45 | /** 46 | * 싱글톤 인스턴스 정리 (메모리 누수 방지) 47 | */ 48 | static destroyInstance(): void { 49 | if (NaverSearchClient.instance) { 50 | NaverSearchClient.instance.cleanup(); 51 | NaverSearchClient.instance = null; 52 | } 53 | } 54 | 55 | /** 56 | * 인스턴스 리소스 정리 57 | */ 58 | protected cleanup(): void { 59 | this.config = null as any; 60 | // HTTP 연결 정리는 부모 클래스에서 처리 61 | super.cleanup(); 62 | } 63 | 64 | /** 65 | * API 자격 증명으로 클라이언트 초기화 66 | */ 67 | initialize(config: NaverSearchConfig) { 68 | this.config = config; 69 | } 70 | 71 | /** 72 | * 네이버 검색 API 호출 (type별로) 73 | * @param type 검색 타입(카테고리) 74 | * @param params 검색 파라미터 75 | */ 76 | async search(type: NaverSearchType, params: SearchArgs): Promise { 77 | return this.get(`${this.searchBaseUrl}/${type}`, params); 78 | } 79 | 80 | /** 81 | * 전문자료 검색 메서드 82 | */ 83 | async searchAcademic( 84 | params: NaverDocumentSearchParams 85 | ): Promise { 86 | return this.get(`${this.searchBaseUrl}/doc`, params); 87 | } 88 | 89 | /** 90 | * 지역 검색 메서드 91 | */ 92 | async searchLocal( 93 | params: NaverLocalSearchParams 94 | ): Promise { 95 | return this.get(`${this.searchBaseUrl}/local`, params); 96 | } 97 | 98 | /** 99 | * 검색어 트렌드 분석 메서드 100 | */ 101 | async searchTrend(params: DatalabSearchRequest): Promise { 102 | return this.post(`${this.datalabBaseUrl}/search`, params); 103 | } 104 | 105 | /** 106 | * 쇼핑 카테고리 트렌드 분석 메서드 107 | */ 108 | async datalabShoppingCategory( 109 | params: DatalabShoppingCategoryRequest 110 | ): Promise { 111 | return this.post(`${this.datalabBaseUrl}/shopping/categories`, params); 112 | } 113 | 114 | /** 115 | * 쇼핑 기기별 트렌드 분석 메서드 116 | */ 117 | async datalabShoppingByDevice( 118 | params: DatalabShoppingDeviceRequest 119 | ): Promise { 120 | return this.post(`${this.datalabBaseUrl}/shopping/category/device`, params); 121 | } 122 | 123 | /** 124 | * 쇼핑 성별 트렌드 분석 메서드 125 | */ 126 | async datalabShoppingByGender( 127 | params: DatalabShoppingGenderRequest 128 | ): Promise { 129 | return this.post(`${this.datalabBaseUrl}/shopping/category/gender`, params); 130 | } 131 | 132 | /** 133 | * 쇼핑 연령별 트렌드 분석 메서드 134 | */ 135 | async datalabShoppingByAge( 136 | params: DatalabShoppingAgeRequest 137 | ): Promise { 138 | return this.post(`${this.datalabBaseUrl}/shopping/category/age`, params); 139 | } 140 | 141 | /** 142 | * 쇼핑 키워드 트렌드 분석 메서드 143 | */ 144 | async datalabShoppingKeywords( 145 | params: DatalabShoppingKeywordsRequest 146 | ): Promise { 147 | return this.post( 148 | `${this.datalabBaseUrl}/shopping/category/keywords`, 149 | params 150 | ); 151 | } 152 | 153 | /** 154 | * 쇼핑 키워드 기기별 트렌드 분석 메서드 155 | */ 156 | async datalabShoppingKeywordByDevice( 157 | params: DatalabShoppingKeywordRequest 158 | ): Promise { 159 | return this.post( 160 | `${this.datalabBaseUrl}/shopping/category/keyword/device`, 161 | params 162 | ); 163 | } 164 | 165 | /** 166 | * 쇼핑 키워드 성별 트렌드 분석 메서드 167 | */ 168 | async datalabShoppingKeywordByGender( 169 | params: DatalabShoppingKeywordRequest 170 | ): Promise { 171 | return this.post( 172 | `${this.datalabBaseUrl}/shopping/category/keyword/gender`, 173 | params 174 | ); 175 | } 176 | 177 | /** 178 | * 쇼핑 키워드 연령별 트렌드 분석 메서드 179 | */ 180 | async datalabShoppingKeywordByAge( 181 | params: DatalabShoppingKeywordRequest 182 | ): Promise { 183 | return this.post( 184 | `${this.datalabBaseUrl}/shopping/category/keyword/age`, 185 | params 186 | ); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/handlers/datalab.handlers.ts: -------------------------------------------------------------------------------- 1 | import { NaverSearchClient } from "../clients/naver-search.client.js"; 2 | import { 3 | DatalabSearch, 4 | DatalabShopping, 5 | DatalabShoppingDevice, 6 | DatalabShoppingGender, 7 | DatalabShoppingAge, 8 | DatalabShoppingKeywords, 9 | DatalabShoppingKeywordDevice, 10 | DatalabShoppingKeywordGender, 11 | DatalabShoppingKeywordAge, 12 | } from "../schemas/datalab.schemas.js"; 13 | 14 | // 클라이언트 인스턴스 (싱글톤) 15 | const client = NaverSearchClient.getInstance(); 16 | 17 | /** 18 | * 데이터랩 도구 핸들러 맵 19 | * 각 도구 이름을 키로, 실행할 핸들러 함수를 값으로 가짐 20 | * index.ts에서 도구 실행 분기 없이 바로 사용 21 | */ 22 | export const datalabToolHandlers: Record Promise> = 23 | { 24 | datalab_search: (args) => { 25 | console.error("datalab_search called with args:", JSON.stringify(args, null, 2)); 26 | return handleSearchTrend(args); 27 | }, 28 | datalab_shopping_category: (args) => { 29 | console.error("datalab_shopping_category called with args:", JSON.stringify(args, null, 2)); 30 | return handleShoppingCategoryTrend(args); 31 | }, 32 | datalab_shopping_by_device: (args) => { 33 | console.error("datalab_shopping_by_device called with args:", JSON.stringify(args, null, 2)); 34 | return handleShoppingByDeviceTrend(args); 35 | }, 36 | datalab_shopping_by_gender: (args) => { 37 | console.error("datalab_shopping_by_gender called with args:", JSON.stringify(args, null, 2)); 38 | return handleShoppingByGenderTrend(args); 39 | }, 40 | datalab_shopping_by_age: (args) => { 41 | console.error("datalab_shopping_by_age called with args:", JSON.stringify(args, null, 2)); 42 | return handleShoppingByAgeTrend(args); 43 | }, 44 | datalab_shopping_keywords: (args) => { 45 | console.error("datalab_shopping_keywords called with args:", JSON.stringify(args, null, 2)); 46 | return handleShoppingKeywordsTrend(args); 47 | }, 48 | datalab_shopping_keyword_by_device: (args) => { 49 | console.error("datalab_shopping_keyword_by_device called with args:", JSON.stringify(args, null, 2)); 50 | return handleShoppingKeywordByDeviceTrend(args); 51 | }, 52 | datalab_shopping_keyword_by_gender: (args) => { 53 | console.error("datalab_shopping_keyword_by_gender called with args:", JSON.stringify(args, null, 2)); 54 | return handleShoppingKeywordByGenderTrend(args); 55 | }, 56 | datalab_shopping_keyword_by_age: (args) => { 57 | console.error("datalab_shopping_keyword_by_age called with args:", JSON.stringify(args, null, 2)); 58 | return handleShoppingKeywordByAgeTrend(args); 59 | }, 60 | }; 61 | 62 | /** 63 | * 검색어 트렌드 핸들러 64 | * 네이버 데이터랩 검색어 트렌드 분석 API 호출 65 | * @param params DatalabSearch 66 | */ 67 | export async function handleSearchTrend(params: DatalabSearch) { 68 | return client.searchTrend(params); 69 | } 70 | 71 | /** 72 | * 쇼핑 카테고리 트렌드 핸들러 73 | * 네이버 데이터랩 쇼핑 카테고리별 트렌드 분석 API 호출 74 | * @param params DatalabShopping 75 | */ 76 | export async function handleShoppingCategoryTrend(params: DatalabShopping) { 77 | return client.datalabShoppingCategory(params); 78 | } 79 | 80 | /** 81 | * 쇼핑 기기별 트렌드 핸들러 82 | * 네이버 데이터랩 쇼핑 기기별 트렌드 분석 API 호출 83 | * @param params DatalabShoppingDevice 84 | */ 85 | export async function handleShoppingByDeviceTrend( 86 | params: DatalabShoppingDevice 87 | ) { 88 | return client.datalabShoppingByDevice({ 89 | startDate: params.startDate, 90 | endDate: params.endDate, 91 | timeUnit: params.timeUnit, 92 | category: params.category, 93 | device: params.device, 94 | }); 95 | } 96 | 97 | /** 98 | * 쇼핑 성별 트렌드 핸들러 99 | * 네이버 데이터랩 쇼핑 성별 트렌드 분석 API 호출 100 | * @param params DatalabShoppingGender 101 | */ 102 | export async function handleShoppingByGenderTrend( 103 | params: DatalabShoppingGender 104 | ) { 105 | return client.datalabShoppingByGender({ 106 | startDate: params.startDate, 107 | endDate: params.endDate, 108 | timeUnit: params.timeUnit, 109 | category: params.category, 110 | gender: params.gender, 111 | }); 112 | } 113 | 114 | /** 115 | * 쇼핑 연령별 트렌드 핸들러 116 | * 네이버 데이터랩 쇼핑 연령별 트렌드 분석 API 호출 117 | * @param params DatalabShoppingAge 118 | */ 119 | export async function handleShoppingByAgeTrend(params: DatalabShoppingAge) { 120 | return client.datalabShoppingByAge({ 121 | startDate: params.startDate, 122 | endDate: params.endDate, 123 | timeUnit: params.timeUnit, 124 | category: params.category, 125 | ages: params.ages, 126 | }); 127 | } 128 | 129 | /** 130 | * 쇼핑 키워드 트렌드 핸들러 (복수 키워드 그룹 지원) 131 | * 네이버 데이터랩 쇼핑 키워드 그룹 트렌드 분석 API 호출 132 | * @param params DatalabShoppingKeywords 133 | */ 134 | export async function handleShoppingKeywordsTrend( 135 | params: DatalabShoppingKeywords 136 | ) { 137 | // 키워드 배열을 네이버 API에 맞는 형식으로 변환 138 | return client.datalabShoppingKeywords({ 139 | startDate: params.startDate, 140 | endDate: params.endDate, 141 | timeUnit: params.timeUnit, 142 | category: params.category, 143 | keyword: params.keyword, 144 | }); 145 | } 146 | 147 | /** 148 | * 쇼핑 키워드 기기별 트렌드 핸들러 149 | * 네이버 데이터랩 쇼핑 키워드 기기별 트렌드 분석 API 호출 150 | * @param params DatalabShoppingKeywordDevice 151 | */ 152 | export async function handleShoppingKeywordByDeviceTrend( 153 | params: DatalabShoppingKeywordDevice 154 | ) { 155 | return client.datalabShoppingKeywordByDevice({ 156 | startDate: params.startDate, 157 | endDate: params.endDate, 158 | timeUnit: params.timeUnit, 159 | category: params.category, 160 | keyword: params.keyword, 161 | device: params.device, 162 | }); 163 | } 164 | 165 | /** 166 | * 쇼핑 키워드 성별 트렌드 핸들러 167 | * 네이버 데이터랩 쇼핑 키워드 성별 트렌드 분석 API 호출 168 | * @param params DatalabShoppingKeywordGender 169 | */ 170 | export async function handleShoppingKeywordByGenderTrend( 171 | params: DatalabShoppingKeywordGender 172 | ) { 173 | return client.datalabShoppingKeywordByGender({ 174 | startDate: params.startDate, 175 | endDate: params.endDate, 176 | timeUnit: params.timeUnit, 177 | category: params.category, 178 | keyword: params.keyword, 179 | gender: params.gender, 180 | }); 181 | } 182 | 183 | /** 184 | * 쇼핑 키워드 연령별 트렌드 핸들러 185 | * 네이버 데이터랩 쇼핑 키워드 연령별 트렌드 분석 API 호출 186 | * @param params DatalabShoppingKeywordAge 187 | */ 188 | export async function handleShoppingKeywordByAgeTrend( 189 | params: DatalabShoppingKeywordAge 190 | ) { 191 | return client.datalabShoppingKeywordByAge({ 192 | startDate: params.startDate, 193 | endDate: params.endDate, 194 | timeUnit: params.timeUnit, 195 | category: params.category, 196 | keyword: params.keyword, 197 | ages: params.ages, 198 | }); 199 | } 200 | -------------------------------------------------------------------------------- /src/handlers/category.handlers.ts: -------------------------------------------------------------------------------- 1 | // Import JSON data - bundle-safe approach 2 | import { existsSync, readFileSync } from 'fs'; 3 | import { join } from 'path'; 4 | 5 | // 메모리 누수 방지를 위한 지연 로딩 캐시 6 | let categoriesCache: any[] | null = null; 7 | 8 | // Load categories data with fallback paths for different environments 9 | function getCategoriesData(): any[] { 10 | // 캐시된 데이터가 있으면 반환 (메모리 효율성) 11 | if (categoriesCache !== null) { 12 | return categoriesCache; 13 | } 14 | 15 | try { 16 | // Try bundled data path first (dist/data) 17 | const bundledPath = join(process.cwd(), 'dist', 'data', 'categories.json'); 18 | if (existsSync(bundledPath)) { 19 | categoriesCache = JSON.parse(readFileSync(bundledPath, 'utf8') as string); 20 | return categoriesCache!; 21 | } 22 | 23 | // Fallback to source data path 24 | const sourcePath = join(process.cwd(), 'data', 'categories.json'); 25 | if (existsSync(sourcePath)) { 26 | categoriesCache = JSON.parse(readFileSync(sourcePath, 'utf8') as string); 27 | return categoriesCache!; 28 | } 29 | 30 | throw new Error('카테고리 데이터 파일을 찾을 수 없습니다'); 31 | } catch (error) { 32 | console.error('카테고리 데이터 로딩 실패:', error); 33 | return []; 34 | } 35 | } 36 | 37 | /** 38 | * 캐시 정리 함수 (메모리 누수 방지) 39 | */ 40 | export function clearCategoriesCache(): void { 41 | categoriesCache = null; 42 | } 43 | 44 | // Category data structure 45 | interface CategoryData { 46 | code: string; 47 | level1: string; // 대분류 48 | level2: string; // 중분류 49 | level3: string; // 소분류 50 | level4: string; // 세분류 51 | } 52 | 53 | /** 54 | * Load category data from bundled JSON file 55 | */ 56 | async function loadCategoryData(): Promise { 57 | // 지연 로딩된 캐시 데이터 사용 58 | const data = getCategoriesData(); 59 | console.error(`Loaded ${data.length} categories from bundled JSON data`); 60 | return data as CategoryData[]; 61 | } 62 | 63 | /** 64 | * Smart category search with fuzzy matching and ranking 65 | */ 66 | async function smartCategorySearch(query: string, maxResults: number = 10): Promise> { 67 | const categories = await loadCategoryData(); 68 | const searchQuery = query.toLowerCase().trim(); 69 | const results: Array = []; 70 | 71 | for (const category of categories) { 72 | let bestScore = 0; 73 | let bestMatchType = ''; 74 | 75 | // Check all levels for matches 76 | const levels = [ 77 | { value: category.level1, name: '대분류' }, 78 | { value: category.level2, name: '중분류' }, 79 | { value: category.level3, name: '소분류' }, 80 | { value: category.level4, name: '세분류' } 81 | ]; 82 | 83 | for (const level of levels) { 84 | if (!level.value) continue; 85 | 86 | const levelText = level.value.toLowerCase(); 87 | let score = 0; 88 | let matchType = ''; 89 | 90 | // Level-based bonus (대분류 우선) 91 | let levelBonus = 0; 92 | if (level.name === '대분류') levelBonus = 50; 93 | else if (level.name === '중분류') levelBonus = 30; 94 | else if (level.name === '소분류') levelBonus = 20; 95 | else if (level.name === '세분류') levelBonus = 10; 96 | 97 | // Exact match (highest priority) 98 | if (levelText === searchQuery) { 99 | score = 100 + levelBonus; 100 | matchType = `정확일치(${level.name})`; 101 | } 102 | // Starts with (high priority) 103 | else if (levelText.startsWith(searchQuery)) { 104 | score = 80 + levelBonus; 105 | matchType = `시작일치(${level.name})`; 106 | } 107 | // Contains (medium priority) 108 | else if (levelText.includes(searchQuery)) { 109 | score = 60 + levelBonus; 110 | matchType = `포함일치(${level.name})`; 111 | } 112 | // Fuzzy match for similar terms 113 | else { 114 | const similarity = calculateSimilarity(searchQuery, levelText); 115 | if (similarity > 0.6) { 116 | score = Math.floor(similarity * 40) + levelBonus; // 0.6-1.0 -> 24-40 points + level bonus 117 | matchType = `유사일치(${level.name})`; 118 | } 119 | } 120 | 121 | if (score > bestScore) { 122 | bestScore = score; 123 | bestMatchType = matchType; 124 | } 125 | } 126 | 127 | if (bestScore > 0) { 128 | results.push({ 129 | ...category, 130 | score: bestScore, 131 | matchType: bestMatchType 132 | }); 133 | } 134 | } 135 | 136 | // Sort by score (descending), then by category code (ascending for 대분류 priority) 137 | return results 138 | .sort((a, b) => { 139 | if (b.score !== a.score) { 140 | return b.score - a.score; // Higher score first 141 | } 142 | // If same score, prioritize by category code (대분류 codes are typically smaller) 143 | return a.code.localeCompare(b.code); 144 | }) 145 | .slice(0, maxResults); 146 | } 147 | 148 | /** 149 | * Calculate similarity between two strings (simple implementation) 150 | */ 151 | function calculateSimilarity(str1: string, str2: string): number { 152 | const longer = str1.length > str2.length ? str1 : str2; 153 | const shorter = str1.length > str2.length ? str2 : str1; 154 | 155 | if (longer.length === 0) return 1.0; 156 | 157 | const editDistance = levenshteinDistance(longer, shorter); 158 | return (longer.length - editDistance) / longer.length; 159 | } 160 | 161 | /** 162 | * Calculate Levenshtein distance between two strings 163 | */ 164 | function levenshteinDistance(str1: string, str2: string): number { 165 | const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null)); 166 | 167 | for (let i = 0; i <= str1.length; i++) matrix[0][i] = i; 168 | for (let j = 0; j <= str2.length; j++) matrix[j][0] = j; 169 | 170 | for (let j = 1; j <= str2.length; j++) { 171 | for (let i = 1; i <= str1.length; i++) { 172 | const substitutionCost = str1[i - 1] === str2[j - 1] ? 0 : 1; 173 | matrix[j][i] = Math.min( 174 | matrix[j][i - 1] + 1, 175 | matrix[j - 1][i] + 1, 176 | matrix[j - 1][i - 1] + substitutionCost 177 | ); 178 | } 179 | } 180 | 181 | return matrix[str2.length][str1.length]; 182 | } 183 | 184 | /** 185 | * 카테고리 검색 핸들러 186 | */ 187 | export const findCategoryHandler = async ({ query, max_results = 10 }: any) => { 188 | try { 189 | const results = await smartCategorySearch(query, max_results); 190 | 191 | if (results.length === 0) { 192 | return { 193 | message: `"${query}"와 관련된 카테고리를 찾을 수 없습니다. 다른 검색어를 시도해보세요.`, 194 | suggestions: ["패션", "화장품", "가구", "스마트폰", "가전제품", "스포츠", "도서", "자동차", "식품", "뷰티"] 195 | }; 196 | } 197 | 198 | const responseData: any = { 199 | message: `"${query}" 검색 결과 (${results.length}개, 관련도순 정렬)`, 200 | total_found: results.length, 201 | categories: results.map(cat => ({ 202 | code: cat.code, 203 | category: [cat.level1, cat.level2, cat.level3, cat.level4].filter(Boolean).join(' > '), 204 | match_type: cat.matchType, 205 | score: cat.score, 206 | levels: { 207 | 대분류: cat.level1 || '', 208 | 중분류: cat.level2 || '', 209 | 소분류: cat.level3 || '', 210 | 세분류: cat.level4 || '' 211 | } 212 | })), 213 | next_steps: { 214 | trend_analysis: `이제 datalab_shopping_category 도구로 각 카테고리의 트렌드 분석이 가능합니다`, 215 | age_analysis: `datalab_shopping_age 도구로 연령별 쇼핑 패턴을 분석할 수 있습니다`, 216 | gender_analysis: `datalab_shopping_gender 도구로 성별 쇼핑 패턴을 분석할 수 있습니다`, 217 | device_analysis: `datalab_shopping_device 도구로 디바이스별 쇼핑 패턴을 분석할 수 있습니다` 218 | } 219 | }; 220 | 221 | return responseData; 222 | } catch (error: any) { 223 | throw new Error(`카테고리 검색 중 오류 발생: ${error.message}`); 224 | } 225 | }; 226 | 227 | export const categoryToolHandlers: Record Promise> = { 228 | find_category: findCategoryHandler, 229 | }; 230 | -------------------------------------------------------------------------------- /src/utils/memory-manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Memory Management Utility for Naver Search MCP Server 3 | * 메모리 누수 방지 및 리소스 관리를 위한 유틸리티 4 | */ 5 | 6 | import { NaverSearchClient } from "../clients/naver-search.client.js"; 7 | import { clearCategoriesCache } from "../handlers/category.handlers.js"; 8 | 9 | export interface MemoryUsage { 10 | rss: number; // Resident Set Size 11 | heapTotal: number; // Total heap size 12 | heapUsed: number; // Used heap size 13 | external: number; // External memory usage 14 | arrayBuffers: number; // ArrayBuffer memory usage 15 | } 16 | 17 | export interface MemoryStats { 18 | usage: MemoryUsage; 19 | timestamp: number; 20 | formattedUsage: { 21 | rss: string; 22 | heapTotal: string; 23 | heapUsed: string; 24 | external: string; 25 | arrayBuffers: string; 26 | }; 27 | } 28 | 29 | /** 30 | * 메모리 사용량 조회 31 | */ 32 | export function getMemoryUsage(): MemoryStats { 33 | const usage = (globalThis as any).process.memoryUsage(); 34 | const timestamp = Date.now(); 35 | 36 | const formatBytes = (bytes: number): string => { 37 | const mb = bytes / 1024 / 1024; 38 | return `${mb.toFixed(2)} MB`; 39 | }; 40 | 41 | return { 42 | usage, 43 | timestamp, 44 | formattedUsage: { 45 | rss: formatBytes(usage.rss), 46 | heapTotal: formatBytes(usage.heapTotal), 47 | heapUsed: formatBytes(usage.heapUsed), 48 | external: formatBytes(usage.external), 49 | arrayBuffers: formatBytes(usage.arrayBuffers), 50 | }, 51 | }; 52 | } 53 | 54 | /** 55 | * 메모리 누수 감지 임계값 (MB) 56 | */ 57 | const MEMORY_THRESHOLDS = { 58 | WARNING: 200, // 200MB 59 | CRITICAL: 500, // 500MB 60 | EMERGENCY: 1024, // 1GB 61 | }; 62 | 63 | /** 64 | * 메모리 상태 확인 65 | */ 66 | export function checkMemoryHealth(): { 67 | status: 'healthy' | 'warning' | 'critical' | 'emergency'; 68 | recommendation: string; 69 | stats: MemoryStats; 70 | } { 71 | const stats = getMemoryUsage(); 72 | const heapUsedMB = stats.usage.heapUsed / 1024 / 1024; 73 | 74 | let status: 'healthy' | 'warning' | 'critical' | 'emergency' = 'healthy'; 75 | let recommendation = '메모리 사용량이 정상 범위입니다.'; 76 | 77 | if (heapUsedMB > MEMORY_THRESHOLDS.EMERGENCY) { 78 | status = 'emergency'; 79 | recommendation = '즉시 메모리 정리가 필요합니다. 서버 재시작을 고려하세요.'; 80 | } else if (heapUsedMB > MEMORY_THRESHOLDS.CRITICAL) { 81 | status = 'critical'; 82 | recommendation = '메모리 사용량이 위험 수준입니다. 리소스 정리를 수행하세요.'; 83 | } else if (heapUsedMB > MEMORY_THRESHOLDS.WARNING) { 84 | status = 'warning'; 85 | recommendation = '메모리 사용량이 높습니다. 모니터링을 계속하세요.'; 86 | } 87 | 88 | return { status, recommendation, stats }; 89 | } 90 | 91 | /** 92 | * 포괄적인 메모리 정리 수행 93 | */ 94 | export async function performMemoryCleanup(): Promise<{ 95 | before: MemoryStats; 96 | after: MemoryStats; 97 | cleanupActions: string[]; 98 | }> { 99 | const before = getMemoryUsage(); 100 | const cleanupActions: string[] = []; 101 | 102 | try { 103 | // 1. 클라이언트 인스턴스 정리 104 | NaverSearchClient.destroyInstance(); 105 | cleanupActions.push('NaverSearchClient 인스턴스 정리 완료'); 106 | 107 | // 2. 카테고리 캐시 정리 108 | clearCategoriesCache(); 109 | cleanupActions.push('카테고리 캐시 정리 완료'); 110 | 111 | // 3. 가비지 컬렉션 강제 실행 (가능한 경우) 112 | if ((globalThis as any).gc) { 113 | (globalThis as any).gc(); 114 | cleanupActions.push('가비지 컬렉션 강제 실행 완료'); 115 | } else { 116 | cleanupActions.push('가비지 컬렉션 사용 불가 (--expose-gc 플래그 필요)'); 117 | } 118 | 119 | // 4. 잠시 대기하여 정리 작업 완료 120 | await new Promise(resolve => setTimeout(resolve, 100)); 121 | 122 | } catch (error) { 123 | cleanupActions.push(`정리 중 오류 발생: ${error}`); 124 | } 125 | 126 | const after = getMemoryUsage(); 127 | 128 | return { before, after, cleanupActions }; 129 | } 130 | 131 | /** 132 | * 메모리 사용량 모니터링 클래스 133 | */ 134 | export class MemoryMonitor { 135 | private intervalId: any = null; 136 | private memoryHistory: MemoryStats[] = []; 137 | private maxHistorySize = 10; 138 | 139 | /** 140 | * 메모리 모니터링 시작 141 | */ 142 | start(intervalMs: number = 60000): void { // 기본 1분 간격 143 | if (this.intervalId) { 144 | this.stop(); 145 | } 146 | 147 | this.intervalId = setInterval(() => { 148 | const stats = getMemoryUsage(); 149 | this.memoryHistory.push(stats); 150 | 151 | // 히스토리 크기 제한 152 | if (this.memoryHistory.length > this.maxHistorySize) { 153 | this.memoryHistory.shift(); 154 | } 155 | 156 | // 메모리 상태 확인 및 로깅 157 | const health = checkMemoryHealth(); 158 | if (health.status !== 'healthy') { 159 | console.error(`[Memory Monitor] Status: ${health.status}, Usage: ${health.stats.formattedUsage.heapUsed}, Recommendation: ${health.recommendation}`); 160 | 161 | // 위험 수준에서 자동 정리 수행 162 | if (health.status === 'critical' || health.status === 'emergency') { 163 | this.performAutoCleanup(); 164 | } 165 | } 166 | }, intervalMs); 167 | 168 | console.error(`[Memory Monitor] Started monitoring with ${intervalMs}ms interval`); 169 | } 170 | 171 | /** 172 | * 메모리 모니터링 중지 173 | */ 174 | stop(): void { 175 | if (this.intervalId) { 176 | clearInterval(this.intervalId); 177 | this.intervalId = null; 178 | console.error('[Memory Monitor] Monitoring stopped'); 179 | } 180 | } 181 | 182 | /** 183 | * 메모리 히스토리 조회 184 | */ 185 | getHistory(): MemoryStats[] { 186 | return [...this.memoryHistory]; 187 | } 188 | 189 | /** 190 | * 메모리 사용량 트렌드 분석 191 | */ 192 | analyzeTrend(): { 193 | trend: 'increasing' | 'decreasing' | 'stable'; 194 | averageUsage: number; 195 | maxUsage: number; 196 | minUsage: number; 197 | } { 198 | if (this.memoryHistory.length < 3) { 199 | return { 200 | trend: 'stable', 201 | averageUsage: 0, 202 | maxUsage: 0, 203 | minUsage: 0 204 | }; 205 | } 206 | 207 | const usages = this.memoryHistory.map(stat => stat.usage.heapUsed); 208 | const recent = usages.slice(-3); 209 | 210 | let trend: 'increasing' | 'decreasing' | 'stable' = 'stable'; 211 | if (recent[2] > recent[1] && recent[1] > recent[0]) { 212 | trend = 'increasing'; 213 | } else if (recent[2] < recent[1] && recent[1] < recent[0]) { 214 | trend = 'decreasing'; 215 | } 216 | 217 | return { 218 | trend, 219 | averageUsage: usages.reduce((a, b) => a + b, 0) / usages.length, 220 | maxUsage: Math.max(...usages), 221 | minUsage: Math.min(...usages) 222 | }; 223 | } 224 | 225 | /** 226 | * 자동 메모리 정리 수행 227 | */ 228 | private async performAutoCleanup(): Promise { 229 | try { 230 | console.error('[Memory Monitor] Performing automatic memory cleanup...'); 231 | const result = await performMemoryCleanup(); 232 | 233 | const beforeMB = result.before.usage.heapUsed / 1024 / 1024; 234 | const afterMB = result.after.usage.heapUsed / 1024 / 1024; 235 | const savedMB = beforeMB - afterMB; 236 | 237 | console.error(`[Memory Monitor] Cleanup completed. Saved ${savedMB.toFixed(2)} MB`); 238 | console.error(`[Memory Monitor] Actions: ${result.cleanupActions.join(', ')}`); 239 | } catch (error) { 240 | console.error(`[Memory Monitor] Auto cleanup failed: ${error}`); 241 | } 242 | } 243 | } 244 | 245 | // 전역 메모리 모니터 인스턴스 246 | let globalMemoryMonitor: MemoryMonitor | null = null; 247 | 248 | /** 249 | * 글로벌 메모리 모니터 시작 250 | */ 251 | export function startGlobalMemoryMonitoring(intervalMs: number = 300000): void { // 기본 5분 간격 252 | if (!globalMemoryMonitor) { 253 | globalMemoryMonitor = new MemoryMonitor(); 254 | } 255 | globalMemoryMonitor.start(intervalMs); 256 | } 257 | 258 | /** 259 | * 글로벌 메모리 모니터 중지 260 | */ 261 | export function stopGlobalMemoryMonitoring(): void { 262 | if (globalMemoryMonitor) { 263 | globalMemoryMonitor.stop(); 264 | globalMemoryMonitor = null; 265 | } 266 | } 267 | 268 | /** 269 | * 현재 메모리 모니터 인스턴스 조회 270 | */ 271 | export function getGlobalMemoryMonitor(): MemoryMonitor | null { 272 | return globalMemoryMonitor; 273 | } -------------------------------------------------------------------------------- /README-ko.md: -------------------------------------------------------------------------------- 1 | # Naver Search MCP Server 2 | 3 | [![English](https://img.shields.io/badge/English-README-yellow)](README.md) 4 | [![smithery badge](https://smithery.ai/badge/@isnow890/naver-search-mcp)](https://smithery.ai/server/@isnow890/naver-search-mcp) 5 | [![MCP.so](https://img.shields.io/badge/MCP.so-Naver%20Search%20MCP-blue)](https://mcp.so/server/naver-search-mcp/isnow890) 6 | 7 | Naver 검색 API와 DataLab API 통합을 위한 MCP 서버로, 다양한 Naver 서비스에서의 종합적인 검색과 데이터 트렌드 분석을 가능하게 합니다. 8 | 9 | #### 버전 히스토리 10 | 11 | ###### 1.0.45 (2025-09-28) 12 | 13 | - Smithery 호환성 문제 해결 - 이제 Smithery를 통해 최신 기능으로 사용 가능 14 | - 카테고리 검색에서 엑셀 호환성 문제 해결 - JSON 기능으로 교체 15 | - 웹 한국어 검색(`search_webkr`) 기능 복구 16 | - Smithery 플랫폼 설치와 완전 호환 17 | 18 | ###### 1.0.44 (2025-08-31) 19 | 20 | - `get_current_korean_time` 도구 추가 - 한국 시간대를 위한 필수 시간 컨텍스트 도구 21 | - 시간적 쿼리를 위한 시간 도구 참조로 모든 기존 도구 설명 강화 22 | - "오늘", "지금", "현재" 검색을 위한 시간적 컨텍스트 처리 개선 23 | - 다양한 출력 형식의 포괄적인 한국어 시간 포맷팅 24 | 25 | ###### 1.0.40 (2025-08-21) 26 | 27 | - `find_category` 도구 추가 28 | **이제 트렌드와 쇼핑 인사이트 검색을 위하여 카테고리 번호를 url로 일일히 찾을 필요가 없습니다. 편하게 자연어로 검색하세요.** 29 | 30 | - Zod 스키마 기반 매개변수 검증 강화 31 | - 카테고리 검색 워크플로우 개선 32 | - 레벨 기반 카테고리 순위 시스템 구현 (대분류 우선) 33 | 34 | ###### 1.0.30 (2025-08-04) 35 | 36 | - MCP SDK 1.17.1로 업그레이드 37 | - Smithery 스펙 변경으로 인한 호환성 오류 수정 38 | - DataLab 쇼핑 카테고리 코드 상세 문서화 추가 39 | 40 | ###### 1.0.2 (2025-04-26) 41 | 42 | - README 업데이트: 카페글 검색 도구 및 버전 히스토리 안내 개선 43 | 44 | ###### 1.0.1 (2025-04-26) 45 | 46 | - 카페글 검색 기능 추가 47 | - zod에 쇼핑 카테고리 정보 추가 48 | - 소스코드 리팩토링 49 | 50 | ###### 1.0.0 (2025-04-08) 51 | 52 | - 오픈오픈 53 | 54 | #### 필수 요구 사항 55 | 56 | - Naver Developers API 키(클라이언트 ID 및 시크릿) 57 | - Node.js 18 이상 58 | - NPM 8 이상 59 | - Docker (선택 사항, 컨테이너 배포용) 60 | 61 | #### API 키 얻기 62 | 63 | 1. [Naver Developers](https://developers.naver.com/apps/#/register)에 방문 64 | 2. "애플리케이션 등록"을 클릭 65 | 3. 애플리케이션 이름을 입력하고 다음 API를 모두 선택: 66 | - 검색 (블로그, 뉴스, 책 검색 등을 위한) 67 | - DataLab (검색 트렌드) 68 | - DataLab (쇼핑 인사이트) 69 | 4. 얻은 클라이언트 ID와 클라이언트 시크릿을 환경 변수로 설정 70 | 71 | ## 도구 세부 정보 72 | 73 | ### 사용 가능한 도구: 74 | 75 | #### 🕐 시간 및 컨텍스트 도구 76 | 77 | - **get_current_korean_time**: 종합적인 날짜/시간 정보와 함께 현재 한국 시간(KST)을 가져옵니다. 한국 시간대에서 "오늘", "지금", "현재" 컨텍스트를 이해하는 데 필수적입니다. 검색이나 분석에 시간적 컨텍스트가 필요할 때 항상 이 도구를 사용하세요. 78 | 79 | #### 🆕 카테고리 검색 80 | 81 | - **find_category**: 카테고리 검색 도구 - 이제 트렌드와 쇼핑 인사이트 검색을 위하여 카테고리 번호를 url로 일일히 찾을 필요가 없습니다. 편하게 자연어로 검색하세요. 82 | 83 | #### 검색 도구 84 | 85 | - **search_webkr**: 웹 문서 검색 86 | - **search_news**: 뉴스 검색 87 | - **search_blog**: 블로그 검색 88 | - **search_cafearticle**: 카페글 검색 89 | - **search_shop**: 쇼핑 검색 90 | - **search_image**: 이미지 검색 91 | - **search_kin**: 지식iN 검색 92 | - **search_book**: 책 검색 93 | - **search_encyc**: 백과사전 검색 94 | - **search_academic**: 학술 논문 검색 95 | - **search_local**: 지역 장소 검색 96 | 97 | #### DataLab 도구 98 | 99 | - **datalab_search**: 검색어 트렌드 분석 100 | - **datalab_shopping_category**: 쇼핑 카테고리 트렌드 분석 101 | - **datalab_shopping_by_device**: 기기별 쇼핑 트렌드 분석 102 | - **datalab_shopping_by_gender**: 성별 쇼핑 트렌드 분석 103 | - **datalab_shopping_by_age**: 연령대별 쇼핑 트렌드 분석 104 | - **datalab_shopping_keywords**: 쇼핑 키워드 트렌드 분석 105 | - **datalab_shopping_keyword_by_device**: 쇼핑 키워드 기기별 트렌드 분석 106 | - **datalab_shopping_keyword_by_gender**: 쇼핑 키워드 성별 트렌드 분석 107 | - **datalab_shopping_keyword_by_age**: 쇼핑 키워드 연령별 트렌드 분석 108 | 109 | ### 🎯 비즈니스 활용 사례 & 시나리오 110 | 111 | #### 🛍️ 전자상거래 시장 조사 112 | 113 | ```javascript 114 | // 패션 트렌드 발견 115 | find_category("패션") → 상위 패션 카테고리와 코드 확인 116 | datalab_shopping_category → 계절별 패션 트렌드 분석 117 | datalab_shopping_age → 패션 타겟 연령층 파악 118 | datalab_shopping_keywords → "원피스" vs "자켓" vs "드레스" 비교 119 | ``` 120 | 121 | #### 📱 디지털 마케팅 전략 122 | 123 | ```javascript 124 | // 뷰티 업계 분석 125 | find_category("화장품") → 뷰티 카테고리 찾기 126 | datalab_shopping_gender → 여성 95% vs 남성 5% 쇼핑객 127 | datalab_shopping_device → 뷰티 쇼핑의 모바일 우세 128 | datalab_shopping_keywords → "틴트" vs "립스틱" 키워드 성과 129 | ``` 130 | 131 | #### 🏢 비즈니스 인텔리전스 & 경쟁 분석 132 | 133 | ```javascript 134 | // 테크 제품 인사이트 135 | find_category("스마트폰") → 전자제품 카테고리 확인 136 | datalab_shopping_category → 아이폰 vs 갤럭시 트렌드 추적 137 | datalab_shopping_age → 20-30대가 주요 스마트폰 구매층 138 | datalab_shopping_device → PC vs 모바일 쇼핑 행동 139 | ``` 140 | 141 | #### 📊 계절별 비즈니스 계획 142 | 143 | ```javascript 144 | // 휴일 쇼핑 분석 145 | find_category("선물") → 선물 카테고리 146 | datalab_shopping_category → 블랙프라이데이, 크리스마스 트렌드 147 | datalab_shopping_keywords → "어버이날 선물" vs "생일선물" 148 | datalab_shopping_age → 연령대별 선물 구매 패턴 149 | ``` 150 | 151 | #### 🎯 고객 페르소나 개발 152 | 153 | ```javascript 154 | // 피트니스 시장 분석 155 | find_category("운동") → 스포츠/피트니스 카테고리 156 | datalab_shopping_gender → 남녀 피트니스 지출 비교 157 | datalab_shopping_age → 피트니스 주요 연령층 (20-40대) 158 | datalab_shopping_keywords → "홈트" vs "헬스장" 트렌드 분석 159 | ``` 160 | 161 | ### 📈 고급 분석 시나리오 162 | 163 | #### 시장 진입 전략 164 | 165 | 1. **카테고리 발견**: `find_category`로 시장 세그먼트 탐색 166 | 2. **트렌드 분석**: 성장하는 vs 쇠퇴하는 카테고리 식별 167 | 3. **인구통계 타겟팅**: 고객 타겟팅을 위한 연령/성별 분석 168 | 4. **경쟁 인텔리전스**: 키워드 성과 비교 169 | 5. **기기 전략**: 모바일 vs PC 쇼핑 최적화 170 | 171 | #### 제품 출시 계획 172 | 173 | 1. **시장 검증**: 카테고리 성장 트렌드와 계절성 174 | 2. **타겟 고객**: 제품 포지셔닝을 위한 인구통계 분석 175 | 3. **마케팅 채널**: 광고 전략을 위한 기기 선호도 176 | 4. **경쟁 환경**: 키워드 경쟁과 기회 177 | 5. **가격 전략**: 카테고리 성과와 가격 연관성 178 | 179 | #### 성과 모니터링 180 | 181 | 1. **카테고리 건강도**: 제품 카테고리 트렌드 모니터링 182 | 2. **키워드 추적**: 브랜드 및 제품 키워드 성과 추적 183 | 3. **인구통계 변화**: 변화하는 고객 인구통계 모니터링 184 | 4. **계절 패턴**: 재고 및 마케팅 캠페인 계획 185 | 5. **경쟁 벤치마킹**: 카테고리 평균 대비 성과 비교 186 | 187 | ### 빠른 참조: 인기 카테고리 코드 188 | 189 | | 카테고리 | 코드 | 한국어 | 190 | | --------------- | -------- | ------------- | 191 | | 패션/의류 | 50000000 | 패션의류 | 192 | | 화장품/뷰티 | 50000002 | 화장품/미용 | 193 | | 디지털/전자제품 | 50000003 | 디지털/가전 | 194 | | 스포츠/레저 | 50000004 | 스포츠/레저 | 195 | | 식품/음료 | 50000008 | 식품/음료 | 196 | | 건강/의료 | 50000009 | 건강/의료용품 | 197 | 198 | 💡 **팁**: "뷰티", "패션", "전자제품"과 같은 퍼지 검색으로 `find_category`를 사용하여 카테고리를 쉽게 찾아보세요. 199 | 200 | ## 설치 201 | 202 | ### 방법 1: NPX 설치 (권장) 203 | 204 | 이 MCP 서버를 사용하는 가장 안정적인 방법은 NPX 직접 설치입니다. 자세한 패키지 정보는 [NPM 패키지 페이지](https://www.npmjs.com/package/@isnow890/naver-search-mcp)를 참조하세요. 205 | 206 | #### Claude Desktop 설정 207 | 208 | Claude Desktop 설정 파일에 다음을 추가하세요 (Windows: `%APPDATA%\Claude\claude_desktop_config.json`, macOS/Linux: `~/Library/Application Support/Claude/claude_desktop_config.json`): 209 | 210 | ```json 211 | { 212 | "mcpServers": { 213 | "naver-search": { 214 | "command": "npx", 215 | "args": ["-y", "@isnow890/naver-search-mcp"], 216 | "env": { 217 | "NAVER_CLIENT_ID": "your_client_id", 218 | "NAVER_CLIENT_SECRET": "your_client_secret" 219 | } 220 | } 221 | } 222 | } 223 | ``` 224 | 225 | #### Cursor AI 설정 226 | 227 | `mcp.json`에 추가: 228 | 229 | ```json 230 | { 231 | "mcpServers": { 232 | "naver-search": { 233 | "command": "npx", 234 | "args": ["-y", "@isnow890/naver-search-mcp"], 235 | "env": { 236 | "NAVER_CLIENT_ID": "your_client_id", 237 | "NAVER_CLIENT_SECRET": "your_client_secret" 238 | } 239 | } 240 | } 241 | } 242 | ``` 243 | 244 | ### 방법 2: Smithery 설치 (대안 - 알려진 문제 있음) 245 | 246 | ⚠️ **중요 안내**: Smithery 설치는 WebSocket relay 인프라 문제로 인해 연결 타임아웃 및 멈춤 현상이 발생할 수 있습니다. 이는 Smithery 플랫폼의 알려진 문제이며, 본 MCP 서버 코드의 문제가 아닙니다. **안정적인 동작을 위해 방법 1 (NPX 설치)을 강력히 권장합니다.** 247 | 248 | #### Smithery의 알려진 문제점: 249 | - 서버 초기화가 멈추거나 타임아웃 발생 250 | - `Error -32001: Request timed out` 에러 251 | - 핸드셰이크 후 WebSocket 연결 끊김 252 | - 요청 처리 전 서버가 예기치 않게 종료됨 253 | 254 | #### 그래도 Smithery를 시도하고 싶다면: 255 | 256 | ##### Claude Desktop용: 257 | 258 | ```bash 259 | npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client claude 260 | ``` 261 | 262 | ##### 기타 AI 클라이언트용: 263 | 264 | ```bash 265 | # Cursor 266 | npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client cursor 267 | 268 | # Windsurf 269 | npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client windsurf 270 | 271 | # Cline 272 | npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client cline 273 | ``` 274 | 275 | **Smithery에서 타임아웃 문제가 발생하면 방법 1 (NPX)로 전환하여 안정적으로 사용하세요.** 276 | 277 | ### 방법 3: 로컬 설치 278 | 279 | 로컬 개발이나 커스텀 수정이 필요한 경우: 280 | 281 | #### 1단계: 소스 코드 다운로드 및 빌드 282 | 283 | ##### Git으로 클론하기 284 | 285 | ```bash 286 | git clone https://github.com/isnow890/naver-search-mcp.git 287 | cd naver-search-mcp 288 | npm install 289 | npm run build 290 | ``` 291 | 292 | ##### 또는 ZIP 파일로 다운로드 293 | 294 | 1. [GitHub 릴리스 페이지](https://github.com/isnow890/naver-search-mcp/releases)에서 최신 버전을 다운로드 295 | 2. ZIP 파일을 원하는 위치에 압축 해제 296 | 3. 터미널에서 압축 해제된 폴더로 이동: 297 | 298 | ```bash 299 | cd /path/to/naver-search-mcp 300 | npm install 301 | npm run build 302 | ``` 303 | 304 | ⚠️ **중요**: 설치 후 반드시 `npm run build`를 실행하여 컴파일된 JavaScript 파일이 포함된 `dist` 폴더를 생성해야 합니다. 305 | 306 | #### 2단계: Claude Desktop 설정 307 | 308 | 빌드 완료 후 다음 정보가 필요합니다: 309 | 310 | - **NAVER_CLIENT_ID**: Naver Developers에서 발급받은 클라이언트 ID 311 | - **NAVER_CLIENT_SECRET**: Naver Developers에서 발급받은 클라이언트 시크릿 312 | - **설치 경로**: 다운로드한 폴더의 절대 경로 313 | 314 | ##### Windows 설정 315 | 316 | Claude Desktop 설정 파일(`%APPDATA%\Claude\claude_desktop_config.json`)에 다음을 추가: 317 | 318 | ```json 319 | { 320 | "mcpServers": { 321 | "naver-search": { 322 | "type": "stdio", 323 | "command": "cmd", 324 | "args": [ 325 | "/c", 326 | "node", 327 | "C:\\path\\to\\naver-search-mcp\\dist\\src\\index.js" 328 | ], 329 | "cwd": "C:\\path\\to\\naver-search-mcp", 330 | "env": { 331 | "NAVER_CLIENT_ID": "your-naver-client-id", 332 | "NAVER_CLIENT_SECRET": "your-naver-client-secret" 333 | } 334 | } 335 | } 336 | } 337 | ``` 338 | 339 | ##### macOS/Linux 설정 340 | 341 | Claude Desktop 설정 파일(`~/Library/Application Support/Claude/claude_desktop_config.json`)에 다음을 추가: 342 | 343 | ```json 344 | { 345 | "mcpServers": { 346 | "naver-search": { 347 | "type": "stdio", 348 | "command": "node", 349 | "args": ["/path/to/naver-search-mcp/dist/src/index.js"], 350 | "cwd": "/path/to/naver-search-mcp", 351 | "env": { 352 | "NAVER_CLIENT_ID": "your-naver-client-id", 353 | "NAVER_CLIENT_SECRET": "your-naver-client-secret" 354 | } 355 | } 356 | } 357 | } 358 | ``` 359 | 360 | ##### 경로 설정 주의사항 361 | 362 | ⚠️ **중요**: 위 설정에서 다음 경로들을 실제 설치 경로로 변경해야 합니다: 363 | 364 | - **Windows**: `C:\\path\\to\\naver-search-mcp`를 실제 다운로드한 폴더 경로로 변경 365 | - **macOS/Linux**: `/path/to/naver-search-mcp`를 실제 다운로드한 폴더 경로로 변경 366 | - **빌드 경로**: 경로가 `dist/src/index.js`를 가리키는지 확인 (`index.js`만이 아님) 367 | 368 | 경로 찾기: 369 | 370 | ```bash 371 | # 현재 위치 확인 372 | pwd 373 | 374 | # 절대 경로 예시 375 | # Windows: C:\Users\홍길동\Downloads\naver-search-mcp 376 | # macOS: /Users/홍길동/Downloads/naver-search-mcp 377 | # Linux: /home/홍길동/Downloads/naver-search-mcp 378 | ``` 379 | 380 | #### 3단계: Claude Desktop 재시작 381 | 382 | 설정 완료 후 Claude Desktop을 완전히 종료하고 다시 시작하면 Naver Search MCP 서버가 활성화됩니다. 383 | 384 | --- 385 | 386 | ## 대안 설치 방법 387 | 388 | ### 방법 4: Docker 설치 389 | 390 | 컨테이너 배포용: 391 | 392 | ```bash 393 | docker run -i --rm \ 394 | -e NAVER_CLIENT_ID=your_client_id \ 395 | -e NAVER_CLIENT_SECRET=your_client_secret \ 396 | mcp/naver-search 397 | ``` 398 | 399 | Claude Desktop용 Docker 설정: 400 | 401 | ```json 402 | { 403 | "mcpServers": { 404 | "naver-search": { 405 | "command": "docker", 406 | "args": [ 407 | "run", 408 | "-i", 409 | "--rm", 410 | "-e", 411 | "NAVER_CLIENT_ID=your_client_id", 412 | "-e", 413 | "NAVER_CLIENT_SECRET=your_client_secret", 414 | "mcp/naver-search" 415 | ] 416 | } 417 | } 418 | } 419 | ``` 420 | 421 | ## 라이선스 422 | 423 | MIT 라이선스 424 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Naver Search MCP Server 2 | 3 | [![한국어](https://img.shields.io/badge/한국어-README-yellow)](README-ko.md) 4 | 5 | [![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/isnow890/naver-search-mcp)](https://archestra.ai/mcp-catalog/isnow890__naver-search-mcp) 6 | [![smithery badge](https://smithery.ai/badge/@isnow890/naver-search-mcp)](https://smithery.ai/server/@isnow890/naver-search-mcp) 7 | [![MCP.so](https://img.shields.io/badge/MCP.so-Naver%20Search%20MCP-blue)](https://mcp.so/server/naver-search-mcp/isnow890) 8 | 9 | MCP server for Naver Search API and DataLab API integration, enabling comprehensive search across various Naver services and data trend analysis. 10 | 11 | #### Version History 12 | 13 | ###### 1.0.45 (2025-09-28) 14 | 15 | - Resolved Smithery compatibility issues so you can use the latest features through Smithery 16 | - Replaced the Excel export in category search with JSON for better compatibility 17 | - Restored the `search_webkr` tool for Korean web search 18 | - Fully compatible with Smithery platform installation 19 | 20 | ###### 1.0.44 (2025-08-31) 21 | 22 | - Added the `get_current_korean_time` tool for essential Korea Standard Time context 23 | - Referenced the time tool across existing tool descriptions for temporal queries 24 | - Improved handling of "today", "now", and "current" searches with temporal context 25 | - Expanded Korean date and time formatting outputs with multiple formats 26 | 27 | ###### 1.0.40 (2025-08-21) 28 | 29 | - Added the `find_category` tool with fuzzy matching so you no longer need to check category numbers manually in URLs 30 | - Enhanced parameter validation with Zod schema 31 | - Improved the category search workflow 32 | - Implemented a level-based category ranking system that prioritizes top-level categories 33 | 34 | ###### 1.0.30 (2025-08-04) 35 | 36 | - MCP SDK upgraded to 1.17.1 37 | - Fixed compatibility issues with Smithery specification changes 38 | - Added comprehensive DataLab shopping category code documentation 39 | 40 | ###### 1.0.2 (2025-04-26) 41 | 42 | - README updated: cafe article search tool and version history section improved 43 | 44 | ###### 1.0.1 (2025-04-26) 45 | 46 | - Cafe article search feature added 47 | - Shopping category info added to zod 48 | - Source code refactored 49 | 50 | ###### 1.0.0 (2025-04-08) 51 | 52 | - Initial release 53 | 54 | #### Prerequisites 55 | 56 | - Naver Developers API Key (Client ID and Secret) 57 | - Node.js 18 or higher 58 | - NPM 8 or higher 59 | - Docker (optional, for container deployment) 60 | 61 | #### Getting API Keys 62 | 63 | 1. Visit [Naver Developers](https://developers.naver.com/apps/#/register) 64 | 2. Click "Register Application" 65 | 3. Enter application name and select ALL of the following APIs: 66 | - Search (for blog, news, book search, etc.) 67 | - DataLab (Search Trends) 68 | - DataLab (Shopping Insight) 69 | 4. Set the obtained Client ID and Client Secret as environment variables 70 | 71 | ## Tool Details 72 | 73 | ### Available tools: 74 | 75 | #### 🕐 Time & Context Tools 76 | 77 | - **get_current_korean_time**: Fetch the current Korea Standard Time (KST) along with comprehensive date and time details. Use this whenever a search or analysis requires temporal context such as "today", "now", or "current" in Korea. 78 | 79 | #### 🆕 Category Search 80 | 81 | - **find_category**: Category search tool so you no longer need to manually check category numbers in URLs for trend and shopping insight searches. Just describe the category in natural language. 82 | 83 | #### Search Tools 84 | 85 | - **search_webkr**: Search Naver web documents 86 | - **search_news**: Search Naver news 87 | - **search_blog**: Search Naver blogs 88 | - **search_cafearticle**: Search Naver cafe articles 89 | - **search_shop**: Search Naver shopping 90 | - **search_image**: Search Naver images 91 | - **search_kin**: Search Naver KnowledgeiN 92 | - **search_book**: Search Naver books 93 | - **search_encyc**: Search Naver encyclopedia 94 | - **search_academic**: Search Naver academic papers 95 | - **search_local**: Search Naver local places 96 | 97 | #### DataLab Tools 98 | 99 | - **datalab_search**: Analyze search term trends 100 | - **datalab_shopping_category**: Analyze shopping category trends 101 | - **datalab_shopping_by_device**: Analyze shopping trends by device 102 | - **datalab_shopping_by_gender**: Analyze shopping trends by gender 103 | - **datalab_shopping_by_age**: Analyze shopping trends by age group 104 | - **datalab_shopping_keywords**: Analyze shopping keyword trends 105 | - **datalab_shopping_keyword_by_device**: Analyze shopping keyword trends by device 106 | - **datalab_shopping_keyword_by_gender**: Analyze shopping keyword trends by gender 107 | - **datalab_shopping_keyword_by_age**: Analyze shopping keyword trends by age group 108 | 109 | #### Complete Category List: 110 | 111 | For a complete list of category codes, you can download from Naver Shopping Partner Center or extract them by browsing Naver Shopping categories. 112 | 113 | ### 🎯 Business Use Cases & Scenarios 114 | 115 | #### 🛍️ E-commerce Market Research 116 | 117 | ```javascript 118 | // Fashion trend discovery 119 | find_category("fashion") → Check top fashion categories and codes 120 | datalab_shopping_category → Analyze seasonal fashion trends 121 | datalab_shopping_age → Identify fashion target demographics 122 | datalab_shopping_keywords → Compare "dress" vs "jacket" vs "coat" 123 | ``` 124 | 125 | #### 📱 Digital Marketing Strategy 126 | 127 | ```javascript 128 | // Beauty industry analysis 129 | find_category("cosmetics") → Find beauty categories 130 | datalab_shopping_gender → 95% female vs 5% male shoppers 131 | datalab_shopping_device → Mobile dominance in beauty shopping 132 | datalab_shopping_keywords → "tint" vs "lipstick" keyword performance 133 | ``` 134 | 135 | #### 🏢 Business Intelligence & Competitive Analysis 136 | 137 | ```javascript 138 | // Tech product insights 139 | find_category("smartphone") → Check electronics categories 140 | datalab_shopping_category → Track iPhone vs Galaxy trends 141 | datalab_shopping_age → 20-30s as main smartphone buyers 142 | datalab_shopping_device → PC vs mobile shopping behavior 143 | ``` 144 | 145 | #### 📊 Seasonal Business Planning 146 | 147 | ```javascript 148 | // Holiday shopping analysis 149 | find_category("gift") → Gift categories 150 | datalab_shopping_category → Black Friday, Christmas trends 151 | datalab_shopping_keywords → "Mother's Day gift" vs "birthday gift" 152 | datalab_shopping_age → Age-based gift purchasing patterns 153 | ``` 154 | 155 | #### 🎯 Customer Persona Development 156 | 157 | ```javascript 158 | // Fitness market analysis 159 | find_category("exercise") → Sports/fitness categories 160 | datalab_shopping_gender → Male vs female fitness spending 161 | datalab_shopping_age → Primary fitness demographics (20-40s) 162 | datalab_shopping_keywords → "home workout" vs "gym" trend analysis 163 | ``` 164 | 165 | ### 📈 Advanced Analysis Scenarios 166 | 167 | #### Market Entry Strategy 168 | 169 | 1. **Category Discovery**: Use `find_category` to explore market segments 170 | 2. **Trend Analysis**: Identify growing vs declining categories 171 | 3. **Demographic Targeting**: Age/gender analysis for customer targeting 172 | 4. **Competitive Intelligence**: Keyword performance comparison 173 | 5. **Device Strategy**: Mobile vs PC shopping optimization 174 | 175 | #### Product Launch Planning 176 | 177 | 1. **Market Validation**: Category growth trends and seasonality 178 | 2. **Target Customers**: Demographic analysis for product positioning 179 | 3. **Marketing Channels**: Device preferences for advertising strategy 180 | 4. **Competitive Landscape**: Keyword competition and opportunities 181 | 5. **Pricing Strategy**: Category performance and price correlation 182 | 183 | #### Performance Monitoring 184 | 185 | 1. **Category Health**: Monitor product category trends 186 | 2. **Keyword Tracking**: Track brand and product keyword performance 187 | 3. **Demographic Shifts**: Monitor changing customer demographics 188 | 4. **Seasonal Patterns**: Plan inventory and marketing campaigns 189 | 5. **Competitive Benchmarking**: Compare performance against category averages 190 | 191 | ### Quick Reference: Popular Category Codes 192 | 193 | | Category | Code | Korean | 194 | | ------------------- | -------- | ------------- | 195 | | Fashion/Clothing | 50000000 | 패션의류 | 196 | | Cosmetics/Beauty | 50000002 | 화장품/미용 | 197 | | Digital/Electronics | 50000003 | 디지털/가전 | 198 | | Sports/Leisure | 50000004 | 스포츠/레저 | 199 | | Food/Beverages | 50000008 | 식품/음료 | 200 | | Health/Medical | 50000009 | 건강/의료용품 | 201 | 202 | 💡 **Tip**: Use `find_category` with fuzzy searches like "beauty", "fashion", "electronics" to easily find categories. 203 | 204 | ## Installation 205 | 206 | ### Method 1: NPX Installation (Recommended) 207 | 208 | The most reliable way to use this MCP server is through NPX. For detailed package information, see the [NPM package page](https://www.npmjs.com/package/@isnow890/naver-search-mcp). 209 | 210 | #### Claude Desktop Configuration 211 | 212 | Add to Claude Desktop config file (`%APPDATA%\Claude\claude_desktop_config.json` on Windows, `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS/Linux): 213 | 214 | ```json 215 | { 216 | "mcpServers": { 217 | "naver-search": { 218 | "command": "npx", 219 | "args": ["-y", "@isnow890/naver-search-mcp"], 220 | "env": { 221 | "NAVER_CLIENT_ID": "your_client_id", 222 | "NAVER_CLIENT_SECRET": "your_client_secret" 223 | } 224 | } 225 | } 226 | } 227 | ``` 228 | 229 | #### Cursor AI Configuration 230 | 231 | Add to `mcp.json`: 232 | 233 | ```json 234 | { 235 | "mcpServers": { 236 | "naver-search": { 237 | "command": "npx", 238 | "args": ["-y", "@isnow890/naver-search-mcp"], 239 | "env": { 240 | "NAVER_CLIENT_ID": "your_client_id", 241 | "NAVER_CLIENT_SECRET": "your_client_secret" 242 | } 243 | } 244 | } 245 | } 246 | ``` 247 | 248 | ### Method 2: Smithery Installation (Alternative - Known Issues) 249 | 250 | ⚠️ **Important Notice**: Smithery installations can run into connection timeouts and freezes because of issues in the Smithery WebSocket relay infrastructure. This is a known platform limitation rather than a bug in this MCP server. For stable usage, we strongly recommend sticking with Method 1 (NPX installation). 251 | 252 | #### Known issues on Smithery: 253 | 254 | - Server initialization may hang or time out 255 | - `Error -32001: Request timed out` can appear 256 | - WebSocket connections can drop immediately after the handshake 257 | - The server can exit unexpectedly before processing requests 258 | 259 | If you still want to try Smithery: 260 | 261 | ##### For Claude Desktop: 262 | 263 | ```bash 264 | npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client claude 265 | ``` 266 | 267 | ##### For other AI clients: 268 | 269 | ```bash 270 | # Cursor 271 | npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client cursor 272 | 273 | # Windsurf 274 | npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client windsurf 275 | 276 | # Cline 277 | npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client cline 278 | ``` 279 | 280 | If you encounter timeouts on Smithery, switch back to Method 1 (NPX) for a stable experience. 281 | 282 | ### Method 3: Local Installation 283 | 284 | For local development or custom modifications: 285 | 286 | #### Step 1: Download and Build Source Code 287 | 288 | ##### Clone with Git 289 | 290 | ```bash 291 | git clone https://github.com/isnow890/naver-search-mcp.git 292 | cd naver-search-mcp 293 | npm install 294 | npm run build 295 | ``` 296 | 297 | ##### Or Download ZIP File 298 | 299 | 1. Download the latest version from [GitHub Releases](https://github.com/isnow890/naver-search-mcp/releases) 300 | 2. Extract the ZIP file to your desired location 301 | 3. Navigate to the extracted folder in terminal: 302 | 303 | ```bash 304 | cd /path/to/naver-search-mcp 305 | npm install 306 | npm run build 307 | ``` 308 | 309 | ⚠️ **Important**: You must run `npm run build` after installation to generate the `dist` folder that contains the compiled JavaScript files. 310 | 311 | #### Step 2: Claude Desktop Configuration 312 | 313 | After building, you'll need the following information: 314 | 315 | - **NAVER_CLIENT_ID**: Client ID from Naver Developers 316 | - **NAVER_CLIENT_SECRET**: Client Secret from Naver Developers 317 | - **Installation Path**: Absolute path to the downloaded folder 318 | 319 | ##### Windows Configuration 320 | 321 | Add to Claude Desktop config file (`%APPDATA%\Claude\claude_desktop_config.json`): 322 | 323 | ```json 324 | { 325 | "mcpServers": { 326 | "naver-search": { 327 | "type": "stdio", 328 | "command": "cmd", 329 | "args": [ 330 | "/c", 331 | "node", 332 | "C:\\path\\to\\naver-search-mcp\\dist\\src\\index.js" 333 | ], 334 | "cwd": "C:\\path\\to\\naver-search-mcp", 335 | "env": { 336 | "NAVER_CLIENT_ID": "your-naver-client-id", 337 | "NAVER_CLIENT_SECRET": "your-naver-client-secret" 338 | } 339 | } 340 | } 341 | } 342 | ``` 343 | 344 | ##### macOS/Linux Configuration 345 | 346 | Add to Claude Desktop config file (`~/Library/Application Support/Claude/claude_desktop_config.json`): 347 | 348 | ```json 349 | { 350 | "mcpServers": { 351 | "naver-search": { 352 | "type": "stdio", 353 | "command": "node", 354 | "args": ["/path/to/naver-search-mcp/dist/src/index.js"], 355 | "cwd": "/path/to/naver-search-mcp", 356 | "env": { 357 | "NAVER_CLIENT_ID": "your-naver-client-id", 358 | "NAVER_CLIENT_SECRET": "your-naver-client-secret" 359 | } 360 | } 361 | } 362 | } 363 | ``` 364 | 365 | ##### Path Configuration Important Notes 366 | 367 | ⚠️ **Important**: You must change the following paths in the above configuration to your actual installation paths: 368 | 369 | - **Windows**: Change `C:\\path\\to\\naver-search-mcp` to your actual downloaded folder path 370 | - **macOS/Linux**: Change `/path/to/naver-search-mcp` to your actual downloaded folder path 371 | - **Build Path**: Make sure the path points to `dist/src/index.js` (not just `index.js`) 372 | 373 | Finding your path: 374 | 375 | ```bash 376 | # Check current location 377 | pwd 378 | 379 | # Absolute path examples 380 | # Windows: C:\Users\username\Downloads\naver-search-mcp 381 | # macOS: /Users/username/Downloads/naver-search-mcp 382 | # Linux: /home/username/Downloads/naver-search-mcp 383 | ``` 384 | 385 | #### Step 3: Restart Claude Desktop 386 | 387 | After completing the configuration, completely close and restart Claude Desktop to activate the Naver Search MCP server. 388 | 389 | --- 390 | 391 | ## Alternative Installation Methods 392 | 393 | ### Method 4: Docker Installation 394 | 395 | For containerized deployment: 396 | 397 | ```bash 398 | docker run -i --rm \ 399 | -e NAVER_CLIENT_ID=your_client_id \ 400 | -e NAVER_CLIENT_SECRET=your_client_secret \ 401 | mcp/naver-search 402 | ``` 403 | 404 | Docker configuration for Claude Desktop: 405 | 406 | ```json 407 | { 408 | "mcpServers": { 409 | "naver-search": { 410 | "command": "docker", 411 | "args": [ 412 | "run", 413 | "-i", 414 | "--rm", 415 | "-e", 416 | "NAVER_CLIENT_ID=your_client_id", 417 | "-e", 418 | "NAVER_CLIENT_SECRET=your_client_secret", 419 | "mcp/naver-search" 420 | ] 421 | } 422 | } 423 | } 424 | ``` 425 | 426 | ## Build 427 | 428 | Docker build: 429 | 430 | ```bash 431 | docker build -t mcp/naver-search . 432 | ``` 433 | 434 | ## License 435 | 436 | MIT License 437 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { z } from "zod"; 5 | import { NaverSearchClient } from "./clients/naver-search.client.js"; 6 | import { searchToolHandlers } from "./handlers/search.handlers.js"; 7 | import { datalabToolHandlers } from "./handlers/datalab.handlers.js"; 8 | import { 9 | SearchArgsSchema, 10 | NaverLocalSearchParamsSchema, 11 | } from "./schemas/search.schemas.js"; 12 | import { 13 | DatalabSearchSchema, 14 | DatalabShoppingSchema, 15 | DatalabShoppingDeviceSchema, 16 | DatalabShoppingGenderSchema, 17 | DatalabShoppingAgeSchema, 18 | DatalabShoppingKeywordsSchema, 19 | DatalabShoppingKeywordDeviceSchema, 20 | DatalabShoppingKeywordGenderSchema, 21 | DatalabShoppingKeywordAgeSchema, 22 | } from "./schemas/datalab.schemas.js"; 23 | import { FindCategorySchema } from "./schemas/category.schemas.js"; 24 | import { findCategoryHandler } from "./handlers/category.handlers.js"; 25 | import { GetKoreanTimeSchema } from "./schemas/time.schemas.js"; 26 | import { timeToolHandlers } from "./handlers/time.handlers.js"; 27 | import { startGlobalMemoryMonitoring, stopGlobalMemoryMonitoring } from "./utils/memory-manager.js"; 28 | 29 | // Configuration schema for Smithery 30 | export const configSchema = z.object({ 31 | NAVER_CLIENT_ID: z.string().describe("Naver API Client ID"), 32 | NAVER_CLIENT_SECRET: z.string().describe("Naver API Client Secret"), 33 | }); 34 | 35 | // Global server instance to prevent memory leaks 36 | let globalServerInstance: McpServer | null = null; 37 | let currentConfig: z.infer | null = null; 38 | 39 | /** 40 | * 서버 인스턴스와 관련 리소스 정리 (메모리 누수 방지) 41 | */ 42 | export function resetServerInstance(): void { 43 | if (globalServerInstance) { 44 | // 메모리 모니터링 중지 45 | stopGlobalMemoryMonitoring(); 46 | 47 | // 클라이언트 인스턴스 정리 48 | NaverSearchClient.destroyInstance(); 49 | 50 | // 카테고리 캐시 정리 51 | import('./handlers/category.handlers.js').then(({ clearCategoriesCache }) => { 52 | clearCategoriesCache(); 53 | }); 54 | 55 | globalServerInstance = null; 56 | currentConfig = null; 57 | 58 | console.error("Server instance and resources cleaned up"); 59 | } 60 | } 61 | 62 | /** 63 | * 설정 변경 감지 함수 64 | */ 65 | function isConfigChanged(newConfig: z.infer): boolean { 66 | if (!currentConfig) return true; 67 | return ( 68 | currentConfig.NAVER_CLIENT_ID !== newConfig.NAVER_CLIENT_ID || 69 | currentConfig.NAVER_CLIENT_SECRET !== newConfig.NAVER_CLIENT_SECRET 70 | ); 71 | } 72 | 73 | export function createNaverSearchServer({ 74 | config, 75 | }: { 76 | config: z.infer; 77 | }) { 78 | // 설정이 변경된 경우 기존 인스턴스 정리 79 | if (globalServerInstance && isConfigChanged(config)) { 80 | console.error("Configuration changed, resetting server instance"); 81 | resetServerInstance(); 82 | } 83 | 84 | // Reuse existing server instance to prevent memory leaks 85 | if (globalServerInstance) { 86 | return globalServerInstance; 87 | } 88 | 89 | // Create a new MCP server only once 90 | const server = new McpServer({ 91 | name: "naver-search", 92 | version: "1.0.44", 93 | }); 94 | 95 | // Initialize Naver client with config 96 | const client = NaverSearchClient.getInstance(); 97 | client.initialize({ 98 | clientId: config.NAVER_CLIENT_ID, 99 | clientSecret: config.NAVER_CLIENT_SECRET, 100 | }); 101 | 102 | server.registerTool( 103 | "search_webkr", 104 | { 105 | description: 106 | "🌐 Search Korean web documents and general content. Comprehensive search across Korean websites and online content. Find articles, information, and documents from various Korean sources. For recent content or 'today's results', use get_current_korean_time first. (네이버 웹문서 검색 - 한국 웹사이트 종합 검색, 최근 콘텐츠나 오늘 결과를 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)", 107 | inputSchema: SearchArgsSchema.shape, 108 | }, 109 | async (args) => { 110 | const result = await searchToolHandlers.search_webkr(args); 111 | return { 112 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 113 | }; 114 | } 115 | ); 116 | 117 | server.registerTool( 118 | "search_news", 119 | { 120 | description: 121 | "📰 Search latest Korean news articles from major outlets. Perfect for current events, breaking news, and recent developments. Covers politics, economy, society, and international news. For today's news or current events, use get_current_korean_time first to understand what 'today' means. (네이버 뉴스 검색 - 최신 뉴스와 시사 정보, 오늘 뉴스를 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)", 122 | inputSchema: SearchArgsSchema.shape, 123 | }, 124 | async (args) => { 125 | const result = await searchToolHandlers.search_news(args); 126 | return { 127 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 128 | }; 129 | } 130 | ); 131 | 132 | server.registerTool( 133 | "search_blog", 134 | { 135 | description: 136 | "✍️ Search personal blogs and reviews for authentic user experiences. Great for product reviews, personal stories, detailed tutorials, and real user opinions. Find genuine Korean perspectives. For recent posts or 'today's content', use get_current_korean_time first. (네이버 블로그 검색 - 실제 사용자 후기와 개인적 경험, 최근 글이나 오늘 내용을 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)", 137 | inputSchema: SearchArgsSchema.shape, 138 | }, 139 | async (args) => { 140 | const result = await searchToolHandlers.search_blog(args); 141 | return { 142 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 143 | }; 144 | } 145 | ); 146 | 147 | server.registerTool( 148 | "search_shop", 149 | { 150 | description: 151 | "🛒 Search Naver Shopping for products, prices, and shopping deals. Compare prices across vendors, find product specifications, and discover shopping trends in Korea. For current deals or today's specials, use get_current_korean_time first. (네이버 쇼핑 검색 - 상품 정보와 가격 비교, 현재 할인이나 오늘 특가를 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)", 152 | inputSchema: SearchArgsSchema.shape, 153 | }, 154 | async (args) => { 155 | const result = await searchToolHandlers.search_shop(args); 156 | return { 157 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 158 | }; 159 | } 160 | ); 161 | 162 | server.registerTool( 163 | "search_image", 164 | { 165 | description: 166 | "🖼️ Search for images with Korean context and relevance. Find visual content, infographics, charts, and photos related to your search terms. Great for visual research and content discovery. For recent images or current visual content, use get_current_korean_time first. (네이버 이미지 검색 - 시각적 컨텐츠 발견, 최근 이미지나 현재 시각 자료를 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)", 167 | inputSchema: SearchArgsSchema.shape, 168 | }, 169 | async (args) => { 170 | const result = await searchToolHandlers.search_image(args); 171 | return { 172 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 173 | }; 174 | } 175 | ); 176 | 177 | server.registerTool( 178 | "search_kin", 179 | { 180 | description: 181 | "❓ Search Naver KnowledgeiN for Q&A and community-driven answers. Find solutions to problems, get expert advice, and discover community insights on various topics. For recent questions or current discussions, use get_current_korean_time first. (네이버 지식iN 검색 - 질문과 답변, 커뮤니티 지식, 최근 질문이나 현재 토론을 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)", 182 | inputSchema: SearchArgsSchema.shape, 183 | }, 184 | async (args) => { 185 | const result = await searchToolHandlers.search_kin(args); 186 | return { 187 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 188 | }; 189 | } 190 | ); 191 | 192 | server.registerTool( 193 | "search_book", 194 | { 195 | description: 196 | "📚 Search for books, publications, and literary content. Find book reviews, author information, publication details, and reading recommendations in Korean literature and translated works. For new releases or current bestsellers, use get_current_korean_time first. (네이버 책 검색 - 도서 정보와 서평, 신간도서나 현재 베스트셀러를 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)", 197 | inputSchema: SearchArgsSchema.shape, 198 | }, 199 | async (args) => { 200 | const result = await searchToolHandlers.search_book(args); 201 | return { 202 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 203 | }; 204 | } 205 | ); 206 | 207 | server.registerTool( 208 | "search_encyc", 209 | { 210 | description: 211 | "📖 Search Naver Encyclopedia for authoritative knowledge and definitions. Best for academic research, getting reliable information, and understanding Korean concepts and terminology. For current definitions or recent updates, use get_current_korean_time for context. (네이버 지식백과 검색 - 신뢰할 수 있는 정보와 정의, 현재 정의나 최근 업데이트를 찾을 때는 get_current_korean_time으로 상황을 파악하세요)", 212 | inputSchema: SearchArgsSchema.shape, 213 | }, 214 | async (args) => { 215 | const result = await searchToolHandlers.search_encyc(args); 216 | return { 217 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 218 | }; 219 | } 220 | ); 221 | 222 | server.registerTool( 223 | "search_academic", 224 | { 225 | description: 226 | "🎓 Search academic papers, research documents, and scholarly content. Access Korean academic resources, research papers, theses, and professional publications. For recent publications or current research, use get_current_korean_time first. (네이버 전문자료 검색 - 학술 논문과 전문 자료, 최근 발표나 현재 연구를 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)", 227 | inputSchema: SearchArgsSchema.shape, 228 | }, 229 | async (args) => { 230 | const result = await searchToolHandlers.search_academic(args); 231 | return { 232 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 233 | }; 234 | } 235 | ); 236 | 237 | server.registerTool( 238 | "search_local", 239 | { 240 | description: 241 | "📍 Search for local businesses, restaurants, and places in Korea. Find location information, reviews, contact details, and business hours for Korean establishments. For current business hours or today's availability, use get_current_korean_time first. (네이버 지역 검색 - 지역 업체와 장소 정보, 현재 영업시간이나 오늘 이용 가능 여부를 확인할 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)", 242 | inputSchema: NaverLocalSearchParamsSchema.shape, 243 | }, 244 | async (args) => { 245 | const result = await searchToolHandlers.search_local(args); 246 | return { 247 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 248 | }; 249 | } 250 | ); 251 | 252 | server.registerTool( 253 | "search_cafearticle", 254 | { 255 | description: 256 | "☕ Search Naver Cafe articles for community discussions and specialized content. Find niche communities, hobby groups, and specialized discussions on various topics. For recent discussions or current community topics, use get_current_korean_time first. (네이버 카페글 검색 - 커뮤니티 토론과 전문 정보, 최근 논의나 현재 커뮤니티 주제를 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)", 257 | inputSchema: SearchArgsSchema.shape, 258 | }, 259 | async (args) => { 260 | const result = await searchToolHandlers.search_cafearticle(args); 261 | return { 262 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 263 | }; 264 | } 265 | ); 266 | 267 | // Register datalab tools 268 | server.registerTool( 269 | "datalab_search", 270 | { 271 | description: 272 | "📊 Analyze search keyword trends over time using Naver DataLab. Track popularity changes, seasonal patterns, and compare multiple keywords. Perfect for market research and trend analysis. For current trend analysis or 'recent trends', use get_current_korean_time to determine proper date ranges. (네이버 데이터랩 검색어 트렌드 분석, 현재 트렌드나 최근 동향 분석 시 get_current_korean_time으로 적절한 날짜 범위를 설정하세요)", 273 | inputSchema: DatalabSearchSchema.shape, 274 | }, 275 | async (args) => { 276 | const result = await datalabToolHandlers.datalab_search(args); 277 | return { 278 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 279 | }; 280 | } 281 | ); 282 | 283 | server.registerTool( 284 | "datalab_shopping_category", 285 | { 286 | description: 287 | "🛍️ STEP 2: Analyze shopping category trends over time. Use find_category first to get category codes. BUSINESS CASES: Market size analysis, seasonal trend identification, category performance comparison. EXAMPLE: Compare '패션의류' vs '화장품' trends over 6 months. For current period analysis, use get_current_korean_time to set proper date ranges. (네이버 쇼핑 카테고리별 트렌드 분석 - 먼저 find_category 도구로 카테고리 코드를 찾고, 현재 기간 분석시 get_current_korean_time으로 적절한 날짜 범위 설정)", 288 | inputSchema: DatalabShoppingSchema.shape, 289 | }, 290 | async (args) => { 291 | const result = await datalabToolHandlers.datalab_shopping_category(args); 292 | return { 293 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 294 | }; 295 | } 296 | ); 297 | 298 | server.registerTool( 299 | "datalab_shopping_by_device", 300 | { 301 | description: 302 | "📱 Analyze shopping trends by device (PC vs Mobile). Use find_category first. BUSINESS CASES: Mobile commerce strategy, responsive design priority, device-specific campaigns. EXAMPLE: 'PC 사용자가 더 많이 구매하는 카테고리는?' For current device trends, use get_current_korean_time to set proper analysis period. (기기별 쇼핑 트렌드 분석 - 먼저 find_category 도구로 카테고리 코드를 찾고, 현재 기기 트렌드 분석시 get_current_korean_time으로 적절한 분석 기간 설정)", 303 | inputSchema: DatalabShoppingDeviceSchema.pick({ 304 | startDate: true, 305 | endDate: true, 306 | timeUnit: true, 307 | category: true, 308 | device: true, 309 | }).shape, 310 | }, 311 | async (args) => { 312 | const result = await datalabToolHandlers.datalab_shopping_by_device(args); 313 | return { 314 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 315 | }; 316 | } 317 | ); 318 | 319 | server.registerTool( 320 | "datalab_shopping_by_gender", 321 | { 322 | description: 323 | "👥 Analyze shopping trends by gender (Male vs Female). Use find_category first. BUSINESS CASES: Gender-targeted marketing, product positioning, demographic analysis. EXAMPLE: '화장품 쇼핑에서 남녀 비율은?' For current gender trends, use get_current_korean_time to set proper analysis period. (성별 쇼핑 트렌드 분석 - 먼저 find_category 도구로 카테고리 코드를 찾고, 현재 성별 트렌드 분석시 get_current_korean_time으로 적절한 분석 기간 설정)", 324 | inputSchema: DatalabShoppingGenderSchema.pick({ 325 | startDate: true, 326 | endDate: true, 327 | timeUnit: true, 328 | category: true, 329 | gender: true, 330 | }).shape, 331 | }, 332 | async (args) => { 333 | const result = await datalabToolHandlers.datalab_shopping_by_gender(args); 334 | return { 335 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 336 | }; 337 | } 338 | ); 339 | 340 | server.registerTool( 341 | "datalab_shopping_by_age", 342 | { 343 | description: 344 | "👶👦👨👴 Analyze shopping trends by age groups (10s, 20s, 30s, 40s, 50s, 60s+). Use find_category first. BUSINESS CASES: Age-targeted products, generational preferences, lifecycle marketing. EXAMPLE: '개발 도구는 어느 연령대가 많이 구매하나?' For current age trends, use get_current_korean_time to set proper analysis period. (연령별 쇼핑 트렌드 - 먼저 find_category 도구로 카테고리 코드를 찾고, 현재 연령 트렌드 분석시 get_current_korean_time으로 적절한 분석 기간 설정)", 345 | inputSchema: DatalabShoppingAgeSchema.pick({ 346 | startDate: true, 347 | endDate: true, 348 | timeUnit: true, 349 | category: true, 350 | ages: true, 351 | }).shape, 352 | }, 353 | async (args) => { 354 | const result = await datalabToolHandlers.datalab_shopping_by_age(args); 355 | return { 356 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 357 | }; 358 | } 359 | ); 360 | 361 | server.registerTool( 362 | "datalab_shopping_keywords", 363 | { 364 | description: 365 | "🔍 Compare specific keywords within a shopping category. Use find_category first. BUSINESS CASES: Product keyword optimization, competitor analysis, search trend identification. EXAMPLE: Within '패션' category, compare '원피스' vs '자켓' vs '드레스' trends. For current keyword trends, use get_current_korean_time to set proper analysis period. (카테고리 내 키워드 비교 - 먼저 find_category 도구로 카테고리 코드를 찾고, 현재 키워드 트렌드 분석시 get_current_korean_time으로 적절한 분석 기간 설정)", 366 | inputSchema: DatalabShoppingKeywordsSchema.shape, 367 | }, 368 | async (args) => { 369 | const result = await datalabToolHandlers.datalab_shopping_keywords(args); 370 | return { 371 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 372 | }; 373 | } 374 | ); 375 | 376 | server.registerTool( 377 | "datalab_shopping_keyword_by_device", 378 | { 379 | description: 380 | "📱🔍 Analyze keyword performance by device within shopping categories. Use find_category first to get category codes. Perfect for understanding mobile vs desktop shopping behavior for specific products. (쇼핑 키워드 기기별 트렌드 - 먼저 find_category 도구로 카테고리 코드를 찾으세요)", 381 | inputSchema: DatalabShoppingKeywordDeviceSchema.shape, 382 | }, 383 | async (args) => { 384 | const result = 385 | await datalabToolHandlers.datalab_shopping_keyword_by_device(args); 386 | return { 387 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 388 | }; 389 | } 390 | ); 391 | 392 | server.registerTool( 393 | "datalab_shopping_keyword_by_gender", 394 | { 395 | description: 396 | "👥🔍 Analyze keyword performance by gender within shopping categories. Use find_category first to get category codes. Essential for gender-targeted marketing and product positioning strategies. (쇼핑 키워드 성별 트렌드 - 먼저 find_category 도구로 카테고리 코드를 찾으세요)", 397 | inputSchema: DatalabShoppingKeywordGenderSchema.shape, 398 | }, 399 | async (args) => { 400 | const result = 401 | await datalabToolHandlers.datalab_shopping_keyword_by_gender(args); 402 | return { 403 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 404 | }; 405 | } 406 | ); 407 | 408 | server.registerTool( 409 | "datalab_shopping_keyword_by_age", 410 | { 411 | description: 412 | "👶👦👨👴🔍 Analyze keyword performance by age groups within shopping categories. Use find_category first to get category codes. Perfect for age-targeted marketing and understanding generational shopping preferences. (쇼핑 키워드 연령별 트렌드 - 먼저 find_category 도구로 카테고리 코드를 찾으세요)", 413 | inputSchema: DatalabShoppingKeywordAgeSchema.shape, 414 | }, 415 | async (args) => { 416 | const result = await datalabToolHandlers.datalab_shopping_keyword_by_age( 417 | args 418 | ); 419 | return { 420 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 421 | }; 422 | } 423 | ); 424 | 425 | // Register current time tool 426 | server.registerTool( 427 | "get_current_korean_time", 428 | { 429 | description: 430 | "🕐 Get current Korean time (KST) with date and time information. Use this tool whenever you need to know 'today', 'now', 'current time', or any time-related queries. Essential for understanding what 'today' means in Korean context. Always use this tool when users mention 'today' or 'current' to provide accurate time context. (현재 한국 시간 조회 - '오늘', '현재', '지금' 등의 시간 정보가 필요할 때 항상 사용하세요)", 431 | inputSchema: GetKoreanTimeSchema.shape, 432 | }, 433 | async (args) => { 434 | const result = await timeToolHandlers.getCurrentKoreanTime(args); 435 | return { 436 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 437 | }; 438 | } 439 | ); 440 | 441 | // Register category search tool 442 | server.registerTool( 443 | "find_category", 444 | { 445 | description: 446 | "🚀 STEP 1: Find shopping categories with Korean search terms. Search in KOREAN (패션, 화장품, 가전제품, etc.) to find category codes needed for datalab tools. Smart fuzzy matching finds similar categories even with partial matches. (카테고리 검색: 한국어로 검색하여 데이터랩 분석에 필요한 카테고리 코드를 찾아주는 필수 도구)", 447 | inputSchema: FindCategorySchema.shape, 448 | }, 449 | async (args) => { 450 | const result = await findCategoryHandler(args); 451 | return { 452 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 453 | }; 454 | } 455 | ); 456 | 457 | // Cache the server instance and config 458 | globalServerInstance = server; 459 | currentConfig = config; 460 | 461 | // 메모리 모니터링 시작 (5분 간격) 462 | startGlobalMemoryMonitoring(5 * 60 * 1000); 463 | console.error("Memory monitoring started"); 464 | 465 | return server.server; 466 | } 467 | 468 | // Export default for Smithery compatibility 469 | export default createNaverSearchServer; 470 | 471 | // Main function to run the server when executed directly 472 | async function main() { 473 | try { 474 | console.error("Starting Naver Search MCP Server..."); 475 | 476 | // Get config from environment variables - check for empty strings too 477 | const clientId = process.env.NAVER_CLIENT_ID?.trim(); 478 | const clientSecret = process.env.NAVER_CLIENT_SECRET?.trim(); 479 | 480 | console.error("Environment variables:", { 481 | NAVER_CLIENT_ID: process.env.NAVER_CLIENT_ID 482 | ? `[${process.env.NAVER_CLIENT_ID.length} chars]` 483 | : "undefined", 484 | NAVER_CLIENT_SECRET: process.env.NAVER_CLIENT_SECRET 485 | ? `[${process.env.NAVER_CLIENT_SECRET.length} chars]` 486 | : "undefined", 487 | }); 488 | 489 | if (!clientId || !clientSecret) { 490 | throw new Error(`Missing required environment variables: 491 | NAVER_CLIENT_ID: ${clientId ? "provided" : "missing"} 492 | NAVER_CLIENT_SECRET: ${clientSecret ? "provided" : "missing"} 493 | 494 | Please set these environment variables before running the server.`); 495 | } 496 | 497 | const config = { 498 | NAVER_CLIENT_ID: clientId, 499 | NAVER_CLIENT_SECRET: clientSecret, 500 | }; 501 | 502 | console.error("Config loaded successfully"); 503 | 504 | // Validate config 505 | const validatedConfig = configSchema.parse(config); 506 | console.error("Config validated successfully"); 507 | 508 | // Create server instance 509 | const serverFactory = createNaverSearchServer({ config: validatedConfig }); 510 | console.error("Server factory created"); 511 | 512 | // Create transport and run server 513 | const transport = new StdioServerTransport(); 514 | console.error("Transport created, connecting..."); 515 | 516 | await serverFactory.connect(transport); 517 | console.error("Server connected and running"); 518 | } catch (error) { 519 | console.error("Error in main function:", error); 520 | throw error; 521 | } 522 | } 523 | 524 | // Run main function if this file is executed directly 525 | // Note: Always run main in CLI mode since this is an MCP server 526 | console.error("Starting MCP server initialization..."); 527 | console.error("process.argv:", process.argv); 528 | 529 | // Check if running as main module - compatible with both ESM and CommonJS 530 | let isMainModule = false; 531 | try { 532 | // Try ESM approach first 533 | if (typeof import.meta !== "undefined" && import.meta.url) { 534 | console.error("import.meta.url:", import.meta.url); 535 | isMainModule = 536 | import.meta.url === `file://${process.argv[1]}` || 537 | import.meta.url.endsWith(process.argv[1]) || 538 | process.argv[1].endsWith("index.js"); 539 | } else { 540 | // Fallback for CommonJS or when import.meta is not available 541 | isMainModule = 542 | process.argv[1].endsWith("index.js") || 543 | process.argv[1].includes("naver-search-mcp"); 544 | } 545 | 546 | // Additional check for NPX execution 547 | if (!isMainModule && process.argv.some(arg => arg.includes('naver-search-mcp'))) { 548 | isMainModule = true; 549 | console.error("Detected NPX execution, forcing main module mode"); 550 | } 551 | } catch (error) { 552 | // Fallback for environments where import.meta causes issues 553 | isMainModule = 554 | process.argv[1].endsWith("index.js") || 555 | process.argv[1].includes("naver-search-mcp"); 556 | } 557 | 558 | console.error("isMainModule:", isMainModule); 559 | 560 | if (isMainModule) { 561 | console.error("Running as main module, starting server..."); 562 | main().catch((error) => { 563 | console.error("Server failed to start:", error); 564 | process.exit(1); 565 | }); 566 | } else { 567 | console.error("Not running as main module, skipping server start"); 568 | } 569 | --------------------------------------------------------------------------------