@isnow890/naver-search-mcp 1.0.20 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,30 @@
1
1
  # Naver Search MCP Server
2
2
 
3
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)
4
6
  [![smithery badge](https://smithery.ai/badge/@isnow890/naver-search-mcp)](https://smithery.ai/server/@isnow890/naver-search-mcp)
5
7
  [![MCP.so](https://img.shields.io/badge/MCP.so-Naver%20Search%20MCP-blue)](https://mcp.so/server/naver-search-mcp/isnow890)
6
8
 
7
9
  MCP server for Naver Search API and DataLab API integration, enabling comprehensive search across various Naver services and data trend analysis.
8
10
 
11
+ > ⚠️ **Smithery Installation Notice**: Due to compatibility issues with the Smithery platform, **direct installation is recommended starting from version 1.0.40**. Smithery installation is only supported up to version 1.0.30.
12
+
9
13
  #### Version History
10
14
 
15
+ ###### 1.0.4 (2025-08-21)
16
+
17
+ - `find_category` tool added - with fuzzy matching and ranking system support
18
+ - Enhanced parameter validation with Zod schema
19
+ - Improved category search workflow
20
+ - Implemented level-based category ranking system (major categories prioritized)
21
+
22
+ ###### 1.0.30 (2025-08-04)
23
+
24
+ - MCP SDK upgraded to 1.17.1
25
+ - Fixed compatibility issues with Smithery specification changes
26
+ - Added comprehensive DataLab shopping category code documentation
27
+
11
28
  ###### 1.0.2 (2025-04-26)
12
29
 
13
30
  - README updated: cafe article search tool and version history section improved
@@ -47,6 +64,12 @@ MCP server for Naver Search API and DataLab API integration, enabling comprehens
47
64
 
48
65
  ### Available tools:
49
66
 
67
+ #### 🆕 Category Search
68
+
69
+ - **find_category**: Category search tool - No more need to manually check category numbers via URL for trend and shopping insight searches.
70
+
71
+ #### Search Tools
72
+
50
73
  - **search_webkr**: Search Naver web documents
51
74
  - **search_news**: Search Naver news
52
75
  - **search_blog**: Search Naver blogs
@@ -58,6 +81,9 @@ MCP server for Naver Search API and DataLab API integration, enabling comprehens
58
81
  - **search_encyc**: Search Naver encyclopedia
59
82
  - **search_academic**: Search Naver academic papers
60
83
  - **search_local**: Search Naver local places
84
+
85
+ #### DataLab Tools
86
+
61
87
  - **datalab_search**: Analyze search term trends
62
88
  - **datalab_shopping_category**: Analyze shopping category trends
63
89
  - **datalab_shopping_by_device**: Analyze shopping trends by device
@@ -68,42 +94,249 @@ MCP server for Naver Search API and DataLab API integration, enabling comprehens
68
94
  - **datalab_shopping_keyword_by_gender**: Analyze shopping keyword trends by gender
69
95
  - **datalab_shopping_keyword_by_age**: Analyze shopping keyword trends by age group
70
96
 
97
+ ### DataLab Shopping Category Codes
98
+
99
+ For DataLab shopping analysis tools, you need to use specific **8-digit category codes**. Here are some common examples:
100
+
101
+ #### Popular Category Codes:
102
+
103
+ - **50000000**: Fashion/Clothing (패션의류)
104
+ - **50000002**: Cosmetics/Beauty (화장품/미용)
105
+ - **50000003**: Digital/Home Appliances (디지털/가전)
106
+ - **50000004**: Sports/Leisure (스포츠/레저)
107
+ - **50000005**: Automotive/Tools (자동차/공구)
108
+ - **50000006**: Books/Music/DVDs (도서/음반/DVD)
109
+ - **50000007**: Toys/Hobbies (완구/취미)
110
+ - **50000008**: Food/Beverages (식품/음료)
111
+ - **50000009**: Health/Medical (건강/의료용품)
112
+ - **50000010**: Household/Living (생활/주방용품)
113
+
114
+ #### How to Find Category Codes:
115
+
116
+ 1. Visit [Naver Shopping](https://shopping.naver.com)
117
+ 2. Navigate to desired category
118
+ 3. Check the `cat_id` parameter in the URL
119
+ 4. Use the 8-digit code (e.g., `cat_id=50000000` → use `"50000000"`)
120
+
121
+ #### Usage Example:
122
+
123
+ When using shopping category tools, specify category like:
124
+
125
+ ```json
126
+ {
127
+ "category": "50000000",
128
+ "startDate": "2024-01-01",
129
+ "endDate": "2024-01-31",
130
+ "timeUnit": "month"
131
+ }
132
+ ```
133
+
134
+ #### Complete Category List:
135
+
136
+ For a complete list of category codes, you can download from Naver Shopping Partner Center or extract them by browsing Naver Shopping categories.
137
+
138
+ ### 🎯 Business Use Cases & Scenarios
139
+
140
+ #### 🛍️ E-commerce Market Research
141
+
142
+ ```javascript
143
+ // Fashion trend discovery
144
+ find_category("fashion") → Check top fashion categories and codes
145
+ datalab_shopping_category → Analyze seasonal fashion trends
146
+ datalab_shopping_age → Identify fashion target demographics
147
+ datalab_shopping_keywords → Compare "dress" vs "jacket" vs "coat"
148
+ ```
149
+
150
+ #### 📱 Digital Marketing Strategy
151
+
152
+ ```javascript
153
+ // Beauty industry analysis
154
+ find_category("cosmetics") → Find beauty categories
155
+ datalab_shopping_gender → 95% female vs 5% male shoppers
156
+ datalab_shopping_device → Mobile dominance in beauty shopping
157
+ datalab_shopping_keywords → "tint" vs "lipstick" keyword performance
158
+ ```
159
+
160
+ #### 🏢 Business Intelligence & Competitive Analysis
161
+
162
+ ```javascript
163
+ // Tech product insights
164
+ find_category("smartphone") → Check electronics categories
165
+ datalab_shopping_category → Track iPhone vs Galaxy trends
166
+ datalab_shopping_age → 20-30s as main smartphone buyers
167
+ datalab_shopping_device → PC vs mobile shopping behavior
168
+ ```
169
+
170
+ #### 📊 Seasonal Business Planning
171
+
172
+ ```javascript
173
+ // Holiday shopping analysis
174
+ find_category("gift") → Gift categories
175
+ datalab_shopping_category → Black Friday, Christmas trends
176
+ datalab_shopping_keywords → "Mother's Day gift" vs "birthday gift"
177
+ datalab_shopping_age → Age-based gift purchasing patterns
178
+ ```
179
+
180
+ #### 🎯 Customer Persona Development
181
+
182
+ ```javascript
183
+ // Fitness market analysis
184
+ find_category("exercise") → Sports/fitness categories
185
+ datalab_shopping_gender → Male vs female fitness spending
186
+ datalab_shopping_age → Primary fitness demographics (20-40s)
187
+ datalab_shopping_keywords → "home workout" vs "gym" trend analysis
188
+ ```
189
+
190
+ ### 📈 Advanced Analysis Scenarios
191
+
192
+ #### Market Entry Strategy
193
+
194
+ 1. **Category Discovery**: Use `find_category` to explore market segments
195
+ 2. **Trend Analysis**: Identify growing vs declining categories
196
+ 3. **Demographic Targeting**: Age/gender analysis for customer targeting
197
+ 4. **Competitive Intelligence**: Keyword performance comparison
198
+ 5. **Device Strategy**: Mobile vs PC shopping optimization
199
+
200
+ #### Product Launch Planning
201
+
202
+ 1. **Market Validation**: Category growth trends and seasonality
203
+ 2. **Target Customers**: Demographic analysis for product positioning
204
+ 3. **Marketing Channels**: Device preferences for advertising strategy
205
+ 4. **Competitive Landscape**: Keyword competition and opportunities
206
+ 5. **Pricing Strategy**: Category performance and price correlation
207
+
208
+ #### Performance Monitoring
209
+
210
+ 1. **Category Health**: Monitor product category trends
211
+ 2. **Keyword Tracking**: Track brand and product keyword performance
212
+ 3. **Demographic Shifts**: Monitor changing customer demographics
213
+ 4. **Seasonal Patterns**: Plan inventory and marketing campaigns
214
+ 5. **Competitive Benchmarking**: Compare performance against category averages
215
+
216
+ ### Quick Reference: Popular Category Codes
217
+
218
+ | Category | Code | Korean |
219
+ | ------------------- | -------- | ------------- |
220
+ | Fashion/Clothing | 50000000 | 패션의류 |
221
+ | Cosmetics/Beauty | 50000002 | 화장품/미용 |
222
+ | Digital/Electronics | 50000003 | 디지털/가전 |
223
+ | Sports/Leisure | 50000004 | 스포츠/레저 |
224
+ | Food/Beverages | 50000008 | 식품/음료 |
225
+ | Health/Medical | 50000009 | 건강/의료용품 |
226
+
227
+ 💡 **Tip**: Use `find_category` with fuzzy searches like "beauty", "fashion", "electronics" to easily find categories.
228
+
71
229
  ## Installation
72
230
 
73
- ### Option 1: Quick Install via Smithery (Recommended)
231
+ ### Step 1: Download Source Code
74
232
 
75
- To install Naver Search MCP Server automatically via Smithery, use one of these commands based on your AI client:
233
+ To use this MCP server, you need to download the source code first:
76
234
 
77
- For Claude Desktop:
235
+ #### Clone with Git
78
236
 
79
237
  ```bash
80
- npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client claude
238
+ git clone https://github.com/isnow890/naver-search-mcp.git
239
+ cd naver-search-mcp
240
+ npm install
241
+ npm run build
81
242
  ```
82
243
 
83
- For Cursor:
244
+ #### Or Download ZIP File
245
+
246
+ 1. Download the latest version from [GitHub Releases](https://github.com/isnow890/naver-search-mcp/releases)
247
+ 2. Extract the ZIP file to your desired location
248
+ 3. Navigate to the extracted folder in terminal:
84
249
 
85
250
  ```bash
86
- npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client cursor
251
+ cd /path/to/naver-search-mcp
252
+ npm install
253
+ npm run build
87
254
  ```
88
255
 
89
- For Windsurf:
256
+ ### Step 2: Claude Desktop Configuration
90
257
 
91
- ```bash
92
- npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client windsurf
258
+ After installation, you'll need the following information:
259
+
260
+ - **NAVER_CLIENT_ID**: Client ID from Naver Developers
261
+ - **NAVER_CLIENT_SECRET**: Client Secret from Naver Developers
262
+ - **Installation Path**: Absolute path to the downloaded folder
263
+
264
+ #### Windows Configuration
265
+
266
+ Add to Claude Desktop config file (`%APPDATA%\Claude\claude_desktop_config.json`):
267
+
268
+ ```json
269
+ {
270
+ "mcpServers": {
271
+ "naver-search": {
272
+ "type": "stdio",
273
+ "command": "cmd",
274
+ "args": [
275
+ "/c",
276
+ "node",
277
+ "C:\\path\\to\\naver-search-mcp\\dist\\src\\index.js"
278
+ ],
279
+ "cwd": "C:\\path\\to\\naver-search-mcp",
280
+ "env": {
281
+ "NAVER_CLIENT_ID": "your-naver-client-id",
282
+ "NAVER_CLIENT_SECRET": "your-naver-client-secret"
283
+ }
284
+ }
285
+ }
286
+ }
287
+ ```
288
+
289
+ #### macOS/Linux Configuration
290
+
291
+ Add to Claude Desktop config file (`~/Library/Application Support/Claude/claude_desktop_config.json`):
292
+
293
+ ```json
294
+ {
295
+ "mcpServers": {
296
+ "naver-search": {
297
+ "type": "stdio",
298
+ "command": "node",
299
+ "args": ["/path/to/naver-search-mcp/dist/src/index.js"],
300
+ "cwd": "/path/to/naver-search-mcp",
301
+ "env": {
302
+ "NAVER_CLIENT_ID": "your-naver-client-id",
303
+ "NAVER_CLIENT_SECRET": "your-naver-client-secret"
304
+ }
305
+ }
306
+ }
307
+ }
93
308
  ```
94
309
 
95
- For Cline:
310
+ ### Step 3: Path Configuration Important Notes
311
+
312
+ ⚠️ **Important**: You must change the following paths in the above configuration to your actual installation paths:
313
+
314
+ - **Windows**: Change `C:\\path\\to\\naver-search-mcp` to your actual downloaded folder path
315
+ - **macOS/Linux**: Change `/path/to/naver-search-mcp` to your actual downloaded folder path
316
+
317
+ Finding your path:
96
318
 
97
319
  ```bash
98
- npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client cline
320
+ # Check current location
321
+ pwd
322
+
323
+ # Absolute path examples
324
+ # Windows: C:\Users\username\Downloads\naver-search-mcp
325
+ # macOS: /Users/username/Downloads/naver-search-mcp
326
+ # Linux: /home/username/Downloads/naver-search-mcp
99
327
  ```
100
328
 
101
- The installer will prompt you for:
329
+ ### Step 4: Restart Claude Desktop
330
+
331
+ After completing the configuration, completely close and restart Claude Desktop to activate the Naver Search MCP server.
332
+
333
+ ---
334
+
335
+ ## Alternative Installation Methods
102
336
 
103
- - NAVER_CLIENT_ID
104
- - NAVER_CLIENT_SECRET
337
+ ### Option 2: NPX Installation (For v1.0.30 and below)
105
338
 
106
- ### Option 2: Manual Installation
339
+ For older versions that support NPX installation:
107
340
 
108
341
  #### Environment Variables
109
342
 
@@ -117,22 +350,7 @@ export NAVER_CLIENT_ID=your_client_id
117
350
  export NAVER_CLIENT_SECRET=your_client_secret
118
351
  ```
119
352
 
120
- #### Run with NPX
121
-
122
- ```bash
123
- npx @isnow890/naver-search-mcp
124
- ```
125
-
126
- #### Run with Docker
127
-
128
- ```bash
129
- docker run -i --rm \
130
- -e NAVER_CLIENT_ID=your_client_id \
131
- -e NAVER_CLIENT_SECRET=your_client_secret \
132
- mcp/naver-search
133
- ```
134
-
135
- ## Claude Desktop Configuration
353
+ #### Claude Desktop Configuration for NPX
136
354
 
137
355
  Add to `claude_desktop_config.json`:
138
356
 
@@ -151,7 +369,7 @@ Add to `claude_desktop_config.json`:
151
369
  }
152
370
  ```
153
371
 
154
- ## Cursor AI Configuration
372
+ #### Cursor AI Configuration for NPX
155
373
 
156
374
  Add to `mcp.json`:
157
375
 
@@ -170,7 +388,39 @@ Add to `mcp.json`:
170
388
  }
171
389
  ```
172
390
 
173
- For Docker:
391
+ ### Option 3: Legacy Smithery Installation (Only for v1.0.30 and below)
392
+
393
+ ⚠️ **Note**: This method only works for versions 1.0.30 and below due to platform compatibility issues.
394
+
395
+ #### For Claude Desktop:
396
+ ```bash
397
+ npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client claude
398
+ ```
399
+
400
+ #### For other AI clients:
401
+ ```bash
402
+ # Cursor
403
+ npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client cursor
404
+
405
+ # Windsurf
406
+ npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client windsurf
407
+
408
+ # Cline
409
+ npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client cline
410
+ ```
411
+
412
+ ### Option 4: Docker Installation
413
+
414
+ For containerized deployment:
415
+
416
+ ```bash
417
+ docker run -i --rm \
418
+ -e NAVER_CLIENT_ID=your_client_id \
419
+ -e NAVER_CLIENT_SECRET=your_client_secret \
420
+ mcp/naver-search
421
+ ```
422
+
423
+ Docker configuration for Claude Desktop:
174
424
 
175
425
  ```json
176
426
  {
@@ -0,0 +1,26 @@
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. 파일 수정시 자동으로 캐시 갱신됨
Binary file
@@ -0,0 +1,240 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { createRequire } from 'module';
5
+ // Bundle-safe directory resolution
6
+ let __dirname;
7
+ try {
8
+ if (typeof import.meta.url !== 'undefined') {
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ __dirname = path.dirname(__filename);
11
+ }
12
+ else {
13
+ // Fallback for bundled environments
14
+ __dirname = path.join(process.cwd(), 'dist', 'src', 'handlers');
15
+ }
16
+ }
17
+ catch {
18
+ // Final fallback
19
+ __dirname = path.join(process.cwd(), 'dist', 'src', 'handlers');
20
+ }
21
+ // Cache management
22
+ let categoryCache = null;
23
+ let cacheTimestamp = 0;
24
+ /**
25
+ * Load and cache category data from Excel file
26
+ */
27
+ async function loadCategoryData() {
28
+ // Use __dirname to get the directory of the current module, then go to data folder
29
+ const dataPath = path.resolve(__dirname, '..', '..', 'data');
30
+ // Find any Excel file in data directory
31
+ const files = fs.readdirSync(dataPath);
32
+ const excelFiles = files.filter(file => file.endsWith('.xlsx') || file.endsWith('.xls'));
33
+ if (excelFiles.length === 0) {
34
+ throw new Error(`카테고리 Excel 파일을 찾을 수 없습니다. data 폴더에 .xlsx 또는 .xls 파일을 넣어주세요: ${dataPath}`);
35
+ }
36
+ // Use the first Excel file found
37
+ const filePath = path.join(dataPath, excelFiles[0]);
38
+ console.error(`Using Excel file: ${excelFiles[0]}`);
39
+ // Check if file exists (redundant but safe)
40
+ if (!fs.existsSync(filePath)) {
41
+ throw new Error(`카테고리 파일을 찾을 수 없습니다: ${filePath}`);
42
+ }
43
+ const fileStats = fs.statSync(filePath);
44
+ // Return cached data if file hasn't changed
45
+ if (categoryCache && fileStats.mtime.getTime() <= cacheTimestamp) {
46
+ return categoryCache;
47
+ }
48
+ console.error('Loading category data from Excel file...');
49
+ // Read Excel file - improved ES modules compatibility
50
+ let XLSX;
51
+ try {
52
+ // Try dynamic import first (modern ES modules)
53
+ try {
54
+ XLSX = await import('xlsx');
55
+ // Handle default export structure
56
+ if (XLSX.default) {
57
+ XLSX = XLSX.default;
58
+ }
59
+ }
60
+ catch (importError) {
61
+ // Fallback to createRequire for bundled environments
62
+ const require = createRequire(import.meta.url);
63
+ XLSX = require('xlsx');
64
+ }
65
+ }
66
+ catch (error) {
67
+ throw new Error(`xlsx 패키지 로드 실패. 패키지가 설치되지 않았거나 호환성 문제입니다. 세부사항: ${error instanceof Error ? error.message : String(error)}`);
68
+ }
69
+ const workbook = XLSX.readFile(filePath);
70
+ const sheetName = workbook.SheetNames[0];
71
+ const worksheet = workbook.Sheets[sheetName];
72
+ // Convert to JSON (skip header row)
73
+ const rawData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
74
+ const dataRows = rawData.slice(1); // Skip header row
75
+ // Parse data
76
+ categoryCache = dataRows
77
+ .filter(row => row.length >= 2 && row[0]) // Filter out empty rows
78
+ .map(row => ({
79
+ code: String(row[0] || '').trim(),
80
+ level1: String(row[1] || '').trim(),
81
+ level2: String(row[2] || '').trim(),
82
+ level3: String(row[3] || '').trim(),
83
+ level4: String(row[4] || '').trim(),
84
+ }))
85
+ .filter(item => item.code); // Only include rows with valid codes
86
+ cacheTimestamp = fileStats.mtime.getTime();
87
+ console.error(`Loaded ${categoryCache.length} categories from Excel file`);
88
+ return categoryCache;
89
+ }
90
+ /**
91
+ * Smart category search with fuzzy matching and ranking
92
+ */
93
+ async function smartCategorySearch(query, maxResults = 10) {
94
+ const categories = await loadCategoryData();
95
+ const searchQuery = query.toLowerCase().trim();
96
+ const results = [];
97
+ for (const category of categories) {
98
+ let bestScore = 0;
99
+ let bestMatchType = '';
100
+ // Check all levels for matches
101
+ const levels = [
102
+ { value: category.level1, name: '대분류' },
103
+ { value: category.level2, name: '중분류' },
104
+ { value: category.level3, name: '소분류' },
105
+ { value: category.level4, name: '세분류' }
106
+ ];
107
+ for (const level of levels) {
108
+ if (!level.value)
109
+ continue;
110
+ const levelText = level.value.toLowerCase();
111
+ let score = 0;
112
+ let matchType = '';
113
+ // Level-based bonus (대분류 우선)
114
+ let levelBonus = 0;
115
+ if (level.name === '대분류')
116
+ levelBonus = 50;
117
+ else if (level.name === '중분류')
118
+ levelBonus = 30;
119
+ else if (level.name === '소분류')
120
+ levelBonus = 20;
121
+ else if (level.name === '세분류')
122
+ levelBonus = 10;
123
+ // Exact match (highest priority)
124
+ if (levelText === searchQuery) {
125
+ score = 100 + levelBonus;
126
+ matchType = `정확일치(${level.name})`;
127
+ }
128
+ // Starts with (high priority)
129
+ else if (levelText.startsWith(searchQuery)) {
130
+ score = 80 + levelBonus;
131
+ matchType = `시작일치(${level.name})`;
132
+ }
133
+ // Contains (medium priority)
134
+ else if (levelText.includes(searchQuery)) {
135
+ score = 60 + levelBonus;
136
+ matchType = `포함일치(${level.name})`;
137
+ }
138
+ // Fuzzy match for similar terms
139
+ else {
140
+ const similarity = calculateSimilarity(searchQuery, levelText);
141
+ if (similarity > 0.6) {
142
+ score = Math.floor(similarity * 40) + levelBonus; // 0.6-1.0 -> 24-40 points + level bonus
143
+ matchType = `유사일치(${level.name})`;
144
+ }
145
+ }
146
+ if (score > bestScore) {
147
+ bestScore = score;
148
+ bestMatchType = matchType;
149
+ }
150
+ }
151
+ if (bestScore > 0) {
152
+ results.push({
153
+ ...category,
154
+ score: bestScore,
155
+ matchType: bestMatchType
156
+ });
157
+ }
158
+ }
159
+ // Sort by score (descending), then by category code (ascending for 대분류 priority)
160
+ return results
161
+ .sort((a, b) => {
162
+ if (b.score !== a.score) {
163
+ return b.score - a.score; // Higher score first
164
+ }
165
+ // If same score, prioritize by category code (대분류 codes are typically smaller)
166
+ return a.code.localeCompare(b.code);
167
+ })
168
+ .slice(0, maxResults);
169
+ }
170
+ /**
171
+ * Calculate similarity between two strings (simple implementation)
172
+ */
173
+ function calculateSimilarity(str1, str2) {
174
+ const longer = str1.length > str2.length ? str1 : str2;
175
+ const shorter = str1.length > str2.length ? str2 : str1;
176
+ if (longer.length === 0)
177
+ return 1.0;
178
+ const editDistance = levenshteinDistance(longer, shorter);
179
+ return (longer.length - editDistance) / longer.length;
180
+ }
181
+ /**
182
+ * Calculate Levenshtein distance between two strings
183
+ */
184
+ function levenshteinDistance(str1, str2) {
185
+ const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null));
186
+ for (let i = 0; i <= str1.length; i++)
187
+ matrix[0][i] = i;
188
+ for (let j = 0; j <= str2.length; j++)
189
+ matrix[j][0] = j;
190
+ for (let j = 1; j <= str2.length; j++) {
191
+ for (let i = 1; i <= str1.length; i++) {
192
+ const substitutionCost = str1[i - 1] === str2[j - 1] ? 0 : 1;
193
+ matrix[j][i] = Math.min(matrix[j][i - 1] + 1, matrix[j - 1][i] + 1, matrix[j - 1][i - 1] + substitutionCost);
194
+ }
195
+ }
196
+ return matrix[str2.length][str1.length];
197
+ }
198
+ /**
199
+ * 카테고리 검색 핸들러
200
+ */
201
+ export const findCategoryHandler = async ({ query, max_results = 10 }) => {
202
+ try {
203
+ const results = await smartCategorySearch(query, max_results);
204
+ if (results.length === 0) {
205
+ return {
206
+ message: `"${query}"와 관련된 카테고리를 찾을 수 없습니다. 다른 검색어를 시도해보세요.`,
207
+ suggestions: ["패션", "화장품", "가구", "스마트폰", "가전제품", "스포츠", "도서", "자동차", "식품", "뷰티"]
208
+ };
209
+ }
210
+ const responseData = {
211
+ message: `"${query}" 검색 결과 (${results.length}개, 관련도순 정렬)`,
212
+ total_found: results.length,
213
+ categories: results.map(cat => ({
214
+ code: cat.code,
215
+ category: [cat.level1, cat.level2, cat.level3, cat.level4].filter(Boolean).join(' > '),
216
+ match_type: cat.matchType,
217
+ score: cat.score,
218
+ levels: {
219
+ 대분류: cat.level1 || '',
220
+ 중분류: cat.level2 || '',
221
+ 소분류: cat.level3 || '',
222
+ 세분류: cat.level4 || ''
223
+ }
224
+ })),
225
+ next_steps: {
226
+ trend_analysis: `이제 datalab_shopping_category 도구로 각 카테고리의 트렌드 분석이 가능합니다`,
227
+ age_analysis: `datalab_shopping_age 도구로 연령별 쇼핑 패턴을 분석할 수 있습니다`,
228
+ gender_analysis: `datalab_shopping_gender 도구로 성별 쇼핑 패턴을 분석할 수 있습니다`,
229
+ device_analysis: `datalab_shopping_device 도구로 디바이스별 쇼핑 패턴을 분석할 수 있습니다`
230
+ }
231
+ };
232
+ return responseData;
233
+ }
234
+ catch (error) {
235
+ throw new Error(`카테고리 검색 중 오류 발생: ${error.message}`);
236
+ }
237
+ };
238
+ export const categoryToolHandlers = {
239
+ find_category: findCategoryHandler,
240
+ };
@@ -7,15 +7,42 @@ const client = NaverSearchClient.getInstance();
7
7
  * index.ts에서 도구 실행 분기 없이 바로 사용
8
8
  */
9
9
  export const datalabToolHandlers = {
10
- datalab_search: (args) => handleSearchTrend(args), // 검색어 트렌드 분석
11
- datalab_shopping_category: (args) => handleShoppingCategoryTrend(args), // 쇼핑 카테고리별 트렌드
12
- datalab_shopping_by_device: (args) => handleShoppingByDeviceTrend(args), // 쇼핑 기기별 트렌드
13
- datalab_shopping_by_gender: (args) => handleShoppingByGenderTrend(args), // 쇼핑 성별 트렌드
14
- datalab_shopping_by_age: (args) => handleShoppingByAgeTrend(args), // 쇼핑 연령별 트렌드
15
- datalab_shopping_keywords: (args) => handleShoppingKeywordsTrend(args), // 쇼핑 키워드 그룹 트렌드
16
- datalab_shopping_keyword_by_device: (args) => handleShoppingKeywordByDeviceTrend(args), // 쇼핑 키워드 기기별 트렌드
17
- datalab_shopping_keyword_by_gender: (args) => handleShoppingKeywordByGenderTrend(args), // 쇼핑 키워드 성별 트렌드
18
- datalab_shopping_keyword_by_age: (args) => handleShoppingKeywordByAgeTrend(args), // 쇼핑 키워드 연령별 트렌드
10
+ datalab_search: (args) => {
11
+ console.error("datalab_search called with args:", JSON.stringify(args, null, 2));
12
+ return handleSearchTrend(args);
13
+ },
14
+ datalab_shopping_category: (args) => {
15
+ console.error("datalab_shopping_category called with args:", JSON.stringify(args, null, 2));
16
+ return handleShoppingCategoryTrend(args);
17
+ },
18
+ datalab_shopping_by_device: (args) => {
19
+ console.error("datalab_shopping_by_device called with args:", JSON.stringify(args, null, 2));
20
+ return handleShoppingByDeviceTrend(args);
21
+ },
22
+ datalab_shopping_by_gender: (args) => {
23
+ console.error("datalab_shopping_by_gender called with args:", JSON.stringify(args, null, 2));
24
+ return handleShoppingByGenderTrend(args);
25
+ },
26
+ datalab_shopping_by_age: (args) => {
27
+ console.error("datalab_shopping_by_age called with args:", JSON.stringify(args, null, 2));
28
+ return handleShoppingByAgeTrend(args);
29
+ },
30
+ datalab_shopping_keywords: (args) => {
31
+ console.error("datalab_shopping_keywords called with args:", JSON.stringify(args, null, 2));
32
+ return handleShoppingKeywordsTrend(args);
33
+ },
34
+ datalab_shopping_keyword_by_device: (args) => {
35
+ console.error("datalab_shopping_keyword_by_device called with args:", JSON.stringify(args, null, 2));
36
+ return handleShoppingKeywordByDeviceTrend(args);
37
+ },
38
+ datalab_shopping_keyword_by_gender: (args) => {
39
+ console.error("datalab_shopping_keyword_by_gender called with args:", JSON.stringify(args, null, 2));
40
+ return handleShoppingKeywordByGenderTrend(args);
41
+ },
42
+ datalab_shopping_keyword_by_age: (args) => {
43
+ console.error("datalab_shopping_keyword_by_age called with args:", JSON.stringify(args, null, 2));
44
+ return handleShoppingKeywordByAgeTrend(args);
45
+ },
19
46
  };
20
47
  /**
21
48
  * 검색어 트렌드 핸들러
@@ -3,17 +3,50 @@ import { SearchArgsSchema } from "../schemas/search.schemas.js";
3
3
  // 클라이언트 인스턴스
4
4
  const client = NaverSearchClient.getInstance();
5
5
  export const searchToolHandlers = {
6
- search_webkr: (args) => handleWebKrSearch(SearchArgsSchema.parse(args)),
7
- search_news: (args) => handleNewsSearch(SearchArgsSchema.parse(args)),
8
- search_blog: (args) => handleBlogSearch(SearchArgsSchema.parse(args)),
9
- search_shop: (args) => handleShopSearch(SearchArgsSchema.parse(args)),
10
- search_image: (args) => handleImageSearch(SearchArgsSchema.parse(args)),
11
- search_kin: (args) => handleKinSearch(SearchArgsSchema.parse(args)),
12
- search_book: (args) => handleBookSearch(SearchArgsSchema.parse(args)),
13
- search_encyc: (args) => handleEncycSearch(SearchArgsSchema.parse(args)),
14
- search_academic: (args) => handleAcademicSearch(SearchArgsSchema.parse(args)),
15
- search_local: (args) => handleLocalSearch(args),
16
- search_cafearticle: (args) => handleCafeArticleSearch(args),
6
+ search_webkr: (args) => {
7
+ console.error("search_webkr called with args:", JSON.stringify(args, null, 2));
8
+ return handleWebKrSearch(SearchArgsSchema.parse(args));
9
+ },
10
+ search_news: (args) => {
11
+ console.error("search_news called with args:", JSON.stringify(args, null, 2));
12
+ return handleNewsSearch(SearchArgsSchema.parse(args));
13
+ },
14
+ search_blog: (args) => {
15
+ console.error("search_blog called with args:", JSON.stringify(args, null, 2));
16
+ return handleBlogSearch(SearchArgsSchema.parse(args));
17
+ },
18
+ search_shop: (args) => {
19
+ console.error("search_shop called with args:", JSON.stringify(args, null, 2));
20
+ return handleShopSearch(SearchArgsSchema.parse(args));
21
+ },
22
+ search_image: (args) => {
23
+ console.error("search_image called with args:", JSON.stringify(args, null, 2));
24
+ return handleImageSearch(SearchArgsSchema.parse(args));
25
+ },
26
+ search_kin: (args) => {
27
+ console.error("search_kin called with args:", JSON.stringify(args, null, 2));
28
+ return handleKinSearch(SearchArgsSchema.parse(args));
29
+ },
30
+ search_book: (args) => {
31
+ console.error("search_book called with args:", JSON.stringify(args, null, 2));
32
+ return handleBookSearch(SearchArgsSchema.parse(args));
33
+ },
34
+ search_encyc: (args) => {
35
+ console.error("search_encyc called with args:", JSON.stringify(args, null, 2));
36
+ return handleEncycSearch(SearchArgsSchema.parse(args));
37
+ },
38
+ search_academic: (args) => {
39
+ console.error("search_academic called with args:", JSON.stringify(args, null, 2));
40
+ return handleAcademicSearch(SearchArgsSchema.parse(args));
41
+ },
42
+ search_local: (args) => {
43
+ console.error("search_local called with args:", JSON.stringify(args, null, 2));
44
+ return handleLocalSearch(args);
45
+ },
46
+ search_cafearticle: (args) => {
47
+ console.error("search_cafearticle called with args:", JSON.stringify(args, null, 2));
48
+ return handleCafeArticleSearch(SearchArgsSchema.parse(args));
49
+ },
17
50
  };
18
51
  /**
19
52
  * 전문자료 검색 핸들러
package/dist/src/index.js CHANGED
@@ -1,20 +1,29 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
3
  import { z } from "zod";
3
4
  import { NaverSearchClient } from "./clients/naver-search.client.js";
4
- import { searchTools } from "./tools/search.tools.js";
5
- import { datalabTools } from "./tools/datalab.tools.js";
6
5
  import { searchToolHandlers } from "./handlers/search.handlers.js";
7
6
  import { datalabToolHandlers } from "./handlers/datalab.handlers.js";
7
+ import { SearchArgsSchema, NaverLocalSearchParamsSchema, } from "./schemas/search.schemas.js";
8
+ import { DatalabSearchSchema, DatalabShoppingSchema, DatalabShoppingDeviceSchema, DatalabShoppingGenderSchema, DatalabShoppingAgeSchema, DatalabShoppingKeywordsSchema, DatalabShoppingKeywordDeviceSchema, DatalabShoppingKeywordGenderSchema, DatalabShoppingKeywordAgeSchema, } from "./schemas/datalab.schemas.js";
9
+ import { FindCategorySchema } from "./schemas/category.schemas.js";
10
+ import { findCategoryHandler } from "./handlers/category.handlers.js";
8
11
  // Configuration schema for Smithery
9
12
  export const configSchema = z.object({
10
13
  NAVER_CLIENT_ID: z.string().describe("Naver API Client ID"),
11
- NAVER_CLIENT_SECRET: z.string().describe("Naver API Client Secret")
14
+ NAVER_CLIENT_SECRET: z.string().describe("Naver API Client Secret"),
12
15
  });
13
- export default function ({ config }) {
14
- // Create a new MCP server per MCP spec
16
+ // Global server instance to prevent memory leaks
17
+ let globalServerInstance = null;
18
+ export function createNaverSearchServer({ config, }) {
19
+ // Reuse existing server instance to prevent memory leaks
20
+ if (globalServerInstance) {
21
+ return globalServerInstance;
22
+ }
23
+ // Create a new MCP server only once
15
24
  const server = new McpServer({
16
25
  name: "naver-search",
17
- version: "1.0.18",
26
+ version: "1.0.40",
18
27
  });
19
28
  // Initialize Naver client with config
20
29
  const client = NaverSearchClient.getInstance();
@@ -22,51 +31,292 @@ export default function ({ config }) {
22
31
  clientId: config.NAVER_CLIENT_ID,
23
32
  clientSecret: config.NAVER_CLIENT_SECRET,
24
33
  });
25
- const toolHandlers = {
26
- ...searchToolHandlers,
27
- ...datalabToolHandlers,
28
- };
29
- // Add all search tools
30
- searchTools.forEach(tool => {
31
- server.tool(tool.name, tool.description, tool.inputSchema, async (args) => {
32
- try {
33
- const handler = toolHandlers[tool.name];
34
- if (!handler)
35
- throw new Error(`Unknown tool: ${tool.name}`);
36
- const result = await handler(args);
37
- return {
38
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
39
- };
40
- }
41
- catch (error) {
42
- const errorMessage = error instanceof Error ? error.message : String(error);
43
- return {
44
- content: [{ type: "text", text: `Error: ${errorMessage}` }],
45
- isError: true,
46
- };
47
- }
48
- });
34
+ server.registerTool("search_news", {
35
+ description: "📰 Search latest Korean news articles from major outlets. Perfect for current events, breaking news, and recent developments. Covers politics, economy, society, and international news. (네이버 뉴스 검색 - 최신 뉴스와 시사 정보)",
36
+ inputSchema: SearchArgsSchema.shape,
37
+ }, async (args) => {
38
+ const result = await searchToolHandlers.search_news(args);
39
+ return {
40
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
41
+ };
49
42
  });
50
- // Add all datalab tools
51
- datalabTools.forEach(tool => {
52
- server.tool(tool.name, tool.description, tool.inputSchema, async (args) => {
53
- try {
54
- const handler = toolHandlers[tool.name];
55
- if (!handler)
56
- throw new Error(`Unknown tool: ${tool.name}`);
57
- const result = await handler(args);
58
- return {
59
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
60
- };
61
- }
62
- catch (error) {
63
- const errorMessage = error instanceof Error ? error.message : String(error);
64
- return {
65
- content: [{ type: "text", text: `Error: ${errorMessage}` }],
66
- isError: true,
67
- };
68
- }
69
- });
43
+ server.registerTool("search_blog", {
44
+ description: "✍️ 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. (네이버 블로그 검색 - 실제 사용자 후기와 개인적 경험)",
45
+ inputSchema: SearchArgsSchema.shape,
46
+ }, async (args) => {
47
+ const result = await searchToolHandlers.search_blog(args);
48
+ return {
49
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
50
+ };
51
+ });
52
+ server.registerTool("search_shop", {
53
+ description: "🛒 Search Naver Shopping for products, prices, and shopping deals. Compare prices across vendors, find product specifications, and discover shopping trends in Korea. (네이버 쇼핑 검색 - 상품 정보와 가격 비교)",
54
+ inputSchema: SearchArgsSchema.shape,
55
+ }, async (args) => {
56
+ const result = await searchToolHandlers.search_shop(args);
57
+ return {
58
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
59
+ };
60
+ });
61
+ server.registerTool("search_image", {
62
+ description: "🖼️ 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. (네이버 이미지 검색 - 시각적 컨텐츠 발견)",
63
+ inputSchema: SearchArgsSchema.shape,
64
+ }, async (args) => {
65
+ const result = await searchToolHandlers.search_image(args);
66
+ return {
67
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
68
+ };
69
+ });
70
+ server.registerTool("search_kin", {
71
+ description: "❓ Search Naver KnowledgeiN for Q&A and community-driven answers. Find solutions to problems, get expert advice, and discover community insights on various topics. (네이버 지식iN 검색 - 질문과 답변, 커뮤니티 지식)",
72
+ inputSchema: SearchArgsSchema.shape,
73
+ }, async (args) => {
74
+ const result = await searchToolHandlers.search_kin(args);
75
+ return {
76
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
77
+ };
78
+ });
79
+ server.registerTool("search_book", {
80
+ description: "📚 Search for books, publications, and literary content. Find book reviews, author information, publication details, and reading recommendations in Korean literature and translated works. (네이버 책 검색 - 도서 정보와 서평)",
81
+ inputSchema: SearchArgsSchema.shape,
82
+ }, async (args) => {
83
+ const result = await searchToolHandlers.search_book(args);
84
+ return {
85
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
86
+ };
87
+ });
88
+ server.registerTool("search_encyc", {
89
+ description: "📖 Search Naver Encyclopedia for authoritative knowledge and definitions. Best for academic research, getting reliable information, and understanding Korean concepts and terminology. (네이버 지식백과 검색 - 신뢰할 수 있는 정보와 정의)",
90
+ inputSchema: SearchArgsSchema.shape,
91
+ }, async (args) => {
92
+ const result = await searchToolHandlers.search_encyc(args);
93
+ return {
94
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
95
+ };
96
+ });
97
+ server.registerTool("search_academic", {
98
+ description: "🎓 Search academic papers, research documents, and scholarly content. Access Korean academic resources, research papers, theses, and professional publications. (네이버 전문자료 검색 - 학술 논문과 전문 자료)",
99
+ inputSchema: SearchArgsSchema.shape,
100
+ }, async (args) => {
101
+ const result = await searchToolHandlers.search_academic(args);
102
+ return {
103
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
104
+ };
105
+ });
106
+ server.registerTool("search_local", {
107
+ description: "📍 Search for local businesses, restaurants, and places in Korea. Find location information, reviews, contact details, and business hours for Korean establishments. (네이버 지역 검색 - 지역 업체와 장소 정보)",
108
+ inputSchema: NaverLocalSearchParamsSchema.shape,
109
+ }, async (args) => {
110
+ const result = await searchToolHandlers.search_local(args);
111
+ return {
112
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
113
+ };
114
+ });
115
+ server.registerTool("search_cafearticle", {
116
+ description: "☕ Search Naver Cafe articles for community discussions and specialized content. Find niche communities, hobby groups, and specialized discussions on various topics. (네이버 카페글 검색 - 커뮤니티 토론과 전문 정보)",
117
+ inputSchema: SearchArgsSchema.shape,
118
+ }, async (args) => {
119
+ const result = await searchToolHandlers.search_cafearticle(args);
120
+ return {
121
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
122
+ };
123
+ });
124
+ // Register datalab tools
125
+ server.registerTool("datalab_search", {
126
+ description: "📊 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. (네이버 데이터랩 검색어 트렌드 분석)",
127
+ inputSchema: DatalabSearchSchema.shape,
128
+ }, async (args) => {
129
+ const result = await datalabToolHandlers.datalab_search(args);
130
+ return {
131
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
132
+ };
133
+ });
134
+ server.registerTool("datalab_shopping_category", {
135
+ description: "🛍️ 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. (네이버 쇼핑 카테고리별 트렌드 분석 - 먼저 find_category 도구로 카테고리 코드를 찾으세요)",
136
+ inputSchema: DatalabShoppingSchema.shape,
137
+ }, async (args) => {
138
+ const result = await datalabToolHandlers.datalab_shopping_category(args);
139
+ return {
140
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
141
+ };
142
+ });
143
+ server.registerTool("datalab_shopping_by_device", {
144
+ description: "📱 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 사용자가 더 많이 구매하는 카테고리는?' (기기별 쇼핑 트렌드 분석 - 먼저 find_category 도구로 카테고리 코드를 찾으세요)",
145
+ inputSchema: DatalabShoppingDeviceSchema.pick({
146
+ startDate: true,
147
+ endDate: true,
148
+ timeUnit: true,
149
+ category: true,
150
+ device: true,
151
+ }).shape,
152
+ }, async (args) => {
153
+ const result = await datalabToolHandlers.datalab_shopping_by_device(args);
154
+ return {
155
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
156
+ };
157
+ });
158
+ server.registerTool("datalab_shopping_by_gender", {
159
+ description: "👥 Analyze shopping trends by gender (Male vs Female). Use find_category first. BUSINESS CASES: Gender-targeted marketing, product positioning, demographic analysis. EXAMPLE: '화장품 쇼핑에서 남녀 비율은?' (성별 쇼핑 트렌드 분석 - 먼저 find_category 도구로 카테고리 코드를 찾으세요)",
160
+ inputSchema: DatalabShoppingGenderSchema.pick({
161
+ startDate: true,
162
+ endDate: true,
163
+ timeUnit: true,
164
+ category: true,
165
+ gender: true,
166
+ }).shape,
167
+ }, async (args) => {
168
+ const result = await datalabToolHandlers.datalab_shopping_by_gender(args);
169
+ return {
170
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
171
+ };
70
172
  });
173
+ server.registerTool("datalab_shopping_by_age", {
174
+ description: "👶👦👨👴 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: '개발 도구는 어느 연령대가 많이 구매하나?' (연령별 쇼핑 트렌드 - 먼저 find_category 도구로 카테고리 코드를 찾으세요)",
175
+ inputSchema: DatalabShoppingAgeSchema.pick({
176
+ startDate: true,
177
+ endDate: true,
178
+ timeUnit: true,
179
+ category: true,
180
+ ages: true,
181
+ }).shape,
182
+ }, async (args) => {
183
+ const result = await datalabToolHandlers.datalab_shopping_by_age(args);
184
+ return {
185
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
186
+ };
187
+ });
188
+ server.registerTool("datalab_shopping_keywords", {
189
+ description: "🔍 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. (카테고리 내 키워드 비교 - 먼저 find_category 도구로 카테고리 코드를 찾으세요)",
190
+ inputSchema: DatalabShoppingKeywordsSchema.shape,
191
+ }, async (args) => {
192
+ const result = await datalabToolHandlers.datalab_shopping_keywords(args);
193
+ return {
194
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
195
+ };
196
+ });
197
+ server.registerTool("datalab_shopping_keyword_by_device", {
198
+ description: "📱🔍 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 도구로 카테고리 코드를 찾으세요)",
199
+ inputSchema: DatalabShoppingKeywordDeviceSchema.shape,
200
+ }, async (args) => {
201
+ const result = await datalabToolHandlers.datalab_shopping_keyword_by_device(args);
202
+ return {
203
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
204
+ };
205
+ });
206
+ server.registerTool("datalab_shopping_keyword_by_gender", {
207
+ description: "👥🔍 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 도구로 카테고리 코드를 찾으세요)",
208
+ inputSchema: DatalabShoppingKeywordGenderSchema.shape,
209
+ }, async (args) => {
210
+ const result = await datalabToolHandlers.datalab_shopping_keyword_by_gender(args);
211
+ return {
212
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
213
+ };
214
+ });
215
+ server.registerTool("datalab_shopping_keyword_by_age", {
216
+ description: "👶👦👨👴🔍 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 도구로 카테고리 코드를 찾으세요)",
217
+ inputSchema: DatalabShoppingKeywordAgeSchema.shape,
218
+ }, async (args) => {
219
+ const result = await datalabToolHandlers.datalab_shopping_keyword_by_age(args);
220
+ return {
221
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
222
+ };
223
+ });
224
+ // Register category search tool
225
+ server.registerTool("find_category", {
226
+ description: "🚀 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. (카테고리 검색: 한국어로 검색하여 데이터랩 분석에 필요한 카테고리 코드를 찾아주는 필수 도구)",
227
+ inputSchema: FindCategorySchema.shape,
228
+ }, async (args) => {
229
+ const result = await findCategoryHandler(args);
230
+ return {
231
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
232
+ };
233
+ });
234
+ // Cache the server instance
235
+ globalServerInstance = server;
71
236
  return server.server;
72
237
  }
238
+ // Export default for Smithery compatibility
239
+ export default createNaverSearchServer;
240
+ // Main function to run the server when executed directly
241
+ async function main() {
242
+ try {
243
+ console.error("Starting Naver Search MCP Server...");
244
+ // Get config from environment variables - check for empty strings too
245
+ const clientId = process.env.NAVER_CLIENT_ID?.trim();
246
+ const clientSecret = process.env.NAVER_CLIENT_SECRET?.trim();
247
+ console.error("Environment variables:", {
248
+ NAVER_CLIENT_ID: process.env.NAVER_CLIENT_ID
249
+ ? `[${process.env.NAVER_CLIENT_ID.length} chars]`
250
+ : "undefined",
251
+ NAVER_CLIENT_SECRET: process.env.NAVER_CLIENT_SECRET
252
+ ? `[${process.env.NAVER_CLIENT_SECRET.length} chars]`
253
+ : "undefined",
254
+ });
255
+ if (!clientId || !clientSecret) {
256
+ throw new Error(`Missing required environment variables:
257
+ NAVER_CLIENT_ID: ${clientId ? "provided" : "missing"}
258
+ NAVER_CLIENT_SECRET: ${clientSecret ? "provided" : "missing"}
259
+
260
+ Please set these environment variables before running the server.`);
261
+ }
262
+ const config = {
263
+ NAVER_CLIENT_ID: clientId,
264
+ NAVER_CLIENT_SECRET: clientSecret,
265
+ };
266
+ console.error("Config loaded successfully");
267
+ // Validate config
268
+ const validatedConfig = configSchema.parse(config);
269
+ console.error("Config validated successfully");
270
+ // Create server instance
271
+ const serverFactory = createNaverSearchServer({ config: validatedConfig });
272
+ console.error("Server factory created");
273
+ // Create transport and run server
274
+ const transport = new StdioServerTransport();
275
+ console.error("Transport created, connecting...");
276
+ await serverFactory.connect(transport);
277
+ console.error("Server connected and running");
278
+ }
279
+ catch (error) {
280
+ console.error("Error in main function:", error);
281
+ throw error;
282
+ }
283
+ }
284
+ // Run main function if this file is executed directly
285
+ // Note: Always run main in CLI mode since this is an MCP server
286
+ console.error("Starting MCP server initialization...");
287
+ console.error("process.argv:", process.argv);
288
+ // Check if running as main module - compatible with both ESM and CommonJS
289
+ let isMainModule = false;
290
+ try {
291
+ // Try ESM approach first
292
+ if (typeof import.meta !== "undefined" && import.meta.url) {
293
+ console.error("import.meta.url:", import.meta.url);
294
+ isMainModule =
295
+ import.meta.url === `file://${process.argv[1]}` ||
296
+ import.meta.url.endsWith(process.argv[1]) ||
297
+ process.argv[1].endsWith("index.js");
298
+ }
299
+ else {
300
+ // Fallback for CommonJS or when import.meta is not available
301
+ isMainModule =
302
+ process.argv[1].endsWith("index.js") ||
303
+ process.argv[1].includes("naver-search-mcp");
304
+ }
305
+ }
306
+ catch (error) {
307
+ // Fallback for environments where import.meta causes issues
308
+ isMainModule =
309
+ process.argv[1].endsWith("index.js") ||
310
+ process.argv[1].includes("naver-search-mcp");
311
+ }
312
+ console.error("isMainModule:", isMainModule);
313
+ if (isMainModule) {
314
+ console.error("Running as main module, starting server...");
315
+ main().catch((error) => {
316
+ console.error("Server failed to start:", error);
317
+ process.exit(1);
318
+ });
319
+ }
320
+ else {
321
+ console.error("Not running as main module, skipping server start");
322
+ }
@@ -0,0 +1,6 @@
1
+ import { z } from "zod";
2
+ // Find category schema - Simple and powerful!
3
+ export const FindCategorySchema = z.object({
4
+ query: z.string().describe("Korean category search query (한국어 카테고리 검색어, 예: '패션', '화장품', '가구', '스마트폰'). Supports exact match and fuzzy search with level-based priority"),
5
+ max_results: z.number().optional().default(10).describe("Maximum number of results to return (반환할 최대 결과 수, 기본값: 10)")
6
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isnow890/naver-search-mcp",
3
- "version": "1.0.20",
3
+ "version": "1.0.40",
4
4
  "description": "Naver Search MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "module": "./src/index.ts",
@@ -9,8 +9,8 @@
9
9
  "dist"
10
10
  ],
11
11
  "scripts": {
12
- "prebuild": "shx rm -rf dist",
13
- "build": "tsc",
12
+ "prebuild": "npx shx rm -rf dist",
13
+ "build": "tsc && shx cp -r data dist/",
14
14
  "prepare": "npm run build",
15
15
  "watch": "tsc --watch",
16
16
  "start": "node dist/src/index.js"
@@ -19,12 +19,13 @@
19
19
  "node": ">=18"
20
20
  },
21
21
  "dependencies": {
22
- "@modelcontextprotocol/sdk": "1.10.2",
22
+ "@modelcontextprotocol/sdk": "1.17.1",
23
23
  "axios": "^1.6.7",
24
- "zod": "^3.22.4",
25
- "zod-to-json-schema": "^3.23.5",
24
+ "zod": "^3.23.0",
25
+ "zod-to-json-schema": "^3.23.0",
26
26
  "sharp": "^0.33.2",
27
- "dotenv": "^16.4.5"
27
+ "dotenv": "^16.4.5",
28
+ "xlsx": "0.18.5"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@types/node": "^22",
@@ -1,70 +0,0 @@
1
- import { zodToJsonSchema } from "zod-to-json-schema";
2
- import { DatalabSearchSchema, DatalabShoppingSchema, DatalabShoppingDeviceSchema, DatalabShoppingGenderSchema, DatalabShoppingAgeSchema, DatalabShoppingKeywordsSchema, DatalabShoppingKeywordDeviceSchema, DatalabShoppingKeywordGenderSchema, DatalabShoppingKeywordAgeSchema } from "../schemas/datalab.schemas.js";
3
- /**
4
- * 데이터랩 관련 도구 정의
5
- */
6
- export const datalabTools = [
7
- {
8
- name: "datalab_search",
9
- description: "Perform a trend analysis on Naver search keywords. (네이버 검색어 트렌드 분석)",
10
- inputSchema: zodToJsonSchema(DatalabSearchSchema),
11
- },
12
- {
13
- name: "datalab_shopping_category",
14
- description: "Perform a trend analysis on Naver Shopping category. (네이버 쇼핑 카테고리별 트렌드 분석)",
15
- inputSchema: zodToJsonSchema(DatalabShoppingSchema),
16
- },
17
- {
18
- name: "datalab_shopping_by_device",
19
- description: "Perform a trend analysis on Naver Shopping by device. (네이버 쇼핑 기기별 트렌드 분석)",
20
- inputSchema: zodToJsonSchema(DatalabShoppingDeviceSchema.pick({
21
- startDate: true,
22
- endDate: true,
23
- timeUnit: true,
24
- category: true,
25
- device: true,
26
- })),
27
- },
28
- {
29
- name: "datalab_shopping_by_gender",
30
- description: "Perform a trend analysis on Naver Shopping by gender. (네이버 쇼핑 성별 트렌드 분석)",
31
- inputSchema: zodToJsonSchema(DatalabShoppingGenderSchema.pick({
32
- startDate: true,
33
- endDate: true,
34
- timeUnit: true,
35
- category: true,
36
- gender: true,
37
- })),
38
- },
39
- {
40
- name: "datalab_shopping_by_age",
41
- description: "Perform a trend analysis on Naver Shopping by age. (네이버 쇼핑 연령별 트렌드 분석)",
42
- inputSchema: zodToJsonSchema(DatalabShoppingAgeSchema.pick({
43
- startDate: true,
44
- endDate: true,
45
- timeUnit: true,
46
- category: true,
47
- ages: true,
48
- })),
49
- },
50
- {
51
- name: "datalab_shopping_keywords",
52
- description: "Perform a trend analysis on Naver Shopping keywords. (네이버 쇼핑 키워드별 트렌드 분석)",
53
- inputSchema: zodToJsonSchema(DatalabShoppingKeywordsSchema),
54
- },
55
- {
56
- name: "datalab_shopping_keyword_by_device",
57
- description: "Perform a trend analysis on Naver Shopping keywords by device. (네이버 쇼핑 키워드 기기별 트렌드 분석)",
58
- inputSchema: zodToJsonSchema(DatalabShoppingKeywordDeviceSchema),
59
- },
60
- {
61
- name: "datalab_shopping_keyword_by_gender",
62
- description: "Perform a trend analysis on Naver Shopping keywords by gender. (네이버 쇼핑 키워드 성별 트렌드 분석)",
63
- inputSchema: zodToJsonSchema(DatalabShoppingKeywordGenderSchema),
64
- },
65
- {
66
- name: "datalab_shopping_keyword_by_age",
67
- description: "Perform a trend analysis on Naver Shopping keywords by age. (네이버 쇼핑 키워드 연령별 트렌드 분석)",
68
- inputSchema: zodToJsonSchema(DatalabShoppingKeywordAgeSchema),
69
- },
70
- ];
@@ -1,62 +0,0 @@
1
- import { zodToJsonSchema } from "zod-to-json-schema";
2
- import { SearchArgsSchema, NaverLocalSearchParamsSchema, } from "../schemas/search.schemas.js";
3
- /**
4
- * 검색 관련 도구 정의
5
- */
6
- export const searchTools = [
7
- {
8
- name: "search_webkr",
9
- description: "Perform a search on Naver Web Documents. (네이버 웹문서 검색)",
10
- inputSchema: zodToJsonSchema(SearchArgsSchema),
11
- },
12
- {
13
- name: "search_news",
14
- description: "Perform a search on Naver News. (네이버 뉴스 검색)",
15
- inputSchema: zodToJsonSchema(SearchArgsSchema),
16
- },
17
- {
18
- name: "search_blog",
19
- description: "Perform a search on Naver Blog. (네이버 블로그 검색)",
20
- inputSchema: zodToJsonSchema(SearchArgsSchema),
21
- },
22
- {
23
- name: "search_shop",
24
- description: "Perform a search on Naver Shopping. (네이버 쇼핑 검색)",
25
- inputSchema: zodToJsonSchema(SearchArgsSchema),
26
- },
27
- {
28
- name: "search_image",
29
- description: "Perform a search on Naver Image. (네이버 이미지 검색)",
30
- inputSchema: zodToJsonSchema(SearchArgsSchema),
31
- },
32
- {
33
- name: "search_kin",
34
- description: "Perform a search on Naver KnowledgeiN. (네이버 지식iN 검색)",
35
- inputSchema: zodToJsonSchema(SearchArgsSchema),
36
- },
37
- {
38
- name: "search_book",
39
- description: "Perform a search on Naver Book. (네이버 책 검색)",
40
- inputSchema: zodToJsonSchema(SearchArgsSchema),
41
- },
42
- {
43
- name: "search_encyc",
44
- description: "Perform a search on Naver Encyclopedia. (네이버 지식백과 검색)",
45
- inputSchema: zodToJsonSchema(SearchArgsSchema),
46
- },
47
- {
48
- name: "search_academic",
49
- description: "Perform a search on Naver Academic. (네이버 전문자료 검색)",
50
- inputSchema: zodToJsonSchema(SearchArgsSchema),
51
- },
52
- {
53
- name: "search_local",
54
- description: "Perform a search on Naver Local. (네이버 지역 검색)",
55
- inputSchema: zodToJsonSchema(NaverLocalSearchParamsSchema),
56
- },
57
- {
58
- name: "search_cafearticle",
59
- description: "Perform a search on Naver Cafe Articles. (네이버 카페글 검색)",
60
- inputSchema: zodToJsonSchema(SearchArgsSchema),
61
- },
62
- ];