@isnow890/naver-search-mcp 1.0.30 → 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 +238 -33
- package/dist/data/README.md +26 -0
- package/dist/data/category.xls +0 -0
- package/dist/src/handlers/category.handlers.js +240 -0
- package/dist/src/index.js +155 -82
- package/dist/src/schemas/category.schemas.js +6 -0
- package/package.json +4 -3
- package/dist/src/tools/datalab.tools.js +0 -70
- package/dist/src/tools/search.tools.js +0 -61
package/README.md
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
# Naver Search MCP Server
|
|
2
2
|
|
|
3
3
|
[](README-ko.md)
|
|
4
|
+
|
|
5
|
+
[](https://archestra.ai/mcp-catalog/isnow890__naver-search-mcp)
|
|
4
6
|
[](https://smithery.ai/server/@isnow890/naver-search-mcp)
|
|
5
7
|
[](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
|
+
|
|
11
22
|
###### 1.0.30 (2025-08-04)
|
|
12
23
|
|
|
13
24
|
- MCP SDK upgraded to 1.17.1
|
|
@@ -53,7 +64,12 @@ MCP server for Naver Search API and DataLab API integration, enabling comprehens
|
|
|
53
64
|
|
|
54
65
|
### Available tools:
|
|
55
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
|
+
|
|
56
71
|
#### Search Tools
|
|
72
|
+
|
|
57
73
|
- **search_webkr**: Search Naver web documents
|
|
58
74
|
- **search_news**: Search Naver news
|
|
59
75
|
- **search_blog**: Search Naver blogs
|
|
@@ -67,6 +83,7 @@ MCP server for Naver Search API and DataLab API integration, enabling comprehens
|
|
|
67
83
|
- **search_local**: Search Naver local places
|
|
68
84
|
|
|
69
85
|
#### DataLab Tools
|
|
86
|
+
|
|
70
87
|
- **datalab_search**: Analyze search term trends
|
|
71
88
|
- **datalab_shopping_category**: Analyze shopping category trends
|
|
72
89
|
- **datalab_shopping_by_device**: Analyze shopping trends by device
|
|
@@ -82,6 +99,7 @@ MCP server for Naver Search API and DataLab API integration, enabling comprehens
|
|
|
82
99
|
For DataLab shopping analysis tools, you need to use specific **8-digit category codes**. Here are some common examples:
|
|
83
100
|
|
|
84
101
|
#### Popular Category Codes:
|
|
102
|
+
|
|
85
103
|
- **50000000**: Fashion/Clothing (패션의류)
|
|
86
104
|
- **50000002**: Cosmetics/Beauty (화장품/미용)
|
|
87
105
|
- **50000003**: Digital/Home Appliances (디지털/가전)
|
|
@@ -94,13 +112,16 @@ For DataLab shopping analysis tools, you need to use specific **8-digit category
|
|
|
94
112
|
- **50000010**: Household/Living (생활/주방용품)
|
|
95
113
|
|
|
96
114
|
#### How to Find Category Codes:
|
|
115
|
+
|
|
97
116
|
1. Visit [Naver Shopping](https://shopping.naver.com)
|
|
98
117
|
2. Navigate to desired category
|
|
99
118
|
3. Check the `cat_id` parameter in the URL
|
|
100
119
|
4. Use the 8-digit code (e.g., `cat_id=50000000` → use `"50000000"`)
|
|
101
120
|
|
|
102
121
|
#### Usage Example:
|
|
122
|
+
|
|
103
123
|
When using shopping category tools, specify category like:
|
|
124
|
+
|
|
104
125
|
```json
|
|
105
126
|
{
|
|
106
127
|
"category": "50000000",
|
|
@@ -111,44 +132,211 @@ When using shopping category tools, specify category like:
|
|
|
111
132
|
```
|
|
112
133
|
|
|
113
134
|
#### Complete Category List:
|
|
135
|
+
|
|
114
136
|
For a complete list of category codes, you can download from Naver Shopping Partner Center or extract them by browsing Naver Shopping categories.
|
|
115
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
|
+
|
|
116
229
|
## Installation
|
|
117
230
|
|
|
118
|
-
###
|
|
231
|
+
### Step 1: Download Source Code
|
|
119
232
|
|
|
120
|
-
To
|
|
233
|
+
To use this MCP server, you need to download the source code first:
|
|
121
234
|
|
|
122
|
-
|
|
235
|
+
#### Clone with Git
|
|
123
236
|
|
|
124
237
|
```bash
|
|
125
|
-
|
|
238
|
+
git clone https://github.com/isnow890/naver-search-mcp.git
|
|
239
|
+
cd naver-search-mcp
|
|
240
|
+
npm install
|
|
241
|
+
npm run build
|
|
126
242
|
```
|
|
127
243
|
|
|
128
|
-
|
|
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:
|
|
129
249
|
|
|
130
250
|
```bash
|
|
131
|
-
|
|
251
|
+
cd /path/to/naver-search-mcp
|
|
252
|
+
npm install
|
|
253
|
+
npm run build
|
|
132
254
|
```
|
|
133
255
|
|
|
134
|
-
|
|
256
|
+
### Step 2: Claude Desktop Configuration
|
|
135
257
|
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
}
|
|
138
287
|
```
|
|
139
288
|
|
|
140
|
-
|
|
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
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
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:
|
|
141
318
|
|
|
142
319
|
```bash
|
|
143
|
-
|
|
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
|
|
144
327
|
```
|
|
145
328
|
|
|
146
|
-
|
|
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
|
|
147
336
|
|
|
148
|
-
|
|
149
|
-
- NAVER_CLIENT_SECRET
|
|
337
|
+
### Option 2: NPX Installation (For v1.0.30 and below)
|
|
150
338
|
|
|
151
|
-
|
|
339
|
+
For older versions that support NPX installation:
|
|
152
340
|
|
|
153
341
|
#### Environment Variables
|
|
154
342
|
|
|
@@ -162,22 +350,7 @@ export NAVER_CLIENT_ID=your_client_id
|
|
|
162
350
|
export NAVER_CLIENT_SECRET=your_client_secret
|
|
163
351
|
```
|
|
164
352
|
|
|
165
|
-
####
|
|
166
|
-
|
|
167
|
-
```bash
|
|
168
|
-
npx @isnow890/naver-search-mcp
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
#### Run with Docker
|
|
172
|
-
|
|
173
|
-
```bash
|
|
174
|
-
docker run -i --rm \
|
|
175
|
-
-e NAVER_CLIENT_ID=your_client_id \
|
|
176
|
-
-e NAVER_CLIENT_SECRET=your_client_secret \
|
|
177
|
-
mcp/naver-search
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
## Claude Desktop Configuration
|
|
353
|
+
#### Claude Desktop Configuration for NPX
|
|
181
354
|
|
|
182
355
|
Add to `claude_desktop_config.json`:
|
|
183
356
|
|
|
@@ -196,7 +369,7 @@ Add to `claude_desktop_config.json`:
|
|
|
196
369
|
}
|
|
197
370
|
```
|
|
198
371
|
|
|
199
|
-
|
|
372
|
+
#### Cursor AI Configuration for NPX
|
|
200
373
|
|
|
201
374
|
Add to `mcp.json`:
|
|
202
375
|
|
|
@@ -215,7 +388,39 @@ Add to `mcp.json`:
|
|
|
215
388
|
}
|
|
216
389
|
```
|
|
217
390
|
|
|
218
|
-
|
|
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:
|
|
219
424
|
|
|
220
425
|
```json
|
|
221
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
|
+
};
|
package/dist/src/index.js
CHANGED
|
@@ -4,18 +4,26 @@ import { z } from "zod";
|
|
|
4
4
|
import { NaverSearchClient } from "./clients/naver-search.client.js";
|
|
5
5
|
import { searchToolHandlers } from "./handlers/search.handlers.js";
|
|
6
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";
|
|
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";
|
|
9
11
|
// Configuration schema for Smithery
|
|
10
12
|
export const configSchema = z.object({
|
|
11
13
|
NAVER_CLIENT_ID: z.string().describe("Naver API Client ID"),
|
|
12
|
-
NAVER_CLIENT_SECRET: z.string().describe("Naver API Client Secret")
|
|
14
|
+
NAVER_CLIENT_SECRET: z.string().describe("Naver API Client Secret"),
|
|
13
15
|
});
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
16
24
|
const server = new McpServer({
|
|
17
25
|
name: "naver-search",
|
|
18
|
-
version: "1.0.
|
|
26
|
+
version: "1.0.40",
|
|
19
27
|
});
|
|
20
28
|
// Initialize Naver client with config
|
|
21
29
|
const client = NaverSearchClient.getInstance();
|
|
@@ -23,166 +31,208 @@ function createNaverSearchServer({ config }) {
|
|
|
23
31
|
clientId: config.NAVER_CLIENT_ID,
|
|
24
32
|
clientSecret: config.NAVER_CLIENT_SECRET,
|
|
25
33
|
});
|
|
26
|
-
// Register search tools using new 1.17.1 API
|
|
27
|
-
server.registerTool("search_webkr", {
|
|
28
|
-
description: "Perform a search on Naver Web Documents. (네이버 웹문서 검색)",
|
|
29
|
-
inputSchema: SearchArgsSchema.shape
|
|
30
|
-
}, async (args) => {
|
|
31
|
-
const result = await searchToolHandlers.search_webkr(args);
|
|
32
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
33
|
-
});
|
|
34
34
|
server.registerTool("search_news", {
|
|
35
|
-
description: "
|
|
36
|
-
inputSchema: SearchArgsSchema.shape
|
|
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
37
|
}, async (args) => {
|
|
38
38
|
const result = await searchToolHandlers.search_news(args);
|
|
39
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
41
|
+
};
|
|
40
42
|
});
|
|
41
43
|
server.registerTool("search_blog", {
|
|
42
|
-
description: "
|
|
43
|
-
inputSchema: SearchArgsSchema.shape
|
|
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,
|
|
44
46
|
}, async (args) => {
|
|
45
47
|
const result = await searchToolHandlers.search_blog(args);
|
|
46
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
50
|
+
};
|
|
47
51
|
});
|
|
48
52
|
server.registerTool("search_shop", {
|
|
49
|
-
description: "
|
|
50
|
-
inputSchema: SearchArgsSchema.shape
|
|
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,
|
|
51
55
|
}, async (args) => {
|
|
52
56
|
const result = await searchToolHandlers.search_shop(args);
|
|
53
|
-
return {
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
59
|
+
};
|
|
54
60
|
});
|
|
55
61
|
server.registerTool("search_image", {
|
|
56
|
-
description: "
|
|
57
|
-
inputSchema: SearchArgsSchema.shape
|
|
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,
|
|
58
64
|
}, async (args) => {
|
|
59
65
|
const result = await searchToolHandlers.search_image(args);
|
|
60
|
-
return {
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
68
|
+
};
|
|
61
69
|
});
|
|
62
70
|
server.registerTool("search_kin", {
|
|
63
|
-
description: "
|
|
64
|
-
inputSchema: SearchArgsSchema.shape
|
|
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,
|
|
65
73
|
}, async (args) => {
|
|
66
74
|
const result = await searchToolHandlers.search_kin(args);
|
|
67
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
77
|
+
};
|
|
68
78
|
});
|
|
69
79
|
server.registerTool("search_book", {
|
|
70
|
-
description: "
|
|
71
|
-
inputSchema: SearchArgsSchema.shape
|
|
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,
|
|
72
82
|
}, async (args) => {
|
|
73
83
|
const result = await searchToolHandlers.search_book(args);
|
|
74
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
86
|
+
};
|
|
75
87
|
});
|
|
76
88
|
server.registerTool("search_encyc", {
|
|
77
|
-
description: "
|
|
78
|
-
inputSchema: SearchArgsSchema.shape
|
|
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,
|
|
79
91
|
}, async (args) => {
|
|
80
92
|
const result = await searchToolHandlers.search_encyc(args);
|
|
81
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
95
|
+
};
|
|
82
96
|
});
|
|
83
97
|
server.registerTool("search_academic", {
|
|
84
|
-
description: "
|
|
85
|
-
inputSchema: SearchArgsSchema.shape
|
|
98
|
+
description: "🎓 Search academic papers, research documents, and scholarly content. Access Korean academic resources, research papers, theses, and professional publications. (네이버 전문자료 검색 - 학술 논문과 전문 자료)",
|
|
99
|
+
inputSchema: SearchArgsSchema.shape,
|
|
86
100
|
}, async (args) => {
|
|
87
101
|
const result = await searchToolHandlers.search_academic(args);
|
|
88
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
104
|
+
};
|
|
89
105
|
});
|
|
90
106
|
server.registerTool("search_local", {
|
|
91
|
-
description: "
|
|
92
|
-
inputSchema: NaverLocalSearchParamsSchema.shape
|
|
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,
|
|
93
109
|
}, async (args) => {
|
|
94
110
|
const result = await searchToolHandlers.search_local(args);
|
|
95
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
113
|
+
};
|
|
96
114
|
});
|
|
97
115
|
server.registerTool("search_cafearticle", {
|
|
98
|
-
description: "
|
|
99
|
-
inputSchema: SearchArgsSchema.shape
|
|
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,
|
|
100
118
|
}, async (args) => {
|
|
101
119
|
const result = await searchToolHandlers.search_cafearticle(args);
|
|
102
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
122
|
+
};
|
|
103
123
|
});
|
|
104
124
|
// Register datalab tools
|
|
105
125
|
server.registerTool("datalab_search", {
|
|
106
|
-
description: "
|
|
107
|
-
inputSchema: DatalabSearchSchema.shape
|
|
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,
|
|
108
128
|
}, async (args) => {
|
|
109
129
|
const result = await datalabToolHandlers.datalab_search(args);
|
|
110
|
-
return {
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
132
|
+
};
|
|
111
133
|
});
|
|
112
134
|
server.registerTool("datalab_shopping_category", {
|
|
113
|
-
description: "
|
|
114
|
-
inputSchema: DatalabShoppingSchema.shape
|
|
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,
|
|
115
137
|
}, async (args) => {
|
|
116
138
|
const result = await datalabToolHandlers.datalab_shopping_category(args);
|
|
117
|
-
return {
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
141
|
+
};
|
|
118
142
|
});
|
|
119
143
|
server.registerTool("datalab_shopping_by_device", {
|
|
120
|
-
description: "
|
|
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 도구로 카테고리 코드를 찾으세요)",
|
|
121
145
|
inputSchema: DatalabShoppingDeviceSchema.pick({
|
|
122
146
|
startDate: true,
|
|
123
147
|
endDate: true,
|
|
124
148
|
timeUnit: true,
|
|
125
149
|
category: true,
|
|
126
150
|
device: true,
|
|
127
|
-
}).shape
|
|
151
|
+
}).shape,
|
|
128
152
|
}, async (args) => {
|
|
129
153
|
const result = await datalabToolHandlers.datalab_shopping_by_device(args);
|
|
130
|
-
return {
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
156
|
+
};
|
|
131
157
|
});
|
|
132
158
|
server.registerTool("datalab_shopping_by_gender", {
|
|
133
|
-
description: "
|
|
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 도구로 카테고리 코드를 찾으세요)",
|
|
134
160
|
inputSchema: DatalabShoppingGenderSchema.pick({
|
|
135
161
|
startDate: true,
|
|
136
162
|
endDate: true,
|
|
137
163
|
timeUnit: true,
|
|
138
164
|
category: true,
|
|
139
165
|
gender: true,
|
|
140
|
-
}).shape
|
|
166
|
+
}).shape,
|
|
141
167
|
}, async (args) => {
|
|
142
168
|
const result = await datalabToolHandlers.datalab_shopping_by_gender(args);
|
|
143
|
-
return {
|
|
169
|
+
return {
|
|
170
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
171
|
+
};
|
|
144
172
|
});
|
|
145
173
|
server.registerTool("datalab_shopping_by_age", {
|
|
146
|
-
description: "
|
|
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 도구로 카테고리 코드를 찾으세요)",
|
|
147
175
|
inputSchema: DatalabShoppingAgeSchema.pick({
|
|
148
176
|
startDate: true,
|
|
149
177
|
endDate: true,
|
|
150
178
|
timeUnit: true,
|
|
151
179
|
category: true,
|
|
152
180
|
ages: true,
|
|
153
|
-
}).shape
|
|
181
|
+
}).shape,
|
|
154
182
|
}, async (args) => {
|
|
155
183
|
const result = await datalabToolHandlers.datalab_shopping_by_age(args);
|
|
156
|
-
return {
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
186
|
+
};
|
|
157
187
|
});
|
|
158
188
|
server.registerTool("datalab_shopping_keywords", {
|
|
159
|
-
description: "
|
|
160
|
-
inputSchema: DatalabShoppingKeywordsSchema.shape
|
|
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,
|
|
161
191
|
}, async (args) => {
|
|
162
192
|
const result = await datalabToolHandlers.datalab_shopping_keywords(args);
|
|
163
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
195
|
+
};
|
|
164
196
|
});
|
|
165
197
|
server.registerTool("datalab_shopping_keyword_by_device", {
|
|
166
|
-
description: "
|
|
167
|
-
inputSchema: DatalabShoppingKeywordDeviceSchema.shape
|
|
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,
|
|
168
200
|
}, async (args) => {
|
|
169
201
|
const result = await datalabToolHandlers.datalab_shopping_keyword_by_device(args);
|
|
170
|
-
return {
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
204
|
+
};
|
|
171
205
|
});
|
|
172
206
|
server.registerTool("datalab_shopping_keyword_by_gender", {
|
|
173
|
-
description: "
|
|
174
|
-
inputSchema: DatalabShoppingKeywordGenderSchema.shape
|
|
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,
|
|
175
209
|
}, async (args) => {
|
|
176
210
|
const result = await datalabToolHandlers.datalab_shopping_keyword_by_gender(args);
|
|
177
|
-
return {
|
|
211
|
+
return {
|
|
212
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
213
|
+
};
|
|
178
214
|
});
|
|
179
215
|
server.registerTool("datalab_shopping_keyword_by_age", {
|
|
180
|
-
description: "
|
|
181
|
-
inputSchema: DatalabShoppingKeywordAgeSchema.shape
|
|
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,
|
|
182
218
|
}, async (args) => {
|
|
183
219
|
const result = await datalabToolHandlers.datalab_shopping_keyword_by_age(args);
|
|
184
|
-
return {
|
|
220
|
+
return {
|
|
221
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
222
|
+
};
|
|
185
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;
|
|
186
236
|
return server.server;
|
|
187
237
|
}
|
|
188
238
|
// Export default for Smithery compatibility
|
|
@@ -195,25 +245,29 @@ async function main() {
|
|
|
195
245
|
const clientId = process.env.NAVER_CLIENT_ID?.trim();
|
|
196
246
|
const clientSecret = process.env.NAVER_CLIENT_SECRET?.trim();
|
|
197
247
|
console.error("Environment variables:", {
|
|
198
|
-
NAVER_CLIENT_ID: process.env.NAVER_CLIENT_ID
|
|
199
|
-
|
|
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",
|
|
200
254
|
});
|
|
201
255
|
if (!clientId || !clientSecret) {
|
|
202
256
|
throw new Error(`Missing required environment variables:
|
|
203
|
-
NAVER_CLIENT_ID: ${clientId ?
|
|
204
|
-
NAVER_CLIENT_SECRET: ${clientSecret ?
|
|
257
|
+
NAVER_CLIENT_ID: ${clientId ? "provided" : "missing"}
|
|
258
|
+
NAVER_CLIENT_SECRET: ${clientSecret ? "provided" : "missing"}
|
|
205
259
|
|
|
206
260
|
Please set these environment variables before running the server.`);
|
|
207
261
|
}
|
|
208
262
|
const config = {
|
|
209
263
|
NAVER_CLIENT_ID: clientId,
|
|
210
|
-
NAVER_CLIENT_SECRET: clientSecret
|
|
264
|
+
NAVER_CLIENT_SECRET: clientSecret,
|
|
211
265
|
};
|
|
212
266
|
console.error("Config loaded successfully");
|
|
213
267
|
// Validate config
|
|
214
268
|
const validatedConfig = configSchema.parse(config);
|
|
215
269
|
console.error("Config validated successfully");
|
|
216
|
-
// Create server instance
|
|
270
|
+
// Create server instance
|
|
217
271
|
const serverFactory = createNaverSearchServer({ config: validatedConfig });
|
|
218
272
|
console.error("Server factory created");
|
|
219
273
|
// Create transport and run server
|
|
@@ -230,12 +284,31 @@ async function main() {
|
|
|
230
284
|
// Run main function if this file is executed directly
|
|
231
285
|
// Note: Always run main in CLI mode since this is an MCP server
|
|
232
286
|
console.error("Starting MCP server initialization...");
|
|
233
|
-
console.error("import.meta.url:", import.meta.url);
|
|
234
287
|
console.error("process.argv:", process.argv);
|
|
235
|
-
// Check if running as main module
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
}
|
|
239
312
|
console.error("isMainModule:", isMainModule);
|
|
240
313
|
if (isMainModule) {
|
|
241
314
|
console.error("Running as main module, starting server...");
|
|
@@ -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.
|
|
3
|
+
"version": "1.0.40",
|
|
4
4
|
"description": "Naver Search MCP Server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"scripts": {
|
|
12
12
|
"prebuild": "npx shx rm -rf dist",
|
|
13
|
-
"build": "tsc",
|
|
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"
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"zod": "^3.23.0",
|
|
25
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
|
-
// Remove zodToJsonSchema import - using Zod schemas directly
|
|
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: DatalabSearchSchema.shape,
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
name: "datalab_shopping_category",
|
|
14
|
-
description: "Perform a trend analysis on Naver Shopping category. (네이버 쇼핑 카테고리별 트렌드 분석)",
|
|
15
|
-
inputSchema: DatalabShoppingSchema.shape,
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
name: "datalab_shopping_by_device",
|
|
19
|
-
description: "Perform a trend analysis on Naver Shopping by device. (네이버 쇼핑 기기별 트렌드 분석)",
|
|
20
|
-
inputSchema: DatalabShoppingDeviceSchema.pick({
|
|
21
|
-
startDate: true,
|
|
22
|
-
endDate: true,
|
|
23
|
-
timeUnit: true,
|
|
24
|
-
category: true,
|
|
25
|
-
device: true,
|
|
26
|
-
}).shape,
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
name: "datalab_shopping_by_gender",
|
|
30
|
-
description: "Perform a trend analysis on Naver Shopping by gender. (네이버 쇼핑 성별 트렌드 분석)",
|
|
31
|
-
inputSchema: DatalabShoppingGenderSchema.pick({
|
|
32
|
-
startDate: true,
|
|
33
|
-
endDate: true,
|
|
34
|
-
timeUnit: true,
|
|
35
|
-
category: true,
|
|
36
|
-
gender: true,
|
|
37
|
-
}).shape,
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
name: "datalab_shopping_by_age",
|
|
41
|
-
description: "Perform a trend analysis on Naver Shopping by age. (네이버 쇼핑 연령별 트렌드 분석)",
|
|
42
|
-
inputSchema: DatalabShoppingAgeSchema.pick({
|
|
43
|
-
startDate: true,
|
|
44
|
-
endDate: true,
|
|
45
|
-
timeUnit: true,
|
|
46
|
-
category: true,
|
|
47
|
-
ages: true,
|
|
48
|
-
}).shape,
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
name: "datalab_shopping_keywords",
|
|
52
|
-
description: "Perform a trend analysis on Naver Shopping keywords. (네이버 쇼핑 키워드별 트렌드 분석)",
|
|
53
|
-
inputSchema: DatalabShoppingKeywordsSchema.shape,
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
name: "datalab_shopping_keyword_by_device",
|
|
57
|
-
description: "Perform a trend analysis on Naver Shopping keywords by device. (네이버 쇼핑 키워드 기기별 트렌드 분석)",
|
|
58
|
-
inputSchema: DatalabShoppingKeywordDeviceSchema.shape,
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: "datalab_shopping_keyword_by_gender",
|
|
62
|
-
description: "Perform a trend analysis on Naver Shopping keywords by gender. (네이버 쇼핑 키워드 성별 트렌드 분석)",
|
|
63
|
-
inputSchema: DatalabShoppingKeywordGenderSchema.shape,
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
name: "datalab_shopping_keyword_by_age",
|
|
67
|
-
description: "Perform a trend analysis on Naver Shopping keywords by age. (네이버 쇼핑 키워드 연령별 트렌드 분석)",
|
|
68
|
-
inputSchema: DatalabShoppingKeywordAgeSchema.shape,
|
|
69
|
-
},
|
|
70
|
-
];
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { SearchArgsSchema, NaverLocalSearchParamsSchema, } from "../schemas/search.schemas.js";
|
|
2
|
-
/**
|
|
3
|
-
* 검색 관련 도구 정의
|
|
4
|
-
*/
|
|
5
|
-
export const searchTools = [
|
|
6
|
-
{
|
|
7
|
-
name: "search_webkr",
|
|
8
|
-
description: "Perform a search on Naver Web Documents. (네이버 웹문서 검색)",
|
|
9
|
-
inputSchema: SearchArgsSchema.shape,
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
name: "search_news",
|
|
13
|
-
description: "Perform a search on Naver News. (네이버 뉴스 검색)",
|
|
14
|
-
inputSchema: SearchArgsSchema.shape,
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
name: "search_blog",
|
|
18
|
-
description: "Perform a search on Naver Blog. (네이버 블로그 검색)",
|
|
19
|
-
inputSchema: SearchArgsSchema.shape,
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
name: "search_shop",
|
|
23
|
-
description: "Perform a search on Naver Shopping. (네이버 쇼핑 검색)",
|
|
24
|
-
inputSchema: SearchArgsSchema.shape,
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: "search_image",
|
|
28
|
-
description: "Perform a search on Naver Image. (네이버 이미지 검색)",
|
|
29
|
-
inputSchema: SearchArgsSchema.shape,
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: "search_kin",
|
|
33
|
-
description: "Perform a search on Naver KnowledgeiN. (네이버 지식iN 검색)",
|
|
34
|
-
inputSchema: SearchArgsSchema.shape,
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
name: "search_book",
|
|
38
|
-
description: "Perform a search on Naver Book. (네이버 책 검색)",
|
|
39
|
-
inputSchema: SearchArgsSchema.shape,
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
name: "search_encyc",
|
|
43
|
-
description: "Perform a search on Naver Encyclopedia. (네이버 지식백과 검색)",
|
|
44
|
-
inputSchema: SearchArgsSchema.shape,
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
name: "search_academic",
|
|
48
|
-
description: "Perform a search on Naver Academic. (네이버 전문자료 검색)",
|
|
49
|
-
inputSchema: SearchArgsSchema.shape,
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
name: "search_local",
|
|
53
|
-
description: "Perform a search on Naver Local. (네이버 지역 검색)",
|
|
54
|
-
inputSchema: NaverLocalSearchParamsSchema.shape,
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: "search_cafearticle",
|
|
58
|
-
description: "Perform a search on Naver Cafe Articles. (네이버 카페글 검색)",
|
|
59
|
-
inputSchema: SearchArgsSchema.shape,
|
|
60
|
-
},
|
|
61
|
-
];
|