@isnow890/naver-search-mcp 1.0.47 → 1.0.49
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 +112 -13
- package/dist/src/clients/naver-api-core.client.d.ts +13 -2
- package/dist/src/clients/naver-api-core.client.js +42 -12
- package/dist/src/clients/naver-api-endpoints.d.ts +16 -0
- package/dist/src/clients/naver-api-endpoints.js +22 -0
- package/dist/src/clients/naver-api-error.d.ts +5 -0
- package/dist/src/clients/naver-api-error.js +33 -0
- package/dist/src/clients/naver-search.client.d.ts +2 -6
- package/dist/src/clients/naver-search.client.js +9 -15
- package/dist/src/config/credentials.d.ts +20 -0
- package/dist/src/config/credentials.js +53 -0
- package/dist/src/handlers/category.handlers.js +17 -13
- package/dist/src/handlers/search.handlers.d.ts +0 -12
- package/dist/src/handlers/search.handlers.js +0 -30
- package/dist/src/index.d.ts +12 -6
- package/dist/src/index.js +47 -56
- package/dist/src/schemas/search.schemas.d.ts +4 -15
- package/dist/src/schemas/search.schemas.js +5 -13
- package/dist/src/types/search.types.d.ts +0 -8
- package/package.json +3 -3
- package/dist/data/README.md +0 -26
package/README.md
CHANGED
|
@@ -3,11 +3,39 @@
|
|
|
3
3
|
[](README-ko.md)
|
|
4
4
|
|
|
5
5
|
[](https://archestra.ai/mcp-catalog/isnow890__naver-search-mcp)
|
|
6
|
-
[](https://smithery.ai/server/@isnow890/naver-search-mcp)
|
|
7
6
|
[](https://mcp.so/server/naver-search-mcp/isnow890)
|
|
8
7
|
|
|
9
8
|
MCP server for Naver Search API and DataLab API integration, enabling comprehensive search across various Naver services and data trend analysis.
|
|
10
9
|
|
|
10
|
+
## ⚠️ Naver is migrating these APIs — read this before you set up
|
|
11
|
+
|
|
12
|
+
Naver is moving Search, Search Trend, and Shopping Insight from the Naver Developers Center to **NAVER API HUB** on NAVER Cloud Platform, and is shutting down three search APIs entirely.
|
|
13
|
+
|
|
14
|
+
| Date | What happens |
|
|
15
|
+
|---|---|
|
|
16
|
+
| **2026-07-31** | Developers Center stops accepting **new** key applications.<br>Shopping / Book / Academic search APIs shut down completely. |
|
|
17
|
+
| **2027-06-30** | Developers Center support ends — **existing keys stop working**. |
|
|
18
|
+
|
|
19
|
+
Source: [Naver Developers Center official notice](https://developers.naver.com/notice/article/32530) (Korean) — "Search API, Search Trend, Shopping Insight 서비스 종료 및 NAVER API HUB 이관 안내".
|
|
20
|
+
|
|
21
|
+
**Setting this up for the first time?** Get your keys from NAVER API HUB. The Developers Center path closes to new applicants on 2026-07-31, so it is no longer the place to start. Step-by-step instructions are in [Option A: NAVER API HUB](#option-a-naver-api-hub-recommended--the-forward-path) below.
|
|
22
|
+
|
|
23
|
+
**Already running with Developers Center keys?** Nothing to change. They keep working until 2027-06-30, and this version supports both platforms from the same install. When you're ready, get a HUB key and swap the two environment variables — no other changes.
|
|
24
|
+
|
|
25
|
+
**Support plan for this server:** both platforms stay supported side by side through Naver's 2027-06-30 cutoff. The Developers Center path will be dropped in a later major version only after that date, once it can no longer work for anyone — so upgrading in the meantime will never take your working setup away.
|
|
26
|
+
|
|
27
|
+
**Three tools were removed in 1.0.49**: `search_shop`, `search_book`, `search_academic`. Naver shuts those search APIs down on 2026-07-31 with no replacement on any platform, so there is nothing this server can do to keep them. Shopping **Insight** (`datalab_shopping_*`) and `find_category` are a *different* API and are **not** affected.
|
|
28
|
+
|
|
29
|
+
## Available on ClawHub for OpenClaw
|
|
30
|
+
|
|
31
|
+
You can install this MCP server as an OpenClaw skill from ClawHub:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
openclaw skills install naver-search-mcp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The ClawHub skill uses the published npm package internally and needs **one** credential pair in your OpenClaw environment — either the NAVER API HUB pair (`NCP_APIGW_API_KEY_ID` / `NCP_APIGW_API_KEY`) or the Developers Center pair (`NAVER_CLIENT_ID` / `NAVER_CLIENT_SECRET`). OpenClaw's `apiKey` field maps to `NAVER_CLIENT_SECRET`; HUB users should set both HUB variables explicitly in the environment instead of using `apiKey`.
|
|
38
|
+
|
|
11
39
|
## Quick Start: Use Without API Key
|
|
12
40
|
|
|
13
41
|
You can use this server immediately without API keys through [Kakao PlayMCP](https://playmcp.kakao.com/mcp/154). Simply visit the link and start using it right away!
|
|
@@ -26,14 +54,16 @@ You can use this server immediately without API keys through [Kakao PlayMCP](htt
|
|
|
26
54
|
- **search_news**: Search Naver news
|
|
27
55
|
- **search_blog**: Search Naver blogs
|
|
28
56
|
- **search_cafearticle**: Search Naver cafe articles
|
|
29
|
-
- **search_shop**: Search Naver shopping
|
|
30
57
|
- **search_image**: Search Naver images
|
|
31
58
|
- **search_kin**: Search Naver KnowledgeiN
|
|
32
|
-
- **search_book**: Search Naver books
|
|
33
59
|
- **search_encyc**: Search Naver encyclopedia
|
|
34
|
-
- **search_academic**: Search Naver academic papers
|
|
35
60
|
- **search_local**: Search Naver local places
|
|
36
61
|
|
|
62
|
+
> **Removed in 1.0.49:** `search_shop`, `search_book`, `search_academic`.
|
|
63
|
+
> Naver shuts down the Shopping / Book / Academic search APIs on 2026-07-31 with
|
|
64
|
+
> no replacement on any platform. This is not a limitation of this server.
|
|
65
|
+
> Shopping **Insight** (`datalab_shopping_*`) is a different API and is unaffected.
|
|
66
|
+
|
|
37
67
|
#### DataLab Tools
|
|
38
68
|
|
|
39
69
|
- **datalab_search**: Analyze search term trends
|
|
@@ -48,18 +78,67 @@ You can use this server immediately without API keys through [Kakao PlayMCP](htt
|
|
|
48
78
|
|
|
49
79
|
## Getting API Keys
|
|
50
80
|
|
|
81
|
+
Two platforms issue keys for this server. Set **one** pair of environment variables — see
|
|
82
|
+
[Configuration](#configuration) below for how the pair you set determines the platform used.
|
|
83
|
+
|
|
84
|
+
### Option A: NAVER API HUB (recommended — the forward path)
|
|
85
|
+
|
|
86
|
+
1. Go to the [NAVER Cloud Platform console](https://www.ncloud.com) and sign up or log in.
|
|
87
|
+
2. Click the region & platform selector in the top right of the console, choose your region/platform, and click **Apply**.
|
|
88
|
+
3. Open **Menu > All Services > Application Services > [NAVER API HUB](https://www.ncloud.com/product/applicationService/naverApiHub)**.
|
|
89
|
+
4. Click **Application**, select (or create) your application, then under API management click **인증 정보 (Authentication Info)**.
|
|
90
|
+
5. Copy the **Client ID** and **Client Secret** shown in the popup.
|
|
91
|
+
6. Use these as `NCP_APIGW_API_KEY_ID` (Client ID) and `NCP_APIGW_API_KEY` (Client Secret) in the configuration below.
|
|
92
|
+
|
|
93
|
+
### Option B: Naver Developers (legacy — existing keys only)
|
|
94
|
+
|
|
95
|
+
> Naver Developers Center stops accepting new applications on **2026-07-31**. If you don't
|
|
96
|
+
> already have a Client ID/Secret from this platform, use NAVER API HUB above instead.
|
|
97
|
+
> Existing Developers Center keys keep working until 2027-06-30.
|
|
98
|
+
|
|
51
99
|
1. Visit [Naver Developers](https://developers.naver.com/apps/#/register) and log in with your Naver account
|
|
52
100
|
2. Click the "Application Registration" (애플리케이션 등록) button
|
|
53
101
|
3. Fill in the application information:
|
|
54
102
|
- **Application Name**: Enter any name (e.g., "Naver Search MCP")
|
|
55
103
|
- **Usage**: Select "Search" (검색)
|
|
56
104
|
4. In the API Settings section, check ALL of the following APIs:
|
|
57
|
-
- **Search** (검색) - Required for blog, news,
|
|
105
|
+
- **Search** (검색) - Required for blog, news, cafe article, web, image, kin, encyclopedia, and local search
|
|
58
106
|
- **DataLab - Search Trends** (데이터랩 - 검색어 트렌드) - Required for search term trend analysis
|
|
59
107
|
- **DataLab - Shopping Insight** (데이터랩 - 쇼핑인사이트) - Required for shopping trend analysis
|
|
60
108
|
5. Click "Register" to complete registration
|
|
61
109
|
6. After registration, you'll see your **Client ID** and **Client Secret** on the application detail page
|
|
62
|
-
7. Use these
|
|
110
|
+
7. Use these as `NAVER_CLIENT_ID` / `NAVER_CLIENT_SECRET` in the configuration below
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
### Credentials
|
|
115
|
+
|
|
116
|
+
Two platforms are supported. Set **one** pair — the server picks the platform from
|
|
117
|
+
which variables you set, since the key strings themselves are indistinguishable.
|
|
118
|
+
|
|
119
|
+
| Variables | Platform | Endpoint |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `NCP_APIGW_API_KEY_ID`, `NCP_APIGW_API_KEY` | NAVER API HUB (NCP) | `naverapihub.apigw.ntruss.com` |
|
|
122
|
+
| `NAVER_CLIENT_ID`, `NAVER_CLIENT_SECRET` | Naver Developers (legacy) | `openapi.naver.com` |
|
|
123
|
+
|
|
124
|
+
If both pairs are set, NAVER API HUB wins.
|
|
125
|
+
|
|
126
|
+
The installation examples below use the legacy `NAVER_CLIENT_ID` / `NAVER_CLIENT_SECRET`
|
|
127
|
+
pair; swap in the HUB pair from the table above if that's what you have.
|
|
128
|
+
|
|
129
|
+
### Migration to NAVER API HUB
|
|
130
|
+
|
|
131
|
+
Naver is moving these APIs from the Developers Center to NAVER API HUB on NAVER Cloud Platform.
|
|
132
|
+
|
|
133
|
+
| Date | What happens |
|
|
134
|
+
|---|---|
|
|
135
|
+
| 2026-06-25 | NAVER API HUB launched |
|
|
136
|
+
| 2026-07-31 | Developers Center stops accepting new applications |
|
|
137
|
+
| 2027-06-30 | Developers Center support ends — existing keys stop working |
|
|
138
|
+
|
|
139
|
+
Existing keys keep working until 2027-06-30. To migrate, get a key from the
|
|
140
|
+
NAVER Cloud Platform console and set `NCP_APIGW_API_KEY_ID` / `NCP_APIGW_API_KEY`.
|
|
141
|
+
Developers Center keys cannot be used against NAVER API HUB.
|
|
63
142
|
|
|
64
143
|
## Installation
|
|
65
144
|
|
|
@@ -105,14 +184,22 @@ Add to your Claude Code settings:
|
|
|
105
184
|
}
|
|
106
185
|
```
|
|
107
186
|
|
|
108
|
-
### Method 2:
|
|
187
|
+
### Method 2: ClawHub Installation for OpenClaw
|
|
109
188
|
|
|
110
|
-
Install
|
|
189
|
+
This MCP server can also be used from OpenClaw through the ClawHub skill wrapper. Install it with:
|
|
111
190
|
|
|
112
191
|
```bash
|
|
113
|
-
|
|
192
|
+
openclaw skills install naver-search-mcp
|
|
114
193
|
```
|
|
115
194
|
|
|
195
|
+
The ClawHub skill uses the same published npm package internally:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npx -y @isnow890/naver-search-mcp
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Make sure **one** credential pair is configured in your OpenClaw environment before using the skill — the NAVER API HUB pair (`NCP_APIGW_API_KEY_ID` / `NCP_APIGW_API_KEY`) or the Developers Center pair (`NAVER_CLIENT_ID` / `NAVER_CLIENT_SECRET`). OpenClaw's `apiKey` field maps to `NAVER_CLIENT_SECRET`; HUB users should set both HUB variables explicitly instead.
|
|
202
|
+
|
|
116
203
|
### Method 3: Local Installation
|
|
117
204
|
|
|
118
205
|
For local development or custom modifications:
|
|
@@ -222,7 +309,7 @@ After completing the configuration, completely close and restart Claude Desktop
|
|
|
222
309
|
|
|
223
310
|
## Prerequisites
|
|
224
311
|
|
|
225
|
-
-
|
|
312
|
+
- A credential pair for one platform — NAVER API HUB or Naver Developers (see [Getting API Keys](#getting-api-keys))
|
|
226
313
|
- Node.js 18 or higher
|
|
227
314
|
- NPM 8 or higher
|
|
228
315
|
|
|
@@ -234,6 +321,18 @@ MIT License
|
|
|
234
321
|
|
|
235
322
|
## Version History
|
|
236
323
|
|
|
324
|
+
### 1.0.49 (2026-07-26)
|
|
325
|
+
|
|
326
|
+
- NAVER API HUB support: set `NCP_APIGW_API_KEY_ID` / `NCP_APIGW_API_KEY` to call the new platform. Existing `NAVER_CLIENT_ID` / `NAVER_CLIENT_SECRET` keep working unchanged — the platform is chosen by which variable pair you set
|
|
327
|
+
- **Removed** `search_shop`, `search_book`, `search_academic`. Naver shuts those search APIs down on 2026-07-31 with no replacement on any platform
|
|
328
|
+
- API errors now throw a plain `Error` instead of the raw `AxiosError`. Code branching on `error.response?.status` sees `undefined`; the same detail is in `Error.message`
|
|
329
|
+
- Error messages name the platform and HTTP status, and 401s suggest checking whether a key was set on the other platform's variables
|
|
330
|
+
|
|
331
|
+
### 1.0.48 (2026-05-12)
|
|
332
|
+
|
|
333
|
+
- Fixed `find_category` data loading when the MCP server is launched by `npx` from a different working directory
|
|
334
|
+
- Category data is now resolved relative to the installed package before falling back to local development paths
|
|
335
|
+
|
|
237
336
|
### 1.0.47 (2025-01-03)
|
|
238
337
|
|
|
239
338
|
- **Added "today" keyword support** for all DataLab date parameters - no need to call separate time tool
|
|
@@ -245,10 +344,10 @@ MIT License
|
|
|
245
344
|
|
|
246
345
|
### 1.0.45 (2025-09-28)
|
|
247
346
|
|
|
248
|
-
- Resolved
|
|
347
|
+
- Resolved platform compatibility issues for hosted MCP installations
|
|
249
348
|
- Replaced the Excel export in category search with JSON for better compatibility
|
|
250
349
|
- Restored the `search_webkr` tool for Korean web search
|
|
251
|
-
-
|
|
350
|
+
- Improved hosted platform installation compatibility
|
|
252
351
|
|
|
253
352
|
### 1.0.44 (2025-08-31)
|
|
254
353
|
|
|
@@ -267,7 +366,7 @@ MIT License
|
|
|
267
366
|
### 1.0.30 (2025-08-04)
|
|
268
367
|
|
|
269
368
|
- MCP SDK upgraded to 1.17.1
|
|
270
|
-
- Fixed compatibility issues with
|
|
369
|
+
- Fixed compatibility issues with hosted MCP platform specification changes
|
|
271
370
|
- Added comprehensive DataLab shopping category code documentation
|
|
272
371
|
|
|
273
372
|
### 1.0.2 (2025-04-26)
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import { AxiosRequestConfig, AxiosInstance } from "axios";
|
|
2
2
|
import { NaverSearchConfig } from "../schemas/search.schemas.js";
|
|
3
|
+
import { NaverApiProvider } from "./naver-api-endpoints.js";
|
|
3
4
|
export declare abstract class NaverApiCoreClient {
|
|
4
|
-
protected searchBaseUrl: string;
|
|
5
|
-
protected datalabBaseUrl: string;
|
|
6
5
|
protected config: NaverSearchConfig | null;
|
|
7
6
|
protected axiosInstance: AxiosInstance;
|
|
8
7
|
constructor();
|
|
9
8
|
initialize(config: NaverSearchConfig): void;
|
|
9
|
+
private requireConfig;
|
|
10
|
+
get provider(): NaverApiProvider;
|
|
11
|
+
/** 검색 API base. `${searchBaseUrl}/${type}` 형태로 사용한다. */
|
|
12
|
+
get searchBaseUrl(): string;
|
|
13
|
+
/** 검색어 트렌드 전체 URL. suffix를 붙이지 않는다. */
|
|
14
|
+
get trendUrl(): string;
|
|
15
|
+
/** 쇼핑인사이트 base. `${shoppingBaseUrl}/categories` 형태로 사용한다. */
|
|
16
|
+
get shoppingBaseUrl(): string;
|
|
10
17
|
protected getHeaders(contentType?: string): AxiosRequestConfig;
|
|
11
18
|
protected get<T>(url: string, params: any): Promise<T>;
|
|
12
19
|
protected post<T>(url: string, data: any): Promise<T>;
|
|
20
|
+
/**
|
|
21
|
+
* 두 플랫폼의 오류 응답 형식이 다르므로 파싱하지 않고 그대로 감싼다.
|
|
22
|
+
*/
|
|
23
|
+
private wrapError;
|
|
13
24
|
/**
|
|
14
25
|
* 리소스 정리 메서드 (메모리 누수 방지)
|
|
15
26
|
*/
|
|
@@ -1,58 +1,88 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
|
+
import { getEndpoints, getAuthHeaderNames, } from "./naver-api-endpoints.js";
|
|
3
|
+
import { formatApiError } from "./naver-api-error.js";
|
|
2
4
|
export class NaverApiCoreClient {
|
|
3
5
|
constructor() {
|
|
4
|
-
this.searchBaseUrl = "https://openapi.naver.com/v1/search";
|
|
5
|
-
this.datalabBaseUrl = "https://openapi.naver.com/v1/datalab";
|
|
6
6
|
this.config = null;
|
|
7
7
|
// HTTP/HTTPS 에이전트 설정으로 연결 풀링 및 메모리 누수 방지
|
|
8
8
|
this.axiosInstance = axios.create({
|
|
9
9
|
timeout: 30000, // 30초 타임아웃
|
|
10
10
|
maxRedirects: 3,
|
|
11
|
-
// HTTP 연결 풀링 설정은 운영체제에서 처리하도록 단순화
|
|
12
11
|
});
|
|
13
12
|
}
|
|
14
13
|
initialize(config) {
|
|
15
14
|
this.config = config;
|
|
16
15
|
}
|
|
17
|
-
|
|
16
|
+
requireConfig() {
|
|
18
17
|
if (!this.config)
|
|
19
18
|
throw new Error("NaverApiCoreClient is not initialized.");
|
|
19
|
+
return this.config;
|
|
20
|
+
}
|
|
21
|
+
get provider() {
|
|
22
|
+
return this.requireConfig().provider;
|
|
23
|
+
}
|
|
24
|
+
/** 검색 API base. `${searchBaseUrl}/${type}` 형태로 사용한다. */
|
|
25
|
+
get searchBaseUrl() {
|
|
26
|
+
return getEndpoints(this.provider).search;
|
|
27
|
+
}
|
|
28
|
+
/** 검색어 트렌드 전체 URL. suffix를 붙이지 않는다. */
|
|
29
|
+
get trendUrl() {
|
|
30
|
+
return getEndpoints(this.provider).trend;
|
|
31
|
+
}
|
|
32
|
+
/** 쇼핑인사이트 base. `${shoppingBaseUrl}/categories` 형태로 사용한다. */
|
|
33
|
+
get shoppingBaseUrl() {
|
|
34
|
+
return getEndpoints(this.provider).shopping;
|
|
35
|
+
}
|
|
36
|
+
getHeaders(contentType = "application/json") {
|
|
37
|
+
const config = this.requireConfig();
|
|
38
|
+
const names = getAuthHeaderNames(config.provider);
|
|
20
39
|
return {
|
|
21
40
|
headers: {
|
|
22
|
-
|
|
23
|
-
|
|
41
|
+
[names.id]: config.clientId,
|
|
42
|
+
[names.secret]: config.clientSecret,
|
|
24
43
|
"Content-Type": contentType,
|
|
25
44
|
},
|
|
26
45
|
};
|
|
27
46
|
}
|
|
28
47
|
async get(url, params) {
|
|
48
|
+
// provider를 요청 시점에 고정한다. 응답이 도착하기 전에 destroyInstance()가
|
|
49
|
+
// config를 비울 수 있고, 그때 this.provider를 읽으면 원래 에러가 사라진다.
|
|
50
|
+
const provider = this.provider;
|
|
29
51
|
try {
|
|
30
52
|
const response = await this.axiosInstance.get(url, {
|
|
31
53
|
params,
|
|
32
|
-
...this.getHeaders()
|
|
54
|
+
...this.getHeaders(),
|
|
33
55
|
});
|
|
34
56
|
return response.data;
|
|
35
57
|
}
|
|
36
58
|
catch (error) {
|
|
37
|
-
|
|
38
|
-
throw error;
|
|
59
|
+
throw this.wrapError(provider, url, error);
|
|
39
60
|
}
|
|
40
61
|
}
|
|
41
62
|
async post(url, data) {
|
|
63
|
+
const provider = this.provider;
|
|
42
64
|
try {
|
|
43
65
|
const response = await this.axiosInstance.post(url, data, this.getHeaders());
|
|
44
66
|
return response.data;
|
|
45
67
|
}
|
|
46
68
|
catch (error) {
|
|
47
|
-
|
|
48
|
-
throw error;
|
|
69
|
+
throw this.wrapError(provider, url, error);
|
|
49
70
|
}
|
|
50
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* 두 플랫폼의 오류 응답 형식이 다르므로 파싱하지 않고 그대로 감싼다.
|
|
74
|
+
*/
|
|
75
|
+
wrapError(provider, url, error) {
|
|
76
|
+
const response = error?.response;
|
|
77
|
+
// 응답이 아예 없는 실패(타임아웃, DNS 실패, 연결 거부)는 axios 메시지가 유일한
|
|
78
|
+
// 단서다. 이걸 버리면 모든 네트워크 오류가 똑같은 문장으로 뭉개진다.
|
|
79
|
+
const body = response ? response.data : error?.message;
|
|
80
|
+
return new Error(formatApiError(provider, url, response?.status, body));
|
|
81
|
+
}
|
|
51
82
|
/**
|
|
52
83
|
* 리소스 정리 메서드 (메모리 누수 방지)
|
|
53
84
|
*/
|
|
54
85
|
cleanup() {
|
|
55
|
-
// 연결 정리 - axios가 자동으로 처리
|
|
56
86
|
this.config = null;
|
|
57
87
|
}
|
|
58
88
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** 네이버 API 제공 플랫폼 */
|
|
2
|
+
export type NaverApiProvider = "legacy" | "hub";
|
|
3
|
+
export interface NaverApiEndpoints {
|
|
4
|
+
/** 검색 API base. `${search}/${type}` 형태로 사용한다. */
|
|
5
|
+
search: string;
|
|
6
|
+
/** 검색어 트렌드 전체 URL. suffix를 붙이지 않는다. */
|
|
7
|
+
trend: string;
|
|
8
|
+
/** 쇼핑인사이트 base. `${shopping}/categories` 형태로 사용한다. */
|
|
9
|
+
shopping: string;
|
|
10
|
+
}
|
|
11
|
+
export interface NaverAuthHeaderNames {
|
|
12
|
+
id: string;
|
|
13
|
+
secret: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function getEndpoints(provider: NaverApiProvider): NaverApiEndpoints;
|
|
16
|
+
export declare function getAuthHeaderNames(provider: NaverApiProvider): NaverAuthHeaderNames;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const ENDPOINTS = {
|
|
2
|
+
legacy: {
|
|
3
|
+
search: "https://openapi.naver.com/v1/search",
|
|
4
|
+
trend: "https://openapi.naver.com/v1/datalab/search",
|
|
5
|
+
shopping: "https://openapi.naver.com/v1/datalab/shopping",
|
|
6
|
+
},
|
|
7
|
+
hub: {
|
|
8
|
+
search: "https://naverapihub.apigw.ntruss.com/search/v1",
|
|
9
|
+
trend: "https://naverapihub.apigw.ntruss.com/search-trend/v1/search",
|
|
10
|
+
shopping: "https://naverapihub.apigw.ntruss.com/shopping/v1",
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
const AUTH_HEADER_NAMES = {
|
|
14
|
+
legacy: { id: "X-Naver-Client-Id", secret: "X-Naver-Client-Secret" },
|
|
15
|
+
hub: { id: "X-NCP-APIGW-API-KEY-ID", secret: "X-NCP-APIGW-API-KEY" },
|
|
16
|
+
};
|
|
17
|
+
export function getEndpoints(provider) {
|
|
18
|
+
return ENDPOINTS[provider];
|
|
19
|
+
}
|
|
20
|
+
export function getAuthHeaderNames(provider) {
|
|
21
|
+
return AUTH_HEADER_NAMES[provider];
|
|
22
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const PROVIDER_LABEL = {
|
|
2
|
+
legacy: "네이버 개발자센터",
|
|
3
|
+
hub: "NAVER API HUB",
|
|
4
|
+
};
|
|
5
|
+
const SWAPPED_KEY_HINT = {
|
|
6
|
+
legacy: "인증에 실패했습니다. NAVER API HUB 키를 NAVER_CLIENT_ID/NAVER_CLIENT_SECRET에 넣지 않았는지 확인하세요. HUB 키는 NCP_APIGW_API_KEY_ID/NCP_APIGW_API_KEY에 넣어야 합니다.",
|
|
7
|
+
hub: "인증에 실패했습니다. 개발자센터 키를 NCP_APIGW_API_KEY_ID/NCP_APIGW_API_KEY에 넣지 않았는지 확인하세요. 개발자센터 키는 NAVER_CLIENT_ID/NAVER_CLIENT_SECRET에 넣어야 합니다.",
|
|
8
|
+
};
|
|
9
|
+
function stringifyBody(body) {
|
|
10
|
+
if (body === undefined || body === null)
|
|
11
|
+
return "";
|
|
12
|
+
if (typeof body === "string")
|
|
13
|
+
return body;
|
|
14
|
+
try {
|
|
15
|
+
return JSON.stringify(body);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return String(body);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 두 플랫폼의 오류 응답 형식이 다르므로 파싱하지 않고 그대로 실어 보낸다.
|
|
23
|
+
*/
|
|
24
|
+
export function formatApiError(provider, url, status, body) {
|
|
25
|
+
const statusText = status === undefined ? "no response" : `HTTP ${status}`;
|
|
26
|
+
const lines = [`[${PROVIDER_LABEL[provider]}] ${statusText} — ${url}`];
|
|
27
|
+
const bodyText = stringifyBody(body);
|
|
28
|
+
if (bodyText)
|
|
29
|
+
lines.push(bodyText);
|
|
30
|
+
if (status === 401)
|
|
31
|
+
lines.push(SWAPPED_KEY_HINT[provider]);
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NaverApiCoreClient } from "./naver-api-core.client.js";
|
|
2
|
-
import { NaverSearchType, NaverSearchConfig, NaverLocalSearchParams,
|
|
3
|
-
import {
|
|
2
|
+
import { NaverSearchType, NaverSearchConfig, NaverLocalSearchParams, SearchArgs } from "../schemas/search.schemas.js";
|
|
3
|
+
import { NaverLocalSearchResponse } from "../types/search.types.js";
|
|
4
4
|
import { DatalabSearchRequest, DatalabShoppingResponse, DatalabShoppingCategoryRequest, DatalabShoppingDeviceRequest, DatalabShoppingGenderRequest, DatalabShoppingAgeRequest, DatalabShoppingKeywordsRequest, DatalabShoppingKeywordRequest } from "../types/datalab.types.js";
|
|
5
5
|
/**
|
|
6
6
|
* NaverSearchClient - 네이버 API 서비스를 위한 싱글톤 클라이언트
|
|
@@ -31,10 +31,6 @@ export declare class NaverSearchClient extends NaverApiCoreClient {
|
|
|
31
31
|
* @param params 검색 파라미터
|
|
32
32
|
*/
|
|
33
33
|
search(type: NaverSearchType, params: SearchArgs): Promise<any>;
|
|
34
|
-
/**
|
|
35
|
-
* 전문자료 검색 메서드
|
|
36
|
-
*/
|
|
37
|
-
searchAcademic(params: NaverDocumentSearchParams): Promise<NaverDocumentSearchResponse>;
|
|
38
34
|
/**
|
|
39
35
|
* 지역 검색 메서드
|
|
40
36
|
*/
|
|
@@ -47,12 +47,6 @@ export class NaverSearchClient extends NaverApiCoreClient {
|
|
|
47
47
|
async search(type, params) {
|
|
48
48
|
return this.get(`${this.searchBaseUrl}/${type}`, params);
|
|
49
49
|
}
|
|
50
|
-
/**
|
|
51
|
-
* 전문자료 검색 메서드
|
|
52
|
-
*/
|
|
53
|
-
async searchAcademic(params) {
|
|
54
|
-
return this.get(`${this.searchBaseUrl}/doc`, params);
|
|
55
|
-
}
|
|
56
50
|
/**
|
|
57
51
|
* 지역 검색 메서드
|
|
58
52
|
*/
|
|
@@ -63,55 +57,55 @@ export class NaverSearchClient extends NaverApiCoreClient {
|
|
|
63
57
|
* 검색어 트렌드 분석 메서드
|
|
64
58
|
*/
|
|
65
59
|
async searchTrend(params) {
|
|
66
|
-
return this.post(
|
|
60
|
+
return this.post(this.trendUrl, params);
|
|
67
61
|
}
|
|
68
62
|
/**
|
|
69
63
|
* 쇼핑 카테고리 트렌드 분석 메서드
|
|
70
64
|
*/
|
|
71
65
|
async datalabShoppingCategory(params) {
|
|
72
|
-
return this.post(`${this.
|
|
66
|
+
return this.post(`${this.shoppingBaseUrl}/categories`, params);
|
|
73
67
|
}
|
|
74
68
|
/**
|
|
75
69
|
* 쇼핑 기기별 트렌드 분석 메서드
|
|
76
70
|
*/
|
|
77
71
|
async datalabShoppingByDevice(params) {
|
|
78
|
-
return this.post(`${this.
|
|
72
|
+
return this.post(`${this.shoppingBaseUrl}/category/device`, params);
|
|
79
73
|
}
|
|
80
74
|
/**
|
|
81
75
|
* 쇼핑 성별 트렌드 분석 메서드
|
|
82
76
|
*/
|
|
83
77
|
async datalabShoppingByGender(params) {
|
|
84
|
-
return this.post(`${this.
|
|
78
|
+
return this.post(`${this.shoppingBaseUrl}/category/gender`, params);
|
|
85
79
|
}
|
|
86
80
|
/**
|
|
87
81
|
* 쇼핑 연령별 트렌드 분석 메서드
|
|
88
82
|
*/
|
|
89
83
|
async datalabShoppingByAge(params) {
|
|
90
|
-
return this.post(`${this.
|
|
84
|
+
return this.post(`${this.shoppingBaseUrl}/category/age`, params);
|
|
91
85
|
}
|
|
92
86
|
/**
|
|
93
87
|
* 쇼핑 키워드 트렌드 분석 메서드
|
|
94
88
|
*/
|
|
95
89
|
async datalabShoppingKeywords(params) {
|
|
96
|
-
return this.post(`${this.
|
|
90
|
+
return this.post(`${this.shoppingBaseUrl}/category/keywords`, params);
|
|
97
91
|
}
|
|
98
92
|
/**
|
|
99
93
|
* 쇼핑 키워드 기기별 트렌드 분석 메서드
|
|
100
94
|
*/
|
|
101
95
|
async datalabShoppingKeywordByDevice(params) {
|
|
102
|
-
return this.post(`${this.
|
|
96
|
+
return this.post(`${this.shoppingBaseUrl}/category/keyword/device`, params);
|
|
103
97
|
}
|
|
104
98
|
/**
|
|
105
99
|
* 쇼핑 키워드 성별 트렌드 분석 메서드
|
|
106
100
|
*/
|
|
107
101
|
async datalabShoppingKeywordByGender(params) {
|
|
108
|
-
return this.post(`${this.
|
|
102
|
+
return this.post(`${this.shoppingBaseUrl}/category/keyword/gender`, params);
|
|
109
103
|
}
|
|
110
104
|
/**
|
|
111
105
|
* 쇼핑 키워드 연령별 트렌드 분석 메서드
|
|
112
106
|
*/
|
|
113
107
|
async datalabShoppingKeywordByAge(params) {
|
|
114
|
-
return this.post(`${this.
|
|
108
|
+
return this.post(`${this.shoppingBaseUrl}/category/keyword/age`, params);
|
|
115
109
|
}
|
|
116
110
|
}
|
|
117
111
|
NaverSearchClient.instance = null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NaverApiProvider } from "../clients/naver-api-endpoints.js";
|
|
2
|
+
export interface CredentialEnv {
|
|
3
|
+
NAVER_CLIENT_ID?: string;
|
|
4
|
+
NAVER_CLIENT_SECRET?: string;
|
|
5
|
+
NCP_APIGW_API_KEY_ID?: string;
|
|
6
|
+
NCP_APIGW_API_KEY?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ResolvedCredentials {
|
|
9
|
+
provider: NaverApiProvider;
|
|
10
|
+
clientId: string;
|
|
11
|
+
clientSecret: string;
|
|
12
|
+
/** 해석은 성공했지만 사용자에게 알릴 것이 있을 때만 채운다. 없으면 키 자체를 넣지 않는다. */
|
|
13
|
+
warning?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 환경변수에서 자격증명을 읽어 사용할 provider를 결정한다.
|
|
17
|
+
* 키 값 자체로는 두 플랫폼을 구분할 수 없으므로, 어느 변수에 들어있는지로 판단한다.
|
|
18
|
+
* HUB 쌍이 legacy 쌍보다 우선한다.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveCredentials(env: CredentialEnv): ResolvedCredentials;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const SETUP_GUIDE = `
|
|
2
|
+
NAVER API HUB(권장): NCP_APIGW_API_KEY_ID, NCP_APIGW_API_KEY
|
|
3
|
+
발급: NAVER Cloud Platform 콘솔 > NAVER API HUB
|
|
4
|
+
네이버 개발자센터(2027-06-30 지원 종료): NAVER_CLIENT_ID, NAVER_CLIENT_SECRET
|
|
5
|
+
|
|
6
|
+
두 자격증명은 서로 호환되지 않습니다. 발급받은 플랫폼에 맞는 환경변수에 넣어주세요.`;
|
|
7
|
+
/**
|
|
8
|
+
* 환경변수에서 자격증명을 읽어 사용할 provider를 결정한다.
|
|
9
|
+
* 키 값 자체로는 두 플랫폼을 구분할 수 없으므로, 어느 변수에 들어있는지로 판단한다.
|
|
10
|
+
* HUB 쌍이 legacy 쌍보다 우선한다.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveCredentials(env) {
|
|
13
|
+
const hubId = env.NCP_APIGW_API_KEY_ID?.trim();
|
|
14
|
+
const hubSecret = env.NCP_APIGW_API_KEY?.trim();
|
|
15
|
+
const legacyId = env.NAVER_CLIENT_ID?.trim();
|
|
16
|
+
const legacySecret = env.NAVER_CLIENT_SECRET?.trim();
|
|
17
|
+
if (hubId && hubSecret) {
|
|
18
|
+
return { provider: "hub", clientId: hubId, clientSecret: hubSecret };
|
|
19
|
+
}
|
|
20
|
+
if (hubId || hubSecret) {
|
|
21
|
+
const missing = hubId ? "NCP_APIGW_API_KEY" : "NCP_APIGW_API_KEY_ID";
|
|
22
|
+
// HUB 키가 반쪽이어도 개발자센터 키가 온전하면 서버를 죽이지 않는다.
|
|
23
|
+
// 이관 중에 HUB 키를 반만 넣어둔 기존 사용자를 막지 않되, 실수는 경고로 드러낸다.
|
|
24
|
+
if (legacyId && legacySecret) {
|
|
25
|
+
return {
|
|
26
|
+
provider: "legacy",
|
|
27
|
+
clientId: legacyId,
|
|
28
|
+
clientSecret: legacySecret,
|
|
29
|
+
warning: `NAVER API HUB 자격증명이 반쪽입니다. ${missing} 이(가) 설정되지 않아 개발자센터 키로 동작합니다.
|
|
30
|
+
HUB로 이관하려면 ${missing} 을(를) 마저 설정하고, 이관할 생각이 없다면 남은 HUB 환경변수를 지우세요.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`NAVER API HUB 자격증명이 불완전합니다.
|
|
34
|
+
NCP_APIGW_API_KEY_ID: ${hubId ? "provided" : "missing"}
|
|
35
|
+
NCP_APIGW_API_KEY: ${hubSecret ? "provided" : "missing"}
|
|
36
|
+
두 값을 함께 설정해야 합니다.`);
|
|
37
|
+
}
|
|
38
|
+
if (legacyId && legacySecret) {
|
|
39
|
+
return {
|
|
40
|
+
provider: "legacy",
|
|
41
|
+
clientId: legacyId,
|
|
42
|
+
clientSecret: legacySecret,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (legacyId || legacySecret) {
|
|
46
|
+
throw new Error(`네이버 개발자센터 자격증명이 불완전합니다.
|
|
47
|
+
NAVER_CLIENT_ID: ${legacyId ? "provided" : "missing"}
|
|
48
|
+
NAVER_CLIENT_SECRET: ${legacySecret ? "provided" : "missing"}
|
|
49
|
+
두 값을 함께 설정해야 합니다.`);
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`자격증명이 설정되지 않았습니다.
|
|
52
|
+
${SETUP_GUIDE}`);
|
|
53
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Import JSON data - bundle-safe approach
|
|
2
2
|
import { existsSync, readFileSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
4
5
|
// 메모리 누수 방지를 위한 지연 로딩 캐시
|
|
5
6
|
let categoriesCache = null;
|
|
7
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
6
8
|
// Load categories data with fallback paths for different environments
|
|
7
9
|
function getCategoriesData() {
|
|
8
10
|
// 캐시된 데이터가 있으면 반환 (메모리 효율성)
|
|
@@ -10,19 +12,21 @@ function getCategoriesData() {
|
|
|
10
12
|
return categoriesCache;
|
|
11
13
|
}
|
|
12
14
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
const candidatePaths = [
|
|
16
|
+
// Package-relative path. Works when launched by npx from any cwd.
|
|
17
|
+
join(moduleDir, '..', '..', 'data', 'categories.json'),
|
|
18
|
+
// Local development fallbacks.
|
|
19
|
+
join(process.cwd(), 'dist', 'data', 'categories.json'),
|
|
20
|
+
join(process.cwd(), 'data', 'categories.json'),
|
|
21
|
+
];
|
|
22
|
+
for (const categoryPath of candidatePaths) {
|
|
23
|
+
if (existsSync(categoryPath)) {
|
|
24
|
+
const categoryJson = readFileSync(categoryPath, 'utf8').toString();
|
|
25
|
+
categoriesCache = JSON.parse(categoryJson);
|
|
26
|
+
return categoriesCache;
|
|
27
|
+
}
|
|
24
28
|
}
|
|
25
|
-
throw new Error(
|
|
29
|
+
throw new Error(`카테고리 데이터 파일을 찾을 수 없습니다. Tried: ${candidatePaths.join(', ')}`);
|
|
26
30
|
}
|
|
27
31
|
catch (error) {
|
|
28
32
|
console.error('카테고리 데이터 로딩 실패:', error);
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import { NaverLocalSearchParams } from "../schemas/search.schemas.js";
|
|
2
2
|
import { SearchArgs } from "../schemas/search.schemas.js";
|
|
3
3
|
export declare const searchToolHandlers: Record<string, (args: any) => Promise<any>>;
|
|
4
|
-
/**
|
|
5
|
-
* 전문자료 검색 핸들러
|
|
6
|
-
*/
|
|
7
|
-
export declare function handleAcademicSearch(params: SearchArgs): Promise<import("../types/search.types.js").NaverDocumentSearchResponse>;
|
|
8
|
-
/**
|
|
9
|
-
* 도서 검색 핸들러
|
|
10
|
-
*/
|
|
11
|
-
export declare function handleBookSearch(params: SearchArgs): Promise<any>;
|
|
12
4
|
/**
|
|
13
5
|
* 지식백과 검색 핸들러
|
|
14
6
|
*/
|
|
@@ -33,10 +25,6 @@ export declare function handleNewsSearch(params: SearchArgs): Promise<any>;
|
|
|
33
25
|
* 블로그 검색 핸들러
|
|
34
26
|
*/
|
|
35
27
|
export declare function handleBlogSearch(params: SearchArgs): Promise<any>;
|
|
36
|
-
/**
|
|
37
|
-
* 쇼핑 검색 핸들러
|
|
38
|
-
*/
|
|
39
|
-
export declare function handleShopSearch(params: SearchArgs): Promise<any>;
|
|
40
28
|
/**
|
|
41
29
|
* 카페글 검색 핸들러
|
|
42
30
|
*/
|
|
@@ -15,10 +15,6 @@ export const searchToolHandlers = {
|
|
|
15
15
|
console.error("search_blog called with args:", JSON.stringify(args, null, 2));
|
|
16
16
|
return handleBlogSearch(SearchArgsSchema.parse(args));
|
|
17
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
18
|
search_image: (args) => {
|
|
23
19
|
console.error("search_image called with args:", JSON.stringify(args, null, 2));
|
|
24
20
|
return handleImageSearch(SearchArgsSchema.parse(args));
|
|
@@ -27,18 +23,10 @@ export const searchToolHandlers = {
|
|
|
27
23
|
console.error("search_kin called with args:", JSON.stringify(args, null, 2));
|
|
28
24
|
return handleKinSearch(SearchArgsSchema.parse(args));
|
|
29
25
|
},
|
|
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
26
|
search_encyc: (args) => {
|
|
35
27
|
console.error("search_encyc called with args:", JSON.stringify(args, null, 2));
|
|
36
28
|
return handleEncycSearch(SearchArgsSchema.parse(args));
|
|
37
29
|
},
|
|
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
30
|
search_local: (args) => {
|
|
43
31
|
console.error("search_local called with args:", JSON.stringify(args, null, 2));
|
|
44
32
|
return handleLocalSearch(args);
|
|
@@ -48,18 +36,6 @@ export const searchToolHandlers = {
|
|
|
48
36
|
return handleCafeArticleSearch(SearchArgsSchema.parse(args));
|
|
49
37
|
},
|
|
50
38
|
};
|
|
51
|
-
/**
|
|
52
|
-
* 전문자료 검색 핸들러
|
|
53
|
-
*/
|
|
54
|
-
export async function handleAcademicSearch(params) {
|
|
55
|
-
return client.searchAcademic(params);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* 도서 검색 핸들러
|
|
59
|
-
*/
|
|
60
|
-
export async function handleBookSearch(params) {
|
|
61
|
-
return client.search("book", params);
|
|
62
|
-
}
|
|
63
39
|
/**
|
|
64
40
|
* 지식백과 검색 핸들러
|
|
65
41
|
*/
|
|
@@ -96,12 +72,6 @@ export async function handleNewsSearch(params) {
|
|
|
96
72
|
export async function handleBlogSearch(params) {
|
|
97
73
|
return client.search("blog", params);
|
|
98
74
|
}
|
|
99
|
-
/**
|
|
100
|
-
* 쇼핑 검색 핸들러
|
|
101
|
-
*/
|
|
102
|
-
export async function handleShopSearch(params) {
|
|
103
|
-
return client.search("shop", params);
|
|
104
|
-
}
|
|
105
75
|
/**
|
|
106
76
|
* 카페글 검색 핸들러
|
|
107
77
|
*/
|
package/dist/src/index.d.ts
CHANGED
|
@@ -2,14 +2,20 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
export declare const configSchema: z.ZodObject<{
|
|
5
|
-
NAVER_CLIENT_ID: z.ZodString
|
|
6
|
-
NAVER_CLIENT_SECRET: z.ZodString
|
|
5
|
+
NAVER_CLIENT_ID: z.ZodOptional<z.ZodString>;
|
|
6
|
+
NAVER_CLIENT_SECRET: z.ZodOptional<z.ZodString>;
|
|
7
|
+
NCP_APIGW_API_KEY_ID: z.ZodOptional<z.ZodString>;
|
|
8
|
+
NCP_APIGW_API_KEY: z.ZodOptional<z.ZodString>;
|
|
7
9
|
}, "strip", z.ZodTypeAny, {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
NCP_APIGW_API_KEY?: string | undefined;
|
|
11
|
+
NCP_APIGW_API_KEY_ID?: string | undefined;
|
|
12
|
+
NAVER_CLIENT_ID?: string | undefined;
|
|
13
|
+
NAVER_CLIENT_SECRET?: string | undefined;
|
|
10
14
|
}, {
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
NCP_APIGW_API_KEY?: string | undefined;
|
|
16
|
+
NCP_APIGW_API_KEY_ID?: string | undefined;
|
|
17
|
+
NAVER_CLIENT_ID?: string | undefined;
|
|
18
|
+
NAVER_CLIENT_SECRET?: string | undefined;
|
|
13
19
|
}>;
|
|
14
20
|
/**
|
|
15
21
|
* 서버 인스턴스와 관련 리소스 정리 (메모리 누수 방지)
|
package/dist/src/index.js
CHANGED
|
@@ -9,10 +9,22 @@ import { SearchArgsSchema, NaverLocalSearchParamsSchema, } from "./schemas/searc
|
|
|
9
9
|
import { DatalabSearchSchema, DatalabShoppingSchema, DatalabShoppingDeviceSchema, DatalabShoppingGenderSchema, DatalabShoppingAgeSchema, DatalabShoppingKeywordsSchema, DatalabShoppingKeywordDeviceSchema, DatalabShoppingKeywordGenderSchema, DatalabShoppingKeywordAgeSchema, } from "./schemas/datalab.schemas.js";
|
|
10
10
|
import { FindCategorySchema } from "./schemas/category.schemas.js";
|
|
11
11
|
import { findCategoryHandler, clearCategoriesCache } from "./handlers/category.handlers.js";
|
|
12
|
-
|
|
12
|
+
import { resolveCredentials } from "./config/credentials.js";
|
|
13
|
+
// Configuration schema for programmatic server creation and stdio startup
|
|
13
14
|
export const configSchema = z.object({
|
|
14
|
-
NAVER_CLIENT_ID: z.string().describe("
|
|
15
|
-
NAVER_CLIENT_SECRET: z
|
|
15
|
+
NAVER_CLIENT_ID: z.string().optional().describe("네이버 개발자센터 Client ID"),
|
|
16
|
+
NAVER_CLIENT_SECRET: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("네이버 개발자센터 Client Secret"),
|
|
20
|
+
NCP_APIGW_API_KEY_ID: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("NAVER API HUB Client ID"),
|
|
24
|
+
NCP_APIGW_API_KEY: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("NAVER API HUB Client Secret"),
|
|
16
28
|
});
|
|
17
29
|
// Global server instance to prevent memory leaks
|
|
18
30
|
let globalServerInstance = null;
|
|
@@ -38,7 +50,9 @@ function isConfigChanged(newConfig) {
|
|
|
38
50
|
if (!currentConfig)
|
|
39
51
|
return true;
|
|
40
52
|
return (currentConfig.NAVER_CLIENT_ID !== newConfig.NAVER_CLIENT_ID ||
|
|
41
|
-
currentConfig.NAVER_CLIENT_SECRET !== newConfig.NAVER_CLIENT_SECRET
|
|
53
|
+
currentConfig.NAVER_CLIENT_SECRET !== newConfig.NAVER_CLIENT_SECRET ||
|
|
54
|
+
currentConfig.NCP_APIGW_API_KEY_ID !== newConfig.NCP_APIGW_API_KEY_ID ||
|
|
55
|
+
currentConfig.NCP_APIGW_API_KEY !== newConfig.NCP_APIGW_API_KEY);
|
|
42
56
|
}
|
|
43
57
|
export function createNaverSearchServer({ config, }) {
|
|
44
58
|
// 설정이 변경된 경우 기존 인스턴스 정리
|
|
@@ -53,14 +67,18 @@ export function createNaverSearchServer({ config, }) {
|
|
|
53
67
|
// Create a new MCP server only once
|
|
54
68
|
const server = new McpServer({
|
|
55
69
|
name: "naver-search",
|
|
56
|
-
version: "1.0.
|
|
70
|
+
version: "1.0.49",
|
|
57
71
|
});
|
|
58
72
|
// Initialize Naver client with config
|
|
73
|
+
const credentials = resolveCredentials(config);
|
|
74
|
+
console.error(`Using ${credentials.provider === "hub"
|
|
75
|
+
? "NAVER API HUB"
|
|
76
|
+
: "네이버 개발자센터 (2027-06-30 지원 종료 예정)"}`);
|
|
77
|
+
if (credentials.warning) {
|
|
78
|
+
console.error(`[경고] ${credentials.warning}`);
|
|
79
|
+
}
|
|
59
80
|
const client = NaverSearchClient.getInstance();
|
|
60
|
-
client.initialize(
|
|
61
|
-
clientId: config.NAVER_CLIENT_ID,
|
|
62
|
-
clientSecret: config.NAVER_CLIENT_SECRET,
|
|
63
|
-
});
|
|
81
|
+
client.initialize(credentials);
|
|
64
82
|
server.registerTool("search_webkr", {
|
|
65
83
|
description: "🌐 Search Korean web documents and general content. Comprehensive search across Korean websites and online content. Find articles, information, and documents from various Korean sources. For recent content or 'today's results', use get_current_korean_time first. (네이버 웹문서 검색 - 한국 웹사이트 종합 검색, 최근 콘텐츠나 오늘 결과를 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)",
|
|
66
84
|
inputSchema: SearchArgsSchema.shape,
|
|
@@ -88,15 +106,6 @@ export function createNaverSearchServer({ config, }) {
|
|
|
88
106
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
89
107
|
};
|
|
90
108
|
});
|
|
91
|
-
server.registerTool("search_shop", {
|
|
92
|
-
description: "🛒 Search Naver Shopping for products, prices, and shopping deals. Compare prices across vendors, find product specifications, and discover shopping trends in Korea. For current deals or today's specials, use get_current_korean_time first. (네이버 쇼핑 검색 - 상품 정보와 가격 비교, 현재 할인이나 오늘 특가를 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)",
|
|
93
|
-
inputSchema: SearchArgsSchema.shape,
|
|
94
|
-
}, async (args) => {
|
|
95
|
-
const result = await searchToolHandlers.search_shop(args);
|
|
96
|
-
return {
|
|
97
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
98
|
-
};
|
|
99
|
-
});
|
|
100
109
|
server.registerTool("search_image", {
|
|
101
110
|
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. For recent images or current visual content, use get_current_korean_time first. (네이버 이미지 검색 - 시각적 컨텐츠 발견, 최근 이미지나 현재 시각 자료를 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)",
|
|
102
111
|
inputSchema: SearchArgsSchema.shape,
|
|
@@ -115,15 +124,6 @@ export function createNaverSearchServer({ config, }) {
|
|
|
115
124
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
116
125
|
};
|
|
117
126
|
});
|
|
118
|
-
server.registerTool("search_book", {
|
|
119
|
-
description: "📚 Search for books, publications, and literary content. Find book reviews, author information, publication details, and reading recommendations in Korean literature and translated works. For new releases or current bestsellers, use get_current_korean_time first. (네이버 책 검색 - 도서 정보와 서평, 신간도서나 현재 베스트셀러를 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)",
|
|
120
|
-
inputSchema: SearchArgsSchema.shape,
|
|
121
|
-
}, async (args) => {
|
|
122
|
-
const result = await searchToolHandlers.search_book(args);
|
|
123
|
-
return {
|
|
124
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
125
|
-
};
|
|
126
|
-
});
|
|
127
127
|
server.registerTool("search_encyc", {
|
|
128
128
|
description: "📖 Search Naver Encyclopedia for authoritative knowledge and definitions. Best for academic research, getting reliable information, and understanding Korean concepts and terminology. For current definitions or recent updates, use get_current_korean_time for context. (네이버 지식백과 검색 - 신뢰할 수 있는 정보와 정의, 현재 정의나 최근 업데이트를 찾을 때는 get_current_korean_time으로 상황을 파악하세요)",
|
|
129
129
|
inputSchema: SearchArgsSchema.shape,
|
|
@@ -133,15 +133,6 @@ export function createNaverSearchServer({ config, }) {
|
|
|
133
133
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
134
134
|
};
|
|
135
135
|
});
|
|
136
|
-
server.registerTool("search_academic", {
|
|
137
|
-
description: "🎓 Search academic papers, research documents, and scholarly content. Access Korean academic resources, research papers, theses, and professional publications. For recent publications or current research, use get_current_korean_time first. (네이버 전문자료 검색 - 학술 논문과 전문 자료, 최근 발표나 현재 연구를 찾을 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)",
|
|
138
|
-
inputSchema: SearchArgsSchema.shape,
|
|
139
|
-
}, async (args) => {
|
|
140
|
-
const result = await searchToolHandlers.search_academic(args);
|
|
141
|
-
return {
|
|
142
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
143
|
-
};
|
|
144
|
-
});
|
|
145
136
|
server.registerTool("search_local", {
|
|
146
137
|
description: "📍 Search for local businesses, restaurants, and places in Korea. Find location information, reviews, contact details, and business hours for Korean establishments. For current business hours or today's availability, use get_current_korean_time first. (네이버 지역 검색 - 지역 업체와 장소 정보, 현재 영업시간이나 오늘 이용 가능 여부를 확인할 때는 먼저 get_current_korean_time으로 현재 시간을 확인하세요)",
|
|
147
138
|
inputSchema: NaverLocalSearchParamsSchema.shape,
|
|
@@ -275,7 +266,7 @@ export function createNaverSearchServer({ config, }) {
|
|
|
275
266
|
currentConfig = config;
|
|
276
267
|
return server.server;
|
|
277
268
|
}
|
|
278
|
-
// Export
|
|
269
|
+
// Export the server factory for programmatic consumers
|
|
279
270
|
export default createNaverSearchServer;
|
|
280
271
|
function registerShutdownHandlers({ server, transport, }) {
|
|
281
272
|
let shuttingDown = false;
|
|
@@ -314,29 +305,29 @@ function registerShutdownHandlers({ server, transport, }) {
|
|
|
314
305
|
async function main() {
|
|
315
306
|
try {
|
|
316
307
|
console.error("Starting Naver Search MCP Server...");
|
|
317
|
-
// Get config from environment variables
|
|
318
|
-
const
|
|
319
|
-
|
|
308
|
+
// Get config from environment variables
|
|
309
|
+
const config = {
|
|
310
|
+
NAVER_CLIENT_ID: process.env.NAVER_CLIENT_ID,
|
|
311
|
+
NAVER_CLIENT_SECRET: process.env.NAVER_CLIENT_SECRET,
|
|
312
|
+
NCP_APIGW_API_KEY_ID: process.env.NCP_APIGW_API_KEY_ID,
|
|
313
|
+
NCP_APIGW_API_KEY: process.env.NCP_APIGW_API_KEY,
|
|
314
|
+
};
|
|
320
315
|
console.error("Environment variables:", {
|
|
321
|
-
NAVER_CLIENT_ID:
|
|
322
|
-
? `[${
|
|
316
|
+
NAVER_CLIENT_ID: config.NAVER_CLIENT_ID
|
|
317
|
+
? `[${config.NAVER_CLIENT_ID.length} chars]`
|
|
318
|
+
: "undefined",
|
|
319
|
+
NAVER_CLIENT_SECRET: config.NAVER_CLIENT_SECRET
|
|
320
|
+
? `[${config.NAVER_CLIENT_SECRET.length} chars]`
|
|
321
|
+
: "undefined",
|
|
322
|
+
NCP_APIGW_API_KEY_ID: config.NCP_APIGW_API_KEY_ID
|
|
323
|
+
? `[${config.NCP_APIGW_API_KEY_ID.length} chars]`
|
|
323
324
|
: "undefined",
|
|
324
|
-
|
|
325
|
-
? `[${
|
|
325
|
+
NCP_APIGW_API_KEY: config.NCP_APIGW_API_KEY
|
|
326
|
+
? `[${config.NCP_APIGW_API_KEY.length} chars]`
|
|
326
327
|
: "undefined",
|
|
327
328
|
});
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
NAVER_CLIENT_ID: ${clientId ? "provided" : "missing"}
|
|
331
|
-
NAVER_CLIENT_SECRET: ${clientSecret ? "provided" : "missing"}
|
|
332
|
-
|
|
333
|
-
Please set these environment variables before running the server.`);
|
|
334
|
-
}
|
|
335
|
-
const config = {
|
|
336
|
-
NAVER_CLIENT_ID: clientId,
|
|
337
|
-
NAVER_CLIENT_SECRET: clientSecret,
|
|
338
|
-
};
|
|
339
|
-
console.error("Config loaded successfully");
|
|
329
|
+
// 자격증명 유효성은 여기서 먼저 확인한다 (실패 시 명확한 안내와 함께 종료)
|
|
330
|
+
resolveCredentials(config);
|
|
340
331
|
// Validate config
|
|
341
332
|
const validatedConfig = configSchema.parse(config);
|
|
342
333
|
console.error("Config validated successfully");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export declare const NaverSearchTypeSchema: z.ZodEnum<["news", "encyc", "blog", "
|
|
2
|
+
export declare const NaverSearchTypeSchema: z.ZodEnum<["news", "encyc", "blog", "webkr", "image", "kin", "cafearticle", "local"]>;
|
|
3
3
|
export declare const NaverSearchParamsSchema: z.ZodObject<{
|
|
4
4
|
query: z.ZodString;
|
|
5
5
|
display: z.ZodOptional<z.ZodNumber>;
|
|
@@ -33,28 +33,18 @@ export declare const SearchArgsSchema: z.ZodObject<{
|
|
|
33
33
|
sort?: "sim" | "date" | undefined;
|
|
34
34
|
}>;
|
|
35
35
|
export declare const NaverSearchConfigSchema: z.ZodObject<{
|
|
36
|
+
provider: z.ZodEnum<["legacy", "hub"]>;
|
|
36
37
|
clientId: z.ZodString;
|
|
37
38
|
clientSecret: z.ZodString;
|
|
38
39
|
}, "strip", z.ZodTypeAny, {
|
|
40
|
+
provider: "legacy" | "hub";
|
|
39
41
|
clientId: string;
|
|
40
42
|
clientSecret: string;
|
|
41
43
|
}, {
|
|
44
|
+
provider: "legacy" | "hub";
|
|
42
45
|
clientId: string;
|
|
43
46
|
clientSecret: string;
|
|
44
47
|
}>;
|
|
45
|
-
export declare const NaverDocumentSearchParamsSchema: z.ZodObject<{
|
|
46
|
-
query: z.ZodString;
|
|
47
|
-
display: z.ZodOptional<z.ZodNumber>;
|
|
48
|
-
start: z.ZodOptional<z.ZodNumber>;
|
|
49
|
-
}, "strip", z.ZodTypeAny, {
|
|
50
|
-
query: string;
|
|
51
|
-
display?: number | undefined;
|
|
52
|
-
start?: number | undefined;
|
|
53
|
-
}, {
|
|
54
|
-
query: string;
|
|
55
|
-
display?: number | undefined;
|
|
56
|
-
start?: number | undefined;
|
|
57
|
-
}>;
|
|
58
48
|
export declare const NaverLocalSearchParamsSchema: z.ZodObject<{
|
|
59
49
|
query: z.ZodString;
|
|
60
50
|
} & {
|
|
@@ -77,4 +67,3 @@ export type NaverSearchParams = z.infer<typeof NaverSearchParamsSchema>;
|
|
|
77
67
|
export type SearchArgs = z.infer<typeof SearchArgsSchema>;
|
|
78
68
|
export type NaverSearchType = z.infer<typeof NaverSearchTypeSchema>;
|
|
79
69
|
export type NaverSearchConfig = z.infer<typeof NaverSearchConfigSchema>;
|
|
80
|
-
export type NaverDocumentSearchParams = z.infer<typeof NaverDocumentSearchParamsSchema>;
|
|
@@ -4,12 +4,9 @@ export const NaverSearchTypeSchema = z.enum([
|
|
|
4
4
|
"news",
|
|
5
5
|
"encyc",
|
|
6
6
|
"blog",
|
|
7
|
-
"shop",
|
|
8
7
|
"webkr",
|
|
9
8
|
"image",
|
|
10
|
-
"doc",
|
|
11
9
|
"kin",
|
|
12
|
-
"book",
|
|
13
10
|
"cafearticle",
|
|
14
11
|
"local",
|
|
15
12
|
]);
|
|
@@ -40,16 +37,11 @@ export const SearchArgsSchema = z.object({
|
|
|
40
37
|
});
|
|
41
38
|
// 네이버 API 인증 정보
|
|
42
39
|
export const NaverSearchConfigSchema = z.object({
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// 전문자료(논문 등) 검색 파라미터
|
|
49
|
-
export const NaverDocumentSearchParamsSchema = z.object({
|
|
50
|
-
query: z.string().describe("검색어"),
|
|
51
|
-
display: z.number().optional().describe("한 번에 가져올 결과 수 (최대 100)"),
|
|
52
|
-
start: z.number().optional().describe("검색 시작 위치 (최대 1000)"),
|
|
40
|
+
provider: z
|
|
41
|
+
.enum(["legacy", "hub"])
|
|
42
|
+
.describe("자격증명 발급 플랫폼 (legacy: 개발자센터, hub: NAVER API HUB)"),
|
|
43
|
+
clientId: z.string().describe("발급받은 Client ID"),
|
|
44
|
+
clientSecret: z.string().describe("발급받은 Client Secret"),
|
|
53
45
|
});
|
|
54
46
|
// 지역 검색 파라미터
|
|
55
47
|
export const NaverLocalSearchParamsSchema = SearchArgsSchema.extend({
|
|
@@ -11,14 +11,6 @@ export interface NaverSearchItem {
|
|
|
11
11
|
link: string;
|
|
12
12
|
description: string;
|
|
13
13
|
}
|
|
14
|
-
export interface NaverDocumentSearchResponse extends NaverSearchResponse {
|
|
15
|
-
items: NaverDocumentItem[];
|
|
16
|
-
}
|
|
17
|
-
export interface NaverDocumentItem extends NaverSearchItem {
|
|
18
|
-
title: string;
|
|
19
|
-
link: string;
|
|
20
|
-
description: string;
|
|
21
|
-
}
|
|
22
14
|
export interface NaverEncyclopediaSearchResponse extends NaverSearchResponse {
|
|
23
15
|
items: NaverEncyclopediaItem[];
|
|
24
16
|
}
|
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.49",
|
|
4
4
|
"description": "Naver Search MCP Server",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
"build": "tsc && shx cp -r data dist/",
|
|
14
14
|
"prepare": "npm run build",
|
|
15
15
|
"watch": "tsc --watch",
|
|
16
|
-
"start": "node dist/src/index.js"
|
|
16
|
+
"start": "node dist/src/index.js",
|
|
17
|
+
"test": "npm run build && node --test"
|
|
17
18
|
},
|
|
18
19
|
"engines": {
|
|
19
20
|
"node": ">=18"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"@modelcontextprotocol/sdk": "1.17.1",
|
|
23
|
-
"@smithery/sdk": "^1.6.6",
|
|
24
24
|
"axios": "^1.6.7",
|
|
25
25
|
"zod": "^3.23.0",
|
|
26
26
|
"zod-to-json-schema": "^3.23.0",
|
package/dist/data/README.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
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. 파일 수정시 자동으로 캐시 갱신됨
|