@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 CHANGED
@@ -3,11 +3,39 @@
3
3
  [![한국어](https://img.shields.io/badge/한국어-README-yellow)](README-ko.md)
4
4
 
5
5
  [![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/isnow890/naver-search-mcp)](https://archestra.ai/mcp-catalog/isnow890__naver-search-mcp)
6
- [![smithery badge](https://smithery.ai/badge/@isnow890/naver-search-mcp)](https://smithery.ai/server/@isnow890/naver-search-mcp)
7
6
  [![MCP.so](https://img.shields.io/badge/MCP.so-Naver%20Search%20MCP-blue)](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, book, cafe article, web, image, kin, encyclopedia, academic, and local search
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 credentials in the configuration below
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: Smithery Installation
187
+ ### Method 2: ClawHub Installation for OpenClaw
109
188
 
110
- Install via Smithery CLI:
189
+ This MCP server can also be used from OpenClaw through the ClawHub skill wrapper. Install it with:
111
190
 
112
191
  ```bash
113
- npx -y @smithery/cli@latest install @isnow890/naver-search-mcp --client claude
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
- - Naver Developers API Key (Client ID and Secret)
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 Smithery compatibility issues so you can use the latest features through Smithery
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
- - Fully compatible with Smithery platform installation
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 Smithery specification changes
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
- getHeaders(contentType = "application/json") {
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
- "X-Naver-Client-Id": this.config.clientId,
23
- "X-Naver-Client-Secret": this.config.clientSecret,
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
- // 연결 정리는 axios가 자동으로 처리하지만 명시적으로 에러 처리
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
- // 연결 정리는 axios가 자동으로 처리하지만 명시적으로 에러 처리
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,5 @@
1
+ import { NaverApiProvider } from "./naver-api-endpoints.js";
2
+ /**
3
+ * 두 플랫폼의 오류 응답 형식이 다르므로 파싱하지 않고 그대로 실어 보낸다.
4
+ */
5
+ export declare function formatApiError(provider: NaverApiProvider, url: string, status: number | undefined, body: unknown): string;
@@ -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, NaverDocumentSearchParams, SearchArgs } from "../schemas/search.schemas.js";
3
- import { NaverDocumentSearchResponse, NaverLocalSearchResponse } from "../types/search.types.js";
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(`${this.datalabBaseUrl}/search`, params);
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.datalabBaseUrl}/shopping/categories`, params);
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.datalabBaseUrl}/shopping/category/device`, params);
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.datalabBaseUrl}/shopping/category/gender`, params);
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.datalabBaseUrl}/shopping/category/age`, params);
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.datalabBaseUrl}/shopping/category/keywords`, params);
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.datalabBaseUrl}/shopping/category/keyword/device`, params);
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.datalabBaseUrl}/shopping/category/keyword/gender`, params);
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.datalabBaseUrl}/shopping/category/keyword/age`, params);
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
- // Try bundled data path first (dist/data)
14
- const bundledPath = join(process.cwd(), 'dist', 'data', 'categories.json');
15
- if (existsSync(bundledPath)) {
16
- categoriesCache = JSON.parse(readFileSync(bundledPath, 'utf8'));
17
- return categoriesCache;
18
- }
19
- // Fallback to source data path
20
- const sourcePath = join(process.cwd(), 'data', 'categories.json');
21
- if (existsSync(sourcePath)) {
22
- categoriesCache = JSON.parse(readFileSync(sourcePath, 'utf8'));
23
- return categoriesCache;
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
  */
@@ -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
- NAVER_CLIENT_ID: string;
9
- NAVER_CLIENT_SECRET: string;
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
- NAVER_CLIENT_ID: string;
12
- NAVER_CLIENT_SECRET: string;
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
- // Configuration schema for Smithery
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("Naver API Client ID"),
15
- NAVER_CLIENT_SECRET: z.string().describe("Naver API Client Secret"),
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.44",
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 default for Smithery compatibility
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 - check for empty strings too
318
- const clientId = process.env.NAVER_CLIENT_ID?.trim();
319
- const clientSecret = process.env.NAVER_CLIENT_SECRET?.trim();
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: process.env.NAVER_CLIENT_ID
322
- ? `[${process.env.NAVER_CLIENT_ID.length} chars]`
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
- NAVER_CLIENT_SECRET: process.env.NAVER_CLIENT_SECRET
325
- ? `[${process.env.NAVER_CLIENT_SECRET.length} chars]`
325
+ NCP_APIGW_API_KEY: config.NCP_APIGW_API_KEY
326
+ ? `[${config.NCP_APIGW_API_KEY.length} chars]`
326
327
  : "undefined",
327
328
  });
328
- if (!clientId || !clientSecret) {
329
- throw new Error(`Missing required environment variables:
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", "shop", "webkr", "image", "doc", "kin", "book", "cafearticle", "local"]>;
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
- clientId: z.string().describe("네이버 개발자센터에서 발급받은 Client ID"),
44
- clientSecret: z
45
- .string()
46
- .describe("네이버 개발자센터에서 발급받은 Client Secret"),
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.47",
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",
@@ -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. 파일 수정시 자동으로 캐시 갱신됨