@sgrsoft/page24-mcp-server 0.1.0 → 0.1.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.
package/README.md ADDED
@@ -0,0 +1,6 @@
1
+ ## page24-mcp
2
+ > Creator: page24@sgrsoft.com
3
+ > Date: 2026/03/25
4
+
5
+ ## Description
6
+ * page24-mcp에 대한 설명을 작성하세요
package/dist/server.js CHANGED
@@ -5,6 +5,7 @@ import { registerMenuTools } from "./tools/menus.js";
5
5
  import { registerSiteTools } from "./tools/site.js";
6
6
  import { registerMemberTools } from "./tools/members.js";
7
7
  import { registerFormTools } from "./tools/forms.js";
8
+ import { registerAnalyticsTools } from "./tools/analytics.js";
8
9
  export function createServer() {
9
10
  const apiUrl = process.env.PAGE24_API_URL;
10
11
  const apiKey = process.env.PAGE24_API_KEY;
@@ -23,5 +24,6 @@ export function createServer() {
23
24
  registerSiteTools(server, api);
24
25
  registerMemberTools(server, api);
25
26
  registerFormTools(server, api);
27
+ registerAnalyticsTools(server, api);
26
28
  return server;
27
29
  }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { Page24ApiClient } from "../api-client.js";
3
+ export declare function registerAnalyticsTools(server: McpServer, api: Page24ApiClient): void;
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ export function registerAnalyticsTools(server, api) {
3
+ server.tool("get_visitor_stats", "사이트 방문자 통계를 조회합니다. 날짜별 PV(페이지뷰)와 UV(순방문자)를 확인할 수 있습니다.", {
4
+ start_date: z.string().describe("시작일 (YYYY-MM-DD)"),
5
+ end_date: z.string().describe("종료일 (YYYY-MM-DD)"),
6
+ mcode: z.string().optional().describe("특정 메뉴의 통계만 조회 (4자리 이상 메뉴코드)"),
7
+ }, async ({ start_date, end_date, mcode }) => {
8
+ const params = {
9
+ hostid: api.getSiteId(),
10
+ start_date,
11
+ end_date,
12
+ };
13
+ if (mcode)
14
+ params.mcode = mcode;
15
+ const res = await api.get("/log/visitLogStat", params);
16
+ if (res.code !== 200) {
17
+ return { content: [{ type: "text", text: `오류: ${res.message}` }] };
18
+ }
19
+ const result = res.result;
20
+ const summary = [
21
+ `📊 방문자 통계 (${start_date} ~ ${end_date})`,
22
+ ``,
23
+ `총 페이지뷰(PV): ${result.total_pv_cnt}`,
24
+ `총 순방문자(UV): ${result.total_uv_cnt}`,
25
+ `일 평균 PV: ${result.avg_pv_cnt}`,
26
+ `일 평균 UV: ${result.avg_uv_cnt}`,
27
+ `최대 일 PV: ${result.max_pv_cnt}`,
28
+ `최대 일 UV: ${result.max_uv_cnt}`,
29
+ ``,
30
+ `📅 일별 상세:`,
31
+ ...(Array.isArray(result.list)
32
+ ? result.list.map((d) => ` ${d.date} PV: ${d.pv_cnt} UV: ${d.uv_cnt}`)
33
+ : []),
34
+ ];
35
+ return { content: [{ type: "text", text: summary.join("\n") }] };
36
+ });
37
+ }
@@ -5,15 +5,31 @@ export function registerMenuTools(server, api) {
5
5
  return { content: [{ type: "text", text: `오류: ${res.message}` }] };
6
6
  }
7
7
  const menus = res.result;
8
- const summary = Array.isArray(menus)
9
- ? menus.map((m) => ({
8
+ if (!Array.isArray(menus)) {
9
+ return { content: [{ type: "text", text: JSON.stringify(menus, null, 2) }] };
10
+ }
11
+ // 1뎁스: 2자리 mcode (하위 분류용), 2뎁스: 4자리 mcode (실제 기능 메뉴)
12
+ const topMenus = menus.filter((m) => m.mcode.length === 2);
13
+ const tree = topMenus.map((parent) => {
14
+ const children = menus
15
+ .filter((m) => m.scode === parent.mcode && m.mcode.length >= 4)
16
+ .map((m) => ({
10
17
  mcode: m.mcode,
11
18
  title: m.title,
12
19
  type: m.type,
13
20
  state: m.state,
14
- scode: m.scode,
15
- }))
16
- : menus;
17
- return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
21
+ }));
22
+ return {
23
+ mcode: parent.mcode,
24
+ title: parent.title,
25
+ note: "하위분류용(게시글 작성 불가)",
26
+ children,
27
+ };
28
+ });
29
+ const boards = menus.filter((m) => m.mcode.length >= 4 && m.type === "board");
30
+ const hint = boards.length > 0
31
+ ? `\n\n📌 게시글 작성 가능한 게시판 메뉴:\n${boards.map((b) => ` - ${b.mcode}: ${b.title}`).join("\n")}`
32
+ : "\n\n⚠️ 게시판(type='board') 메뉴가 없습니다.";
33
+ return { content: [{ type: "text", text: JSON.stringify(tree, null, 2) + hint }] };
18
34
  });
19
35
  }
@@ -1,11 +1,20 @@
1
1
  import { z } from "zod";
2
+ function validateMcode(mcode) {
3
+ if (mcode.length < 4) {
4
+ return `오류: mcode '${mcode}'는 ${mcode.length}자리입니다. 2자리 메뉴는 하위 분류용이므로 게시글을 작성할 수 없습니다. 4자리 이상의 메뉴코드(type이 'board'인 메뉴)를 사용하세요. list_menus로 사용 가능한 게시판 메뉴를 확인하세요.`;
5
+ }
6
+ return null;
7
+ }
2
8
  export function registerPostTools(server, api) {
3
- server.tool("list_posts", "게시글 목록을 조회합니다. mcode(메뉴코드) 필요합니다. list_menus로 먼저 메뉴 목록을 확인하세요.", {
4
- mcode: z.string().describe("메뉴 코드 (list_menus 확인)"),
9
+ server.tool("list_posts", "게시글 목록을 조회합니다. mcode는 반드시 4자리 이상이어야 합니다(2자리는 하위 분류용). list_menus로 먼저 type이 'board'인 메뉴의 mcode를 확인하세요.", {
10
+ mcode: z.string().min(4).describe("메뉴 코드 (4자리 이상, list_menus에서 type='board'인 메뉴)"),
5
11
  pageNo: z.number().optional().default(1).describe("페이지 번호"),
6
12
  limit: z.number().optional().default(10).describe("페이지당 개수"),
7
13
  keyword: z.string().optional().describe("검색어"),
8
14
  }, async ({ mcode, pageNo, limit, keyword }) => {
15
+ const err = validateMcode(mcode);
16
+ if (err)
17
+ return { content: [{ type: "text", text: err }] };
9
18
  const res = await api.get("/board/List", {
10
19
  mcode,
11
20
  pageNo,
@@ -17,22 +26,28 @@ export function registerPostTools(server, api) {
17
26
  }
18
27
  return { content: [{ type: "text", text: JSON.stringify(res.result, null, 2) }] };
19
28
  });
20
- server.tool("get_post", "게시글 상세 내용을 조회합니다.", {
21
- mcode: z.string().describe("메뉴 코드"),
29
+ server.tool("get_post", "게시글 상세 내용을 조회합니다. mcode는 4자리 이상이어야 합니다.", {
30
+ mcode: z.string().min(4).describe("메뉴 코드 (4자리 이상)"),
22
31
  board_id: z.number().describe("게시글 번호"),
23
32
  }, async ({ mcode, board_id }) => {
33
+ const err = validateMcode(mcode);
34
+ if (err)
35
+ return { content: [{ type: "text", text: err }] };
24
36
  const res = await api.get("/board/read", { mcode, board_id });
25
37
  if (res.code !== 200) {
26
38
  return { content: [{ type: "text", text: `오류: ${res.message}` }] };
27
39
  }
28
40
  return { content: [{ type: "text", text: JSON.stringify(res.result, null, 2) }] };
29
41
  });
30
- server.tool("create_post", "새 게시글을 작성합니다. (readwrite 권한 필요)", {
31
- mcode: z.string().describe("메뉴 코드"),
42
+ server.tool("create_post", "새 게시글을 작성합니다. (readwrite 권한 필요) mcode는 반드시 4자리 이상의 게시판(type='board') 메뉴코드여야 합니다. 2자리 메뉴는 하위 분류용이라 게시글 작성이 불가합니다.", {
43
+ mcode: z.string().min(4).describe("메뉴 코드 (4자리 이상, type='board'인 메뉴만 가능)"),
32
44
  title: z.string().describe("게시글 제목"),
33
45
  content: z.string().describe("게시글 내용 (HTML)"),
34
46
  category: z.string().optional().describe("카테고리"),
35
47
  }, async ({ mcode, title, content, category }) => {
48
+ const err = validateMcode(mcode);
49
+ if (err)
50
+ return { content: [{ type: "text", text: err }] };
36
51
  const data = { mcode, title, content };
37
52
  if (category)
38
53
  data.category = category;
@@ -42,12 +57,15 @@ export function registerPostTools(server, api) {
42
57
  }
43
58
  return { content: [{ type: "text", text: `게시글 작성 완료: ${JSON.stringify(res.result)}` }] };
44
59
  });
45
- server.tool("update_post", "기존 게시글을 수정합니다. (readwrite 권한 필요)", {
46
- mcode: z.string().describe("메뉴 코드"),
60
+ server.tool("update_post", "기존 게시글을 수정합니다. (readwrite 권한 필요) mcode는 4자리 이상이어야 합니다.", {
61
+ mcode: z.string().min(4).describe("메뉴 코드 (4자리 이상)"),
47
62
  board_id: z.number().describe("게시글 번호"),
48
63
  title: z.string().optional().describe("수정할 제목"),
49
64
  content: z.string().optional().describe("수정할 내용 (HTML)"),
50
65
  }, async ({ mcode, board_id, title, content }) => {
66
+ const err = validateMcode(mcode);
67
+ if (err)
68
+ return { content: [{ type: "text", text: err }] };
51
69
  const data = { mcode, board_id };
52
70
  if (title)
53
71
  data.title = title;
@@ -59,10 +77,13 @@ export function registerPostTools(server, api) {
59
77
  }
60
78
  return { content: [{ type: "text", text: `게시글 수정 완료: ${JSON.stringify(res.result)}` }] };
61
79
  });
62
- server.tool("delete_post", "게시글을 삭제합니다. (readwrite 권한 필요)", {
63
- mcode: z.string().describe("메뉴 코드"),
80
+ server.tool("delete_post", "게시글을 삭제합니다. (readwrite 권한 필요) mcode는 4자리 이상이어야 합니다.", {
81
+ mcode: z.string().min(4).describe("메뉴 코드 (4자리 이상)"),
64
82
  board_id: z.number().describe("게시글 번호"),
65
83
  }, async ({ mcode, board_id }) => {
84
+ const err = validateMcode(mcode);
85
+ if (err)
86
+ return { content: [{ type: "text", text: err }] };
66
87
  const res = await api.post("/board/delete", { mcode, board_id });
67
88
  if (res.code !== 200) {
68
89
  return { content: [{ type: "text", text: `오류: ${res.message}` }] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sgrsoft/page24-mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "MCP server for page24 CMS",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,8 @@
10
10
  "scripts": {
11
11
  "build": "tsc",
12
12
  "dev": "tsx src/index.ts",
13
- "prepublishOnly": "npm run build"
13
+ "prepublishOnly": "npm run build",
14
+ "release": "release-it"
14
15
  },
15
16
  "dependencies": {
16
17
  "@modelcontextprotocol/sdk": "^1.12.1",
@@ -20,8 +21,14 @@
20
21
  "devDependencies": {
21
22
  "typescript": "^5.7.0",
22
23
  "tsx": "^4.19.0",
23
- "@types/node": "^22.0.0"
24
+ "@types/node": "^22.0.0",
25
+ "release-it": "^18.1.0"
24
26
  },
25
- "files": ["dist"],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
26
33
  "license": "MIT"
27
34
  }