@isnow890/naver-search-mcp 1.0.53 → 1.0.55

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.
@@ -6,6 +6,11 @@ const SWAPPED_KEY_HINT = {
6
6
  legacy: "인증에 실패했습니다. NAVER API HUB 키를 NAVER_CLIENT_ID/NAVER_CLIENT_SECRET에 넣지 않았는지 확인하세요. HUB 키는 NCP_APIGW_API_KEY_ID/NCP_APIGW_API_KEY에 넣어야 합니다.",
7
7
  hub: "인증에 실패했습니다. 개발자센터 키를 NCP_APIGW_API_KEY_ID/NCP_APIGW_API_KEY에 넣지 않았는지 확인하세요. 개발자센터 키는 NAVER_CLIENT_ID/NAVER_CLIENT_SECRET에 넣어야 합니다.",
8
8
  };
9
+ // HUB는 키가 맞아도 Application에 해당 API가 켜져 있지 않으면 401을 준다.
10
+ // 이때는 키를 바꿔 넣으라는 힌트가 오히려 사용자를 엉뚱한 곳으로 보낸다.
11
+ // 키 자체가 틀리면 본문이 "Authentication Failed"로 온다 (2026-10-21 실측).
12
+ const HUB_API_NOT_ENABLED = /요청한 API는 이 Application에서 활성화되어 있지 않습니다/;
13
+ const HUB_API_NOT_ENABLED_HINT = "키는 정상이지만 이 API가 NAVER API HUB Application에서 활성화되어 있지 않습니다. NAVER Cloud Platform 콘솔 > NAVER API HUB에서 해당 Application에 이 API를 활성화하세요.";
9
14
  function stringifyBody(body) {
10
15
  if (body === undefined || body === null)
11
16
  return "";
@@ -27,7 +32,9 @@ export function formatApiError(provider, url, status, body) {
27
32
  const bodyText = stringifyBody(body);
28
33
  if (bodyText)
29
34
  lines.push(bodyText);
30
- if (status === 401)
31
- lines.push(SWAPPED_KEY_HINT[provider]);
35
+ if (status === 401) {
36
+ const notEnabled = provider === "hub" && HUB_API_NOT_ENABLED.test(bodyText);
37
+ lines.push(notEnabled ? HUB_API_NOT_ENABLED_HINT : SWAPPED_KEY_HINT[provider]);
38
+ }
32
39
  return lines.join("\n");
33
40
  }
@@ -1,7 +1,8 @@
1
1
  import { NaverSearchClient } from "../clients/naver-search.client.js";
2
2
  import { resolveDateRange } from "../utils/date.utils.js";
3
- // 클라이언트 인스턴스 (싱글톤)
4
- const client = NaverSearchClient.getInstance();
3
+ // 클라이언트는 호출 시점에 꺼낸다. 모듈 로드 시점에 잡아두면 설정 변경으로
4
+ // resetServerInstance()가 싱글톤을 교체한 뒤에도 비워진 옛 인스턴스를 계속 쓴다.
5
+ const client = () => NaverSearchClient.getInstance();
5
6
  /**
6
7
  * 데이터랩 도구 핸들러 맵
7
8
  * 각 도구 이름을 키로, 실행할 핸들러 함수를 값으로 가짐
@@ -52,7 +53,7 @@ export const datalabToolHandlers = {
52
53
  */
53
54
  export async function handleSearchTrend(params) {
54
55
  const { startDate, endDate } = resolveDateRange(params.startDate, params.endDate);
55
- return client.searchTrend({ ...params, startDate, endDate });
56
+ return client().searchTrend({ ...params, startDate, endDate });
56
57
  }
57
58
  /**
58
59
  * 쇼핑 카테고리 트렌드 핸들러
@@ -61,7 +62,7 @@ export async function handleSearchTrend(params) {
61
62
  */
62
63
  export async function handleShoppingCategoryTrend(params) {
63
64
  const { startDate, endDate } = resolveDateRange(params.startDate, params.endDate);
64
- return client.datalabShoppingCategory({ ...params, startDate, endDate });
65
+ return client().datalabShoppingCategory({ ...params, startDate, endDate });
65
66
  }
66
67
  /**
67
68
  * 쇼핑 기기별 트렌드 핸들러
@@ -70,7 +71,7 @@ export async function handleShoppingCategoryTrend(params) {
70
71
  */
71
72
  export async function handleShoppingByDeviceTrend(params) {
72
73
  const { startDate, endDate } = resolveDateRange(params.startDate, params.endDate);
73
- return client.datalabShoppingByDevice({
74
+ return client().datalabShoppingByDevice({
74
75
  startDate,
75
76
  endDate,
76
77
  timeUnit: params.timeUnit,
@@ -85,7 +86,7 @@ export async function handleShoppingByDeviceTrend(params) {
85
86
  */
86
87
  export async function handleShoppingByGenderTrend(params) {
87
88
  const { startDate, endDate } = resolveDateRange(params.startDate, params.endDate);
88
- return client.datalabShoppingByGender({
89
+ return client().datalabShoppingByGender({
89
90
  startDate,
90
91
  endDate,
91
92
  timeUnit: params.timeUnit,
@@ -100,7 +101,7 @@ export async function handleShoppingByGenderTrend(params) {
100
101
  */
101
102
  export async function handleShoppingByAgeTrend(params) {
102
103
  const { startDate, endDate } = resolveDateRange(params.startDate, params.endDate);
103
- return client.datalabShoppingByAge({
104
+ return client().datalabShoppingByAge({
104
105
  startDate,
105
106
  endDate,
106
107
  timeUnit: params.timeUnit,
@@ -115,7 +116,7 @@ export async function handleShoppingByAgeTrend(params) {
115
116
  */
116
117
  export async function handleShoppingKeywordsTrend(params) {
117
118
  const { startDate, endDate } = resolveDateRange(params.startDate, params.endDate);
118
- return client.datalabShoppingKeywords({
119
+ return client().datalabShoppingKeywords({
119
120
  startDate,
120
121
  endDate,
121
122
  timeUnit: params.timeUnit,
@@ -130,7 +131,7 @@ export async function handleShoppingKeywordsTrend(params) {
130
131
  */
131
132
  export async function handleShoppingKeywordByDeviceTrend(params) {
132
133
  const { startDate, endDate } = resolveDateRange(params.startDate, params.endDate);
133
- return client.datalabShoppingKeywordByDevice({
134
+ return client().datalabShoppingKeywordByDevice({
134
135
  startDate,
135
136
  endDate,
136
137
  timeUnit: params.timeUnit,
@@ -146,7 +147,7 @@ export async function handleShoppingKeywordByDeviceTrend(params) {
146
147
  */
147
148
  export async function handleShoppingKeywordByGenderTrend(params) {
148
149
  const { startDate, endDate } = resolveDateRange(params.startDate, params.endDate);
149
- return client.datalabShoppingKeywordByGender({
150
+ return client().datalabShoppingKeywordByGender({
150
151
  startDate,
151
152
  endDate,
152
153
  timeUnit: params.timeUnit,
@@ -162,7 +163,7 @@ export async function handleShoppingKeywordByGenderTrend(params) {
162
163
  */
163
164
  export async function handleShoppingKeywordByAgeTrend(params) {
164
165
  const { startDate, endDate } = resolveDateRange(params.startDate, params.endDate);
165
- return client.datalabShoppingKeywordByAge({
166
+ return client().datalabShoppingKeywordByAge({
166
167
  startDate,
167
168
  endDate,
168
169
  timeUnit: params.timeUnit,
@@ -1,7 +1,8 @@
1
1
  import { NaverSearchClient } from "../clients/naver-search.client.js";
2
2
  import { SearchArgsSchema } from "../schemas/search.schemas.js";
3
- // 클라이언트 인스턴스
4
- const client = NaverSearchClient.getInstance();
3
+ // 클라이언트는 호출 시점에 꺼낸다. 모듈 로드 시점에 잡아두면 설정 변경으로
4
+ // resetServerInstance()가 싱글톤을 교체한 뒤에도 비워진 옛 인스턴스를 계속 쓴다.
5
+ const client = () => NaverSearchClient.getInstance();
5
6
  export const searchToolHandlers = {
6
7
  search_webkr: (args) => {
7
8
  console.error("search_webkr called with args:", JSON.stringify(args, null, 2));
@@ -40,45 +41,44 @@ export const searchToolHandlers = {
40
41
  * 지식백과 검색 핸들러
41
42
  */
42
43
  export async function handleEncycSearch(params) {
43
- return client.search("encyc", params);
44
+ return client().search("encyc", params);
44
45
  }
45
46
  /**
46
47
  * 이미지 검색 핸들러
47
48
  */
48
49
  export async function handleImageSearch(params) {
49
- return client.search("image", params);
50
+ return client().search("image", params);
50
51
  }
51
52
  /**
52
53
  * 지식iN 검색 핸들러
53
54
  */
54
55
  export async function handleKinSearch(params) {
55
- return client.search("kin", params);
56
+ return client().search("kin", params);
56
57
  }
57
58
  /**
58
59
  * 지역 검색 핸들러
59
60
  */
60
61
  export async function handleLocalSearch(params) {
61
- return client.searchLocal(params);
62
+ return client().searchLocal(params);
62
63
  }
63
64
  /**
64
65
  * 뉴스 검색 핸들러
65
66
  */
66
67
  export async function handleNewsSearch(params) {
67
- return client.search("news", params);
68
+ return client().search("news", params);
68
69
  }
69
70
  /**
70
71
  * 블로그 검색 핸들러
71
72
  */
72
73
  export async function handleBlogSearch(params) {
73
- return client.search("blog", params);
74
+ return client().search("blog", params);
74
75
  }
75
76
  /**
76
77
  * 카페글 검색 핸들러
77
78
  */
78
79
  export async function handleCafeArticleSearch(params) {
79
- return client.search("cafearticle", params);
80
+ return client().search("cafearticle", params);
80
81
  }
81
82
  export async function handleWebKrSearch(args) {
82
- const client = NaverSearchClient.getInstance();
83
- return await client.search("webkr", args);
83
+ return client().search("webkr", args);
84
84
  }
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
2
  import { z } from "zod";
4
3
  export declare const configSchema: z.ZodObject<{
5
4
  NAVER_CLIENT_ID: z.ZodOptional<z.ZodString>;
@@ -23,7 +22,7 @@ export declare const configSchema: z.ZodObject<{
23
22
  export declare function resetServerInstance(): void;
24
23
  export declare function createNaverSearchServer({ config, }: {
25
24
  config: z.infer<typeof configSchema>;
26
- }): McpServer | import("@modelcontextprotocol/sdk/server/index.js").Server<{
25
+ }): import("@modelcontextprotocol/sdk/server/index.js").Server<{
27
26
  method: string;
28
27
  params?: {
29
28
  [x: string]: unknown;
package/dist/src/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync, realpathSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
2
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
7
  import { z } from "zod";
@@ -10,6 +13,9 @@ import { DatalabSearchSchema, DatalabShoppingSchema, DatalabShoppingDeviceSchema
10
13
  import { FindCategorySchema } from "./schemas/category.schemas.js";
11
14
  import { findCategoryHandler, clearCategoriesCache } from "./handlers/category.handlers.js";
12
15
  import { resolveCredentials } from "./config/credentials.js";
16
+ // 빌드 결과물(dist/src/index.js) 기준 두 단계 위가 패키지 루트다.
17
+ // 버전을 하드코딩하면 릴리스할 때마다 serverInfo가 뒤처진다.
18
+ const { version: PACKAGE_VERSION } = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json"), "utf8").toString());
13
19
  // Configuration schema for programmatic server creation and stdio startup
14
20
  export const configSchema = z.object({
15
21
  NAVER_CLIENT_ID: z.string().optional().describe("네이버 개발자센터 Client ID"),
@@ -61,13 +67,14 @@ export function createNaverSearchServer({ config, }) {
61
67
  resetServerInstance();
62
68
  }
63
69
  // Reuse existing server instance to prevent memory leaks
70
+ // 첫 호출과 같은 타입(Server)을 돌려줘야 호출 횟수에 따라 반환값이 달라지지 않는다.
64
71
  if (globalServerInstance) {
65
- return globalServerInstance;
72
+ return globalServerInstance.server;
66
73
  }
67
74
  // Create a new MCP server only once
68
75
  const server = new McpServer({
69
76
  name: "naver-search",
70
- version: "1.0.49",
77
+ version: PACKAGE_VERSION,
71
78
  });
72
79
  // Initialize Naver client with config
73
80
  const credentials = resolveCredentials(config);
@@ -350,35 +357,24 @@ async function main() {
350
357
  // Note: Always run main in CLI mode since this is an MCP server
351
358
  console.error("Starting MCP server initialization...");
352
359
  console.error("process.argv:", process.argv);
353
- // Check if running as main module - compatible with both ESM and CommonJS
354
- let isMainModule = false;
355
- try {
356
- // Try ESM approach first
357
- if (typeof import.meta !== "undefined" && import.meta.url) {
358
- console.error("import.meta.url:", import.meta.url);
359
- isMainModule =
360
- import.meta.url === `file://${process.argv[1]}` ||
361
- import.meta.url.endsWith(process.argv[1]) ||
362
- process.argv[1].endsWith("index.js");
363
- }
364
- else {
365
- // Fallback for CommonJS or when import.meta is not available
366
- isMainModule =
367
- process.argv[1].endsWith("index.js") ||
368
- process.argv[1].includes("naver-search-mcp");
360
+ /**
361
+ * 이 파일이 진입점으로 실행됐는지 판별한다.
362
+ * npx·전역 설치는 bin 심볼릭 링크로 실행되므로 argv[1]과 이 파일을 realpath로 비교한다.
363
+ * 파일명("index.js")이나 argv 문자열("naver-search-mcp")로 추측하면, 이 모듈을
364
+ * import만 한 다른 프로그램에서도 stdio 서버가 떠서 프로세스를 가로챈다.
365
+ */
366
+ function isEntryPoint() {
367
+ if (!process.argv[1])
368
+ return false;
369
+ try {
370
+ return (realpathSync(process.argv[1]) ===
371
+ realpathSync(fileURLToPath(import.meta.url)));
369
372
  }
370
- // Additional check for NPX execution
371
- if (!isMainModule && process.argv.some(arg => arg.includes('naver-search-mcp'))) {
372
- isMainModule = true;
373
- console.error("Detected NPX execution, forcing main module mode");
373
+ catch {
374
+ return false;
374
375
  }
375
376
  }
376
- catch (error) {
377
- // Fallback for environments where import.meta causes issues
378
- isMainModule =
379
- process.argv[1].endsWith("index.js") ||
380
- process.argv[1].includes("naver-search-mcp");
381
- }
377
+ const isMainModule = isEntryPoint();
382
378
  console.error("isMainModule:", isMainModule);
383
379
  if (isMainModule) {
384
380
  console.error("Running as main module, starting server...");
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "@isnow890/naver-search-mcp",
3
- "version": "1.0.53",
3
+ "version": "1.0.55",
4
4
  "description": "Naver Search MCP Server",
5
5
  "main": "dist/src/index.js",
6
- "module": "./src/index.ts",
7
- "types": "dist/index.d.ts",
6
+ "types": "dist/src/index.d.ts",
8
7
  "files": [
9
8
  "dist"
10
9
  ],