content-genie-mcp 2.9.1 → 2.9.2

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.
Files changed (3) hide show
  1. package/Dockerfile +10 -4
  2. package/dist/index.js +191 -41
  3. package/package.json +4 -1
package/Dockerfile CHANGED
@@ -2,12 +2,18 @@ FROM node:20-alpine
2
2
 
3
3
  WORKDIR /app
4
4
 
5
- # 모든 파일 복사 먼저 (prepare 스크립트 실행을 위해)
6
- COPY package*.json tsconfig.json ./
5
+ # package.json과 tsconfig.json 복사
6
+ COPY package*.json ./
7
+ COPY tsconfig.json ./
8
+
9
+ # 의존성 설치 (prepare 스크립트 무시)
10
+ RUN npm ci --ignore-scripts
11
+
12
+ # 소스 코드 복사
7
13
  COPY src/ ./src/
8
14
 
9
- # 모든 의존성 설치 (npm ci가 prepare 스크립트 실행하여 빌드도 함께)
10
- RUN npm ci
15
+ # 빌드 실행
16
+ RUN npm run build
11
17
 
12
18
  # 프로덕션 의존성만 남기기
13
19
  RUN npm prune --production
package/dist/index.js CHANGED
@@ -1,17 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
5
5
  import { z } from "zod";
6
6
  import axios from "axios";
7
7
  import * as cheerio from "cheerio";
8
- import { createServer } from "http";
8
+ import express from "express";
9
+ import cors from "cors";
10
+ import crypto from "crypto";
9
11
  // =============================================================================
10
12
  // Content Genie MCP v2.5 - 한국 콘텐츠 크리에이터를 위한 AI 어시스턴트 (프로 버전)
11
13
  // =============================================================================
12
14
  const server = new McpServer({
13
15
  name: "content-genie-mcp",
14
- version: "2.7.0",
16
+ version: "2.9.2",
15
17
  });
16
18
  // =============================================================================
17
19
  // 공통 스키마
@@ -354,13 +356,13 @@ server.tool("optimize_title_hashtags", "AI 기반으로 콘텐츠 제목을 최
354
356
  // =============================================================================
355
357
  // Tool 4: SEO 키워드 분석 (analyze_seo_keywords) - 고도화
356
358
  // =============================================================================
357
- server.tool("analyze_seo_keywords", "키워드의 SEO 잠재력을 심층 분석하고 네이버/구글 최적화 전략을 제공합니다.", {
359
+ server.tool("analyze_seo_keywords", "키워드의 SEO 잠재력을 심층 분석하고 다음/네이버/구글 최적화 전략을 제공합니다.", {
358
360
  keyword: z.string().describe("분석할 메인 키워드"),
359
- search_engine: z.enum(["naver", "google", "both"]).optional().describe("검색엔진 (naver, google, both)"),
361
+ search_engine: z.enum(["daum", "naver", "google", "all"]).optional().describe("검색엔진 (daum, naver, google, all)"),
360
362
  include_questions: z.boolean().optional().describe("관련 질문 키워드 포함"),
361
363
  include_longtail: z.boolean().optional().describe("롱테일 키워드 포함"),
362
364
  competitor_analysis: z.boolean().optional().describe("경쟁 분석 포함"),
363
- }, async ({ keyword, search_engine = "both", include_questions = true, include_longtail = true, competitor_analysis = true }) => {
365
+ }, async ({ keyword, search_engine = "all", include_questions = true, include_longtail = true, competitor_analysis = true }) => {
364
366
  try {
365
367
  const analysis = await analyzeAdvancedSEOKeywords(keyword, search_engine, include_questions, include_longtail, competitor_analysis);
366
368
  return {
@@ -618,6 +620,31 @@ async function getGoogleAutocomplete(keyword) {
618
620
  return [];
619
621
  }
620
622
  }
623
+ // 다음 자동완성 키워드
624
+ async function getDaumAutocomplete(keyword) {
625
+ try {
626
+ const response = await axios.get(`https://suggest.search.daum.net/sushi/ns`, {
627
+ params: {
628
+ w: 'tot',
629
+ mod: 'json',
630
+ code: 'utf_in_out',
631
+ q: keyword,
632
+ },
633
+ headers: {
634
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
635
+ },
636
+ timeout: 5000,
637
+ });
638
+ // 응답 형식: {"q":"keyword","items":["suggestion1", "suggestion2", ...]}
639
+ if (response.data && Array.isArray(response.data.items)) {
640
+ return response.data.items.slice(0, 10);
641
+ }
642
+ return [];
643
+ }
644
+ catch {
645
+ return [];
646
+ }
647
+ }
621
648
  // 네이버 연관검색어 스크래핑
622
649
  async function getNaverRelatedKeywords(keyword) {
623
650
  try {
@@ -705,6 +732,34 @@ async function getGoogleSearchResultCount(keyword) {
705
732
  return 100000;
706
733
  }
707
734
  }
735
+ // 다음 검색 결과 수 추정
736
+ async function getDaumSearchResultCount(keyword) {
737
+ try {
738
+ const response = await axios.get(`https://search.daum.net/search`, {
739
+ params: {
740
+ w: 'blog',
741
+ q: keyword,
742
+ },
743
+ headers: {
744
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
745
+ },
746
+ timeout: 5000,
747
+ });
748
+ const $ = cheerio.load(response.data);
749
+ // 검색 결과 수 추출 시도
750
+ const countText = $('.sub_expander .txt_info, .cont_result .txt_info, [class*="count"]').first().text();
751
+ const match = countText.match(/[\d,]+/);
752
+ if (match) {
753
+ return parseInt(match[0].replace(/,/g, ''), 10);
754
+ }
755
+ // 대략적 추정: 검색 결과 아이템 수 기반
756
+ const itemCount = $('.wrap_cont.blog, .cont_blog').length;
757
+ return itemCount > 0 ? itemCount * 10000 : 50000;
758
+ }
759
+ catch {
760
+ return 50000; // 기본값
761
+ }
762
+ }
708
763
  // 검색량 레벨 추정 (자동완성 순위 기반)
709
764
  function estimateSearchVolume(autocompleteRank, resultCount) {
710
765
  // 자동완성 상위 + 결과 수 많음 = 검색량 높음
@@ -1984,20 +2039,26 @@ function calculateReadabilityScore(text) {
1984
2039
  // 고급 SEO 키워드 분석 - 실시간 데이터 기반
1985
2040
  async function analyzeAdvancedSEOKeywords(keyword, searchEngine, includeQuestions, includeLongtail, competitorAnalysis) {
1986
2041
  // 병렬로 실시간 데이터 수집
1987
- const [naverAutocomplete, googleAutocomplete, naverRelated, naverResultCount, googleResultCount] = await Promise.all([
1988
- searchEngine !== 'google' ? getNaverAutocomplete(keyword) : Promise.resolve([]),
1989
- searchEngine !== 'naver' ? getGoogleAutocomplete(keyword) : Promise.resolve([]),
2042
+ const includeNaver = searchEngine === 'naver' || searchEngine === 'all';
2043
+ const includeGoogle = searchEngine === 'google' || searchEngine === 'all';
2044
+ const includeDaum = searchEngine === 'daum' || searchEngine === 'all';
2045
+ const [naverAutocomplete, googleAutocomplete, daumAutocomplete, naverRelated, naverResultCount, googleResultCount, daumResultCount] = await Promise.all([
2046
+ includeNaver ? getNaverAutocomplete(keyword) : Promise.resolve([]),
2047
+ includeGoogle ? getGoogleAutocomplete(keyword) : Promise.resolve([]),
2048
+ includeDaum ? getDaumAutocomplete(keyword) : Promise.resolve([]),
1990
2049
  getNaverRelatedKeywords(keyword),
1991
- searchEngine !== 'google' ? getNaverSearchResultCount(keyword) : Promise.resolve(0),
1992
- searchEngine !== 'naver' ? getGoogleSearchResultCount(keyword) : Promise.resolve(0),
2050
+ includeNaver ? getNaverSearchResultCount(keyword) : Promise.resolve(0),
2051
+ includeGoogle ? getGoogleSearchResultCount(keyword) : Promise.resolve(0),
2052
+ includeDaum ? getDaumSearchResultCount(keyword) : Promise.resolve(0),
1993
2053
  ]);
1994
2054
  // 주요 검색 엔진 결과 수 선택
1995
2055
  const primaryResultCount = searchEngine === 'naver' ? naverResultCount :
1996
2056
  searchEngine === 'google' ? googleResultCount :
1997
- Math.max(naverResultCount, googleResultCount);
2057
+ searchEngine === 'daum' ? daumResultCount :
2058
+ Math.max(naverResultCount, googleResultCount, daumResultCount);
1998
2059
  // 경쟁도 및 검색량 추정
1999
2060
  const competition = estimateCompetition(primaryResultCount);
2000
- const autocompleteKeywords = [...new Set([...naverAutocomplete, ...googleAutocomplete])];
2061
+ const autocompleteKeywords = [...new Set([...daumAutocomplete, ...naverAutocomplete, ...googleAutocomplete])];
2001
2062
  const keywordRank = autocompleteKeywords.findIndex(k => k.includes(keyword)) + 1 || 10;
2002
2063
  const searchVolume = estimateSearchVolume(keywordRank, primaryResultCount);
2003
2064
  // SEO 난이도 계산
@@ -2140,6 +2201,18 @@ async function analyzeAdvancedSEOKeywords(keyword, searchEngine, includeQuestion
2140
2201
  ],
2141
2202
  content_types: ["웹사이트", "유튜브", "뉴스"],
2142
2203
  },
2204
+ daum: {
2205
+ result_count: daumResultCount.toLocaleString(),
2206
+ competition: estimateCompetition(daumResultCount).level,
2207
+ tips: [
2208
+ "다음 블로그/카페에 발행하세요",
2209
+ "카카오 채널과 연동 고려",
2210
+ "티스토리 블로그 활용 추천",
2211
+ "다음 뉴스 검색 노출 전략",
2212
+ "카카오톡 공유 최적화",
2213
+ ],
2214
+ content_types: ["티스토리", "다음카페", "브런치"],
2215
+ },
2143
2216
  };
2144
2217
  // 추천 액션 생성
2145
2218
  const recommendedAction = seoDifficulty > 70
@@ -2150,9 +2223,11 @@ async function analyzeAdvancedSEOKeywords(keyword, searchEngine, includeQuestion
2150
2223
  return {
2151
2224
  main_keyword: keyword,
2152
2225
  data_source: {
2226
+ daum_autocomplete: daumAutocomplete.length,
2153
2227
  naver_autocomplete: naverAutocomplete.length,
2154
2228
  google_autocomplete: googleAutocomplete.length,
2155
2229
  naver_related: naverRelated.length,
2230
+ daum_results: daumResultCount.toLocaleString(),
2156
2231
  naver_results: naverResultCount.toLocaleString(),
2157
2232
  google_results: googleResultCount.toLocaleString(),
2158
2233
  },
@@ -2169,7 +2244,7 @@ async function analyzeAdvancedSEOKeywords(keyword, searchEngine, includeQuestion
2169
2244
  related_keywords: relatedKeywords.slice(0, 15),
2170
2245
  question_keywords: questionKeywords.slice(0, 8),
2171
2246
  longtail_keywords: longtailKeywords.slice(0, 8),
2172
- search_engine_strategy: searchEngine === "both" ? searchEngineStrategy : searchEngineStrategy[searchEngine],
2247
+ search_engine_strategy: searchEngine === "all" ? searchEngineStrategy : searchEngineStrategy[searchEngine],
2173
2248
  content_recommendations: {
2174
2249
  ideal_length: seoDifficulty > 60 ? "4000-6000자 (경쟁 대응)" : "2500-4000자",
2175
2250
  must_include: ["정의", "방법", "예시", "FAQ", "비교"],
@@ -3910,44 +3985,119 @@ async function main() {
3910
3985
  if (isHttpMode) {
3911
3986
  // HTTP/SSE 모드 (PlayMCP, 웹 클라이언트용)
3912
3987
  console.log(`Starting Content Genie MCP Server v2.9.0 in HTTP mode on port ${port}...`);
3913
- const httpTransport = new StreamableHTTPServerTransport({
3914
- sessionIdGenerator: () => `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
3988
+ const app = express();
3989
+ app.use(cors());
3990
+ app.use(express.json());
3991
+ // 도구 목록 (PlayMCP 연결 확인용)
3992
+ const toolsList = [
3993
+ { name: "get_korean_trends", description: "다음/네이버 실시간 트렌드 조회" },
3994
+ { name: "analyze_news_trends", description: "뉴스 트렌드 분석" },
3995
+ { name: "get_seasonal_content_guide", description: "시즌 콘텐츠 가이드" },
3996
+ { name: "analyze_seo_keywords", description: "SEO 키워드 심층 분석" },
3997
+ { name: "generate_hashtag_strategy", description: "해시태그 전략 생성" },
3998
+ { name: "analyze_competitor_content", description: "경쟁사 콘텐츠 분석" },
3999
+ { name: "generate_content_ideas", description: "콘텐츠 아이디어 생성" },
4000
+ { name: "optimize_title_hashtags", description: "제목/해시태그 최적화" },
4001
+ { name: "create_content_calendar", description: "콘텐츠 캘린더 생성" },
4002
+ { name: "generate_script_outline", description: "스크립트 아웃라인 생성" },
4003
+ { name: "repurpose_content", description: "콘텐츠 리퍼포징" },
4004
+ { name: "predict_viral_score", description: "바이럴 점수 예측" },
4005
+ { name: "benchmark_content_performance", description: "성과 벤치마크" },
4006
+ { name: "predict_content_performance", description: "콘텐츠 성과 예측" },
4007
+ { name: "analyze_thumbnail", description: "썸네일 분석" },
4008
+ { name: "generate_ab_test_variants", description: "A/B 테스트 변형 생성" },
4009
+ { name: "analyze_influencer_collab", description: "인플루언서 협업 분석" },
4010
+ ];
4011
+ // Health check
4012
+ app.get('/', (_req, res) => {
4013
+ res.json({
4014
+ status: 'ok',
4015
+ server: 'content-genie-mcp',
4016
+ version: '2.9.2',
4017
+ tools: 17,
4018
+ timestamp: new Date().toISOString()
4019
+ });
3915
4020
  });
3916
- const httpServer = createServer(async (req, res) => {
3917
- // CORS 헤더
3918
- res.setHeader('Access-Control-Allow-Origin', '*');
3919
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
3920
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Session-Id');
3921
- if (req.method === 'OPTIONS') {
3922
- res.writeHead(204);
3923
- res.end();
4021
+ app.get('/health', (_req, res) => {
4022
+ res.json({
4023
+ status: 'ok',
4024
+ server: 'content-genie-mcp',
4025
+ version: '2.9.2',
4026
+ tools: 17,
4027
+ timestamp: new Date().toISOString()
4028
+ });
4029
+ });
4030
+ // MCP 엔드포인트 - PlayMCP 연결 확인용 (간단한 응답)
4031
+ app.post('/mcp', (req, res) => {
4032
+ const { method, id } = req.body;
4033
+ // initialize 요청에 대한 응답
4034
+ if (method === 'initialize') {
4035
+ res.json({
4036
+ jsonrpc: '2.0',
4037
+ id,
4038
+ result: {
4039
+ protocolVersion: '2024-11-05',
4040
+ capabilities: { tools: { listChanged: true } },
4041
+ serverInfo: { name: 'content-genie-mcp', version: '2.9.2' }
4042
+ }
4043
+ });
3924
4044
  return;
3925
4045
  }
3926
- // Health check
3927
- if (req.url === '/health' || req.url === '/') {
3928
- res.writeHead(200, { 'Content-Type': 'application/json' });
3929
- res.end(JSON.stringify({
3930
- status: 'ok',
3931
- server: 'content-genie-mcp',
3932
- version: '2.9.0',
3933
- tools: 17,
3934
- timestamp: new Date().toISOString()
3935
- }));
4046
+ // tools/list 요청에 대한 응답
4047
+ if (method === 'tools/list') {
4048
+ res.json({
4049
+ jsonrpc: '2.0',
4050
+ id,
4051
+ result: { tools: toolsList }
4052
+ });
3936
4053
  return;
3937
4054
  }
3938
- // MCP 요청 처리
3939
- if (req.url === '/mcp' || req.url === '/sse') {
3940
- await httpTransport.handleRequest(req, res);
4055
+ // 기타 요청
4056
+ res.json({
4057
+ jsonrpc: '2.0',
4058
+ id,
4059
+ error: { code: -32601, message: 'Method not found' }
4060
+ });
4061
+ });
4062
+ // SSE endpoint for MCP connection (MCP Inspector용)
4063
+ const transports = new Map();
4064
+ app.get('/sse', async (_req, res) => {
4065
+ console.log('New SSE connection established');
4066
+ const transport = new SSEServerTransport('/message', res);
4067
+ const sessionId = crypto.randomUUID();
4068
+ transports.set(sessionId, transport);
4069
+ res.on('close', () => {
4070
+ console.log('SSE connection closed');
4071
+ transports.delete(sessionId);
4072
+ });
4073
+ await server.connect(transport);
4074
+ });
4075
+ // Message endpoint for MCP communication
4076
+ app.post('/message', async (req, res) => {
4077
+ const sessionId = req.query.sessionId;
4078
+ if (!sessionId) {
4079
+ const transport = Array.from(transports.values())[0];
4080
+ if (transport) {
4081
+ await transport.handlePostMessage(req, res);
4082
+ }
4083
+ else {
4084
+ res.status(400).json({ error: 'No active session' });
4085
+ }
3941
4086
  return;
3942
4087
  }
3943
- res.writeHead(404);
3944
- res.end('Not Found');
4088
+ const transport = transports.get(sessionId);
4089
+ if (transport) {
4090
+ await transport.handlePostMessage(req, res);
4091
+ }
4092
+ else {
4093
+ res.status(404).json({ error: 'Session not found' });
4094
+ }
3945
4095
  });
3946
- await server.connect(httpTransport);
3947
- httpServer.listen(port, () => {
4096
+ app.listen(port, () => {
3948
4097
  console.log(`Content Genie MCP Server v2.9.0 running on HTTP port ${port}`);
3949
4098
  console.log(`Health check: http://localhost:${port}/health`);
3950
4099
  console.log(`MCP endpoint: http://localhost:${port}/mcp`);
4100
+ console.log(`SSE endpoint: http://localhost:${port}/sse`);
3951
4101
  });
3952
4102
  }
3953
4103
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-genie-mcp",
3
- "version": "2.9.1",
3
+ "version": "2.9.2",
4
4
  "description": "AI Content Creation Assistant MCP - 한국 콘텐츠 크리에이터를 위한 트렌드 분석 및 콘텐츠 생성 도우미",
5
5
  "main": "dist/index.js",
6
6
  "bin": "dist/index.js",
@@ -36,9 +36,12 @@
36
36
  "@modelcontextprotocol/sdk": "^1.25.1",
37
37
  "axios": "^1.13.2",
38
38
  "cheerio": "^1.1.2",
39
+ "cors": "^2.8.5",
39
40
  "zod": "^4.2.1"
40
41
  },
41
42
  "devDependencies": {
43
+ "@types/cors": "^2.8.19",
44
+ "@types/express": "^5.0.6",
42
45
  "@types/node": "^25.0.3",
43
46
  "ts-node": "^10.9.2",
44
47
  "typescript": "^5.9.3"