@roottale/cms-mcp 0.48.0 → 0.50.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @roottale/cms-mcp
2
2
 
3
+ ## 0.50.0
4
+
5
+ ### Patch Changes
6
+
7
+ - ebd8fa1: `docs/seo.md` 에 "색인 위생 — 모음 페이지는 기본 '검색 노출 안 함'" 절을 추가하고, 예시 코드 `app/blog/categories/[slug]/page.tsx`·`lib/blog.ts` 를 `decideArchiveIndex`·`buildArchiveMetadata`·`resolveCategoryPromotion` 사용으로 갱신한다. 사이트맵 절에도 "분류·작가 모음 페이지는 기본으로 빠진다" 를 명시했다.
8
+
9
+ 문서 예시가 **존재하지 않는 옵션**(`fetchPosts({ category })`)을 쓰던 오류를 고쳤다 — 공개 API 에는 분류별 서버 필터가 없어 최근 글을 `ARCHIVE_POST_COUNT_LIMIT` 만큼 받아 분류별로 세는 것이 정답이다. 그대로 따라 하면 TypeScript 는 컴파일 오류, JavaScript 는 옵션이 조용히 무시돼 전체 최근 글 수를 그 분류의 글 수로 오판했다.
10
+
11
+ 승격 상태를 boolean 이 아니라 3-state(`promoted`/`not-promoted`/`unknown`)로 다루도록 예시와 정책 표를 바꿨다 — 설정 조회 실패를 "안 켬" 으로 바꿔 넘기면 이미 검색에 올라가 있던 주소에 noindex 가 붙는다. 글 수 모집단 한도를 사이트맵과 공유해야 하는 이유와 그 한계도 문서화했다.
12
+
13
+ 이 문서는 고객 측 AI 에이전트가 읽으므로 렌더러 변경과 같이 재발행해야 드리프트가 없다.
14
+
15
+ - ef72e0a: 분류별 글 수를 서버가 센다 — 오래된 글에만 달린 분류가 검색에서 사라지지 않게
16
+
17
+ 분류 모음 페이지의 "글 0건이면 404" 판정과 사이트맵 포함 여부는 분류별 글 수에서
18
+ 나옵니다. 지금까지는 최근 200건을 받아 세었기 때문에, 글이 많은 사이트에서 그
19
+ 범위 밖에만 글이 있는 분류가 **모음 페이지(404)와 사이트맵 양쪽에서** 함께
20
+ 빠졌습니다.
21
+ - 공개 API에 `GET /v1/cms/public/categories` 추가 — 모든 분류와 발행 글 수를
22
+ 서버가 SQL로 집계해 내려줍니다. 집계 범위는 글 목록 API와 같은 어휘
23
+ (`exclude_collections` / `collection_key` / `locale`)로 지정합니다.
24
+ - `@roottale/cms-client` 에 `fetchCategoryCounts()` 추가.
25
+ - `fetchBlogArchiveCategories`(cms-renderer-next)가 이 API를 먼저 씁니다. 못 받으면
26
+ (구버전 API·일시 장애) 예전 방식으로 자동 복귀하므로 기존 동작이 깨지지 않고,
27
+ 사이트맵과 모음 페이지는 어느 경로에서든 같은 값을 봅니다.
28
+
29
+ 사이트 코드 변경은 필요 없습니다 — 패키지를 올리면 적용됩니다. 다만 사이트가
30
+ 바라보는 API가 아직 이 엔드포인트를 배포하지 않았다면 예전 동작이 그대로
31
+ 유지됩니다.
32
+
33
+ - 1787ac0: `docs/seo.md`의 에셋 도메인 정합 스캔 섹션을 `scanAssetHosts` 하드닝(base/iframe/embed/object/track/audio/input[type=image] 스캔 대상 확장, `<link rel>` 필터링, `imagesrcset`/`image-set()` 지원, HTML 엔티티 디코드 + fail-closed 파싱)에 맞춰 갱신한다. 이 문서는 npm 패키지에 번들되어 고객 측 AI 에이전트가 읽으므로 renderer 변경과 함께 재발행해야 드리프트가 없다.
34
+
35
+ ## 0.49.0
36
+
37
+ ### Minor Changes
38
+
39
+ - b618458: Rank Math 1.0.274의 MCP 기능을 참고해 Google Search Console 상위 검색어 조회, 글 SEO·가독성 분석, 발행 글 SEO 점수 조회 도구를 추가했습니다.
40
+
3
41
  ## 0.48.0
4
42
 
5
43
  ### Patch Changes
package/README.md CHANGED
@@ -12,6 +12,8 @@ RootTale CMS 연동 MCP 서버 + CLI — AI 코딩 에이전트(Claude Code, Cur
12
12
  라우트, RSS/사이트맵, 상담문의 서버 액션 등 완전한 연동 1세트
13
13
  - **CMS 자동화 tool** — 초안 작성·수정·예약·발행, 카테고리/태그 연결,
14
14
  썸네일·본문 이미지 업로드/수정/삭제
15
+ - **SEO 분석 tool** — Google Search Console 상위 검색어, 글별 SEO·가독성
16
+ 분석, 발행 글 점수 필터
15
17
  - **공개 CLI** — 같은 기능을 `npx @roottale/cms-mcp cli ...`로 실행
16
18
 
17
19
  ## 설정
@@ -85,6 +87,9 @@ npx -y @roottale/cms-mcp cli posts publish "<post-id>"
85
87
  | `uploadCmsMedia` | 로컬 이미지/PDF 업로드 및 media id 발급 |
86
88
  | `updateCmsMedia` | 대체 텍스트·캡션 수정 |
87
89
  | `deleteCmsMedia` | 미디어 삭제 |
90
+ | `getTopSeoKeywords` | Google Search Console 상위 검색어 조회 |
91
+ | `analyzeCmsPostSeo` | ROOT-ADMIN과 같은 규칙으로 글 SEO·가독성 분석 |
92
+ | `getCmsPostSeoScores` | 발행 글의 최신 SEO 점수·등급 조회 |
88
93
 
89
94
  ## 사람용 문서 (브라우저 열람)
90
95
 
package/dist/index.js CHANGED
@@ -542,7 +542,7 @@ import {
542
542
  ListToolsRequestSchema,
543
543
  McpError
544
544
  } from "@modelcontextprotocol/sdk/types.js";
545
- import { z as z3 } from "zod";
545
+ import { z as z4 } from "zod";
546
546
 
547
547
  // src/docs.ts
548
548
  import { readdir, readFile as readFile3 } from "fs/promises";
@@ -624,16 +624,155 @@ ${content}`;
624
624
  return sections.join("\n\n");
625
625
  }
626
626
 
627
- // src/tools.ts
628
- var ReadDocInput = z3.object({ path: z3.string().min(1) }).strict();
629
- var SearchDocsInput = z3.object({ query: z3.string().min(1) }).strict();
630
- var ListPostsInput = z3.object({
627
+ // src/seo-tools.ts
628
+ import { z as z3 } from "zod";
629
+
630
+ // src/seo-definitions.ts
631
+ var DATE_RANGES = [
632
+ "last_7_days",
633
+ "last_30_days",
634
+ "last_3_months",
635
+ "last_6_months",
636
+ "last_12_months"
637
+ ];
638
+ var SEO_TOOL_DEFINITIONS = [
639
+ tool2(
640
+ "getTopSeoKeywords",
641
+ "Google Search Console\uC5D0\uC11C \uC0AC\uC774\uD2B8\uC758 \uC0C1\uC704 \uAC80\uC0C9\uC5B4\uC640 \uD074\uB9AD\xB7\uB178\uCD9C\xB7CTR\xB7\uD3C9\uADE0 \uC21C\uC704\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.",
642
+ {
643
+ siteId: stringProperty2("\uC0AC\uC774\uD2B8 ID"),
644
+ dateRange: enumProperty2(DATE_RANGES, "\uC870\uD68C \uAE30\uAC04, \uAE30\uBCF8 last_30_days"),
645
+ limit: numberProperty2("1-100, \uAE30\uBCF8 25")
646
+ }
647
+ ),
648
+ tool2(
649
+ "analyzeCmsPostSeo",
650
+ "ROOT-ADMIN\uACFC \uAC19\uC740 \uADDC\uCE59\uC73C\uB85C \uAE00\uC758 SEO\uC640 \uD55C\uAD6D\uC5B4 \uAC00\uB3C5\uC131\uC744 \uB2E4\uC2DC \uAC80\uC0AC\uD569\uB2C8\uB2E4. \uAE00\uC744 \uC218\uC815\uD558\uAC70\uB098 \uBC1C\uD589\uD558\uAE30 \uC804\uC5D0 \uC0AC\uC6A9\uD558\uC138\uC694.",
651
+ {
652
+ postId: stringProperty2("\uBD84\uC11D\uD560 \uAE00 ID"),
653
+ siteId: stringProperty2("\uC0AC\uC774\uD2B8 ID"),
654
+ focusKeyword: stringProperty2("\uC774\uBC88 \uBD84\uC11D\uC5D0 \uC0AC\uC6A9\uD560 \uC9D1\uC911 \uD0A4\uC6CC\uB4DC")
655
+ },
656
+ ["postId"]
657
+ ),
658
+ tool2(
659
+ "getCmsPostSeoScores",
660
+ "\uBC1C\uD589\uB41C \uAE00\uC758 \uCD5C\uC2E0 SEO \uC810\uC218\uC640 \uB4F1\uAE09\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uAC1C\uC120\uC774 \uD544\uC694\uD55C \uAE00\uC774\uB098 \uC9D1\uC911 \uD0A4\uC6CC\uB4DC\uAC00 \uC5C6\uB294 \uAE00\uC744 \uCC3E\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
661
+ {
662
+ siteId: stringProperty2("\uC0AC\uC774\uD2B8 ID"),
663
+ limit: numberProperty2("1-100, \uAE30\uBCF8 10"),
664
+ type: enumProperty2(["post", "page"], "\uCF58\uD150\uCE20 \uC720\uD615"),
665
+ seoFilter: enumProperty2(
666
+ ["bad", "ok", "good", "empty_keyword"],
667
+ "SEO \uC0C1\uD0DC \uD544\uD130"
668
+ )
669
+ }
670
+ )
671
+ ];
672
+ function isSeoToolName(name) {
673
+ return SEO_TOOL_DEFINITIONS.some((definition) => definition.name === name);
674
+ }
675
+ function tool2(name, description, properties, required = []) {
676
+ return {
677
+ name,
678
+ description,
679
+ inputSchema: {
680
+ type: "object",
681
+ properties,
682
+ ...required.length > 0 ? { required } : {},
683
+ additionalProperties: false
684
+ }
685
+ };
686
+ }
687
+ function stringProperty2(description) {
688
+ return { type: "string", description };
689
+ }
690
+ function numberProperty2(description) {
691
+ return { type: "number", description };
692
+ }
693
+ function enumProperty2(values, description) {
694
+ return { type: "string", enum: values, description };
695
+ }
696
+
697
+ // src/seo-tools.ts
698
+ var SiteIdInput = z3.object({ siteId: z3.string().optional() });
699
+ var TopKeywordsInput = SiteIdInput.extend({
700
+ dateRange: z3.enum([
701
+ "last_7_days",
702
+ "last_30_days",
703
+ "last_3_months",
704
+ "last_6_months",
705
+ "last_12_months"
706
+ ]).optional(),
707
+ limit: z3.number().int().min(1).max(100).optional()
708
+ }).strict();
709
+ var AnalyzePostInput = SiteIdInput.extend({
710
+ postId: z3.string().min(1),
711
+ focusKeyword: z3.string().max(200).optional()
712
+ }).strict();
713
+ var ScoresInput = SiteIdInput.extend({
631
714
  limit: z3.number().int().min(1).max(100).optional(),
632
715
  type: z3.enum(["post", "page"]).optional(),
633
- cursor: z3.string().optional()
716
+ seoFilter: z3.enum(["bad", "ok", "good", "empty_keyword"]).optional()
717
+ }).strict();
718
+ async function callSeoTool(name, args) {
719
+ switch (name) {
720
+ case "getTopSeoKeywords": {
721
+ const input = TopKeywordsInput.parse(args);
722
+ return requestRootTaleApi({
723
+ method: "GET",
724
+ pathname: "/v1/cms/seo/keywords",
725
+ query: optionalQuery2({
726
+ site_id: input.siteId,
727
+ date_range: input.dateRange,
728
+ limit: input.limit
729
+ })
730
+ });
731
+ }
732
+ case "analyzeCmsPostSeo": {
733
+ const input = AnalyzePostInput.parse(args);
734
+ return requestRootTaleApi({
735
+ method: "GET",
736
+ pathname: `/v1/cms/seo/posts/${encodeURIComponent(input.postId)}/analysis`,
737
+ query: optionalQuery2({
738
+ site_id: input.siteId,
739
+ focus_keyword: input.focusKeyword
740
+ })
741
+ });
742
+ }
743
+ case "getCmsPostSeoScores": {
744
+ const input = ScoresInput.parse(args);
745
+ return requestRootTaleApi({
746
+ method: "GET",
747
+ pathname: "/v1/cms/seo/scores",
748
+ query: optionalQuery2({
749
+ site_id: input.siteId,
750
+ limit: input.limit,
751
+ type: input.type,
752
+ seo_filter: input.seoFilter
753
+ })
754
+ });
755
+ }
756
+ }
757
+ }
758
+ function optionalQuery2(values) {
759
+ return Object.fromEntries(
760
+ Object.entries(values).filter(
761
+ (entry) => entry[1] !== void 0
762
+ ).map(([key, value]) => [key, String(value)])
763
+ );
764
+ }
765
+
766
+ // src/tools.ts
767
+ var ReadDocInput = z4.object({ path: z4.string().min(1) }).strict();
768
+ var SearchDocsInput = z4.object({ query: z4.string().min(1) }).strict();
769
+ var ListPostsInput = z4.object({
770
+ limit: z4.number().int().min(1).max(100).optional(),
771
+ type: z4.enum(["post", "page"]).optional(),
772
+ cursor: z4.string().optional()
634
773
  }).strict();
635
- var GetPostInput = z3.object({ slugOrId: z3.string().min(1) }).strict();
636
- var GetSiteKnowledgeInput = z3.object({ siteId: z3.string().optional() }).strict();
774
+ var GetPostInput = z4.object({ slugOrId: z4.string().min(1) }).strict();
775
+ var GetSiteKnowledgeInput = z4.object({ siteId: z4.string().optional() }).strict();
637
776
  var DOCUMENTATION_TOOL_DEFINITIONS = [
638
777
  {
639
778
  name: "listRootTaleDocs",
@@ -711,7 +850,8 @@ var DOCUMENTATION_TOOL_DEFINITIONS = [
711
850
  ];
712
851
  var TOOL_DEFINITIONS = [
713
852
  ...DOCUMENTATION_TOOL_DEFINITIONS,
714
- ...AUTOMATION_TOOL_DEFINITIONS
853
+ ...AUTOMATION_TOOL_DEFINITIONS,
854
+ ...SEO_TOOL_DEFINITIONS
715
855
  ];
716
856
  function textResponse(text) {
717
857
  return { content: [{ type: "text", text }] };
@@ -731,6 +871,9 @@ function registerTools(server) {
731
871
  await callAutomationTool(request.params.name, args)
732
872
  );
733
873
  }
874
+ if (isSeoToolName(request.params.name)) {
875
+ return jsonResponse(await callSeoTool(request.params.name, args));
876
+ }
734
877
  switch (request.params.name) {
735
878
  case "listRootTaleDocs":
736
879
  return jsonResponse(await listDocs());
@@ -801,7 +944,7 @@ function registerTools(server) {
801
944
  }
802
945
 
803
946
  // src/server.ts
804
- var VERSION = true ? "0.48.0" : "dev";
947
+ var VERSION = true ? "0.50.0" : "dev";
805
948
  var SERVER_INSTRUCTIONS = `
806
949
  roottale-cms-mcp\uB294 RootTale CMS\uB97C \uC678\uBD80 \uC0AC\uC774\uD2B8(\uC8FC\uB85C Next.js)\uC5D0 \uC5F0\uB3D9\uD558\uACE0
807
950
  \uAE00\xB7\uC378\uB124\uC77C\xB7\uBCF8\uBB38 \uC774\uBBF8\uC9C0\uB97C \uC790\uB3D9\uD654\uD558\uAE30 \uC704\uD55C \uBB38\uC11C\xB7\uC608\uC2DC \uCF54\uB4DC\xB7API tool\uC744 \uC81C\uACF5\uD569\uB2C8\uB2E4.
@@ -817,6 +960,9 @@ roottale-cms-mcp\uB294 RootTale CMS\uB97C \uC678\uBD80 \uC0AC\uC774\uD2B8(\uC8FC
817
960
  publish=true \uB610\uB294 publishCmsPost\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.
818
961
  - \uC774\uBBF8\uC9C0\uB294 uploadCmsMedia\uB85C \uBA3C\uC800 \uB4F1\uB85D\uD55C \uB4A4 \uBC18\uD658\uB41C id\uB97C featuredMediaId\uC5D0,
819
962
  \uBC18\uD658\uB41C URL\uC744 Tiptap image \uB178\uB4DC\uC758 attrs.src\uC5D0 \uC0AC\uC6A9\uD558\uC138\uC694.
963
+ - \uAE00\uC744 \uC218\uC815\uD558\uAC70\uB098 \uBC1C\uD589\uD558\uAE30 \uC804 analyzeCmsPostSeo\uB85C SEO\uC640 \uAC00\uB3C5\uC131\uC744 \uD655\uC778\uD558\uC138\uC694.
964
+ - \uAC1C\uC120\uD560 \uAE00\uC744 \uCC3E\uC744 \uB54C getCmsPostSeoScores\uB97C, \uC2E4\uC81C \uC720\uC785 \uAC80\uC0C9\uC5B4\uB97C \uACE0\uB97C \uB54C
965
+ getTopSeoKeywords\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.
820
966
  - \uC774\uBBF8 \uD559\uC2B5\uB41C \uB0B4\uC6A9\uC774\uB354\uB77C\uB3C4 \uBCF8 \uC11C\uBC84\uC758 \uBB38\uC11C\uB85C \uB354\uBE14\uCCB4\uD06C \uD6C4 \uC791\uC5C5\uD558\uC138\uC694.
821
967
  `.trim();
822
968
  function createServer() {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/cli.ts","../src/api.ts","../src/automation-definitions.ts","../src/automation-media.ts","../src/automation-schemas.ts","../src/automation-tools.ts","../src/server.ts","../src/tools.ts","../src/docs.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport { runCmsCli } from \"./cli.js\";\nimport { createServer } from \"./server.js\";\n\nif (process.argv[2] === \"cli\") {\n await runCmsCli(process.argv);\n} else {\n const server = createServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n","import { readFile } from \"node:fs/promises\";\n\nimport { Command, InvalidArgumentError } from \"commander\";\n\nimport { callAutomationTool } from \"./automation-tools.js\";\n\ntype CommonOptions = { readonly siteId?: string };\n\nexport function createCmsCli(): Command {\n const cli = new Command()\n .name(\"roottale-cms\")\n .description(\"RootTale CMS 글·썸네일·본문 이미지 자동화 CLI\");\n\n registerPostCommands(cli);\n registerMediaCommands(cli);\n return cli;\n}\n\nexport async function runCmsCli(argv: readonly string[]): Promise<void> {\n try {\n await createCmsCli().parseAsync([\n argv[0] ?? \"node\",\n \"roottale-cms\",\n ...argv.slice(3),\n ]);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`${message}\\n`);\n process.exitCode = 1;\n }\n}\n\nfunction registerPostCommands(cli: Command): void {\n const posts = cli.command(\"posts\").description(\"CMS 글 자동화\");\n\n posts\n .command(\"list\")\n .description(\"초안·예약·발행 글 목록 조회\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--limit <number>\", \"조회 개수\", integer, 20)\n .option(\"--cursor <cursor>\", \"다음 페이지 커서\")\n .option(\"--type <type>\", \"post 또는 page\")\n .option(\"--status <status>\", \"draft, published, scheduled, private\")\n .action(async (options) => {\n await run(\"listManagedCmsPosts\", options);\n });\n\n posts\n .command(\"create\")\n .description(\"Tiptap JSON 본문으로 새 글 생성\")\n .requiredOption(\"--title <title>\", \"글 제목\")\n .requiredOption(\"--slug <slug>\", \"URL 슬러그\")\n .requiredOption(\"--body-file <path>\", \"Tiptap 본문 JSON 파일\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--type <type>\", \"post 또는 page\", \"post\")\n .option(\"--excerpt <text>\", \"요약\")\n .option(\"--featured-media-id <id>\", \"썸네일 미디어 ID\")\n .option(\"--publish\", \"즉시 발행\")\n .option(\"--scheduled-at <iso>\", \"예약 발행 ISO 시각\")\n .option(\"--category-ids <ids>\", \"쉼표로 구분한 카테고리 ID\")\n .option(\"--tag-ids <ids>\", \"쉼표로 구분한 태그 ID\")\n .action(async (options) => {\n await run(\"createCmsPost\", {\n ...options,\n bodyJson: await readJsonObject(options.bodyFile),\n categoryIds: commaSeparated(options.categoryIds),\n tagIds: commaSeparated(options.tagIds),\n bodyFile: undefined,\n });\n });\n\n posts\n .command(\"update\")\n .description(\"기존 글의 일부 필드 수정\")\n .argument(\"<post-id>\", \"글 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--title <title>\", \"글 제목\")\n .option(\"--slug <slug>\", \"URL 슬러그\")\n .option(\"--body-file <path>\", \"Tiptap 본문 JSON 파일\")\n .option(\"--excerpt <text>\", \"요약\")\n .option(\"--featured-media-id <id>\", \"썸네일 미디어 ID\")\n .option(\"--clear-featured-media\", \"썸네일 연결 해제\")\n .option(\"--scheduled-at <iso>\", \"예약 발행 ISO 시각\")\n .option(\"--clear-schedule\", \"예약 해제\")\n .action(async (postId: string, options) => {\n await run(\"updateCmsPost\", {\n postId,\n siteId: options.siteId,\n title: options.title,\n slug: options.slug,\n bodyJson: options.bodyFile\n ? await readJsonObject(options.bodyFile)\n : undefined,\n excerpt: options.excerpt,\n featuredMediaId: options.clearFeaturedMedia\n ? null\n : options.featuredMediaId,\n scheduledAt: options.clearSchedule ? null : options.scheduledAt,\n });\n });\n\n lifecycle(posts, \"publish\", \"글을 즉시 발행\", \"publishCmsPost\");\n lifecycle(posts, \"unpublish\", \"발행 글을 초안으로 전환\", \"unpublishCmsPost\");\n\n posts\n .command(\"set-terms\")\n .description(\"글의 카테고리 또는 태그 전체 교체\")\n .argument(\"<post-id>\", \"글 ID\")\n .requiredOption(\"--taxonomy <type>\", \"category 또는 tag\")\n .requiredOption(\"--term-ids <ids>\", \"쉼표로 구분한 term ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .action(async (postId: string, options) => {\n await run(\"setCmsPostTerms\", {\n postId,\n siteId: options.siteId,\n taxonomy: options.taxonomy,\n termIds: commaSeparated(options.termIds) ?? [],\n });\n });\n}\n\nfunction registerMediaCommands(cli: Command): void {\n const media = cli.command(\"media\").description(\"CMS 미디어 자동화\");\n\n media\n .command(\"list\")\n .description(\"미디어 라이브러리 조회\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--limit <number>\", \"조회 개수\", integer, 20)\n .option(\"--offset <number>\", \"조회 시작 위치\", integer, 0)\n .action(async (options) => {\n await run(\"listCmsMedia\", options);\n });\n\n media\n .command(\"upload\")\n .description(\"로컬 파일을 업로드하고 CMS 미디어로 등록\")\n .argument(\"<file>\", \"jpg, png, webp, gif 또는 pdf 파일\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--content-type <type>\", \"MIME type 직접 지정\")\n .option(\"--alt <text>\", \"대체 텍스트\")\n .option(\"--caption <text>\", \"캡션\")\n .action(async (filePath: string, options) => {\n await run(\"uploadCmsMedia\", { filePath, ...options });\n });\n\n media\n .command(\"update\")\n .description(\"미디어 대체 텍스트 또는 캡션 수정\")\n .argument(\"<media-id>\", \"미디어 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--alt <text>\", \"대체 텍스트\")\n .option(\"--caption <text>\", \"캡션\")\n .action(async (mediaId: string, options) => {\n await run(\"updateCmsMedia\", { mediaId, ...options });\n });\n\n media\n .command(\"delete\")\n .description(\"미디어 파일과 라이브러리 항목 삭제\")\n .argument(\"<media-id>\", \"미디어 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .action(async (mediaId: string, options: CommonOptions) => {\n await run(\"deleteCmsMedia\", { mediaId, ...options });\n });\n}\n\nfunction lifecycle(\n posts: Command,\n commandName: string,\n description: string,\n toolName: \"publishCmsPost\" | \"unpublishCmsPost\",\n): void {\n posts\n .command(commandName)\n .description(description)\n .argument(\"<post-id>\", \"글 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .action(async (postId: string, options: CommonOptions) => {\n await run(toolName, { postId, ...options });\n });\n}\n\nasync function run(\n name: Parameters<typeof callAutomationTool>[0],\n args: Record<string, unknown>,\n): Promise<void> {\n const definedArgs = Object.fromEntries(\n Object.entries(args).filter(([, value]) => value !== undefined),\n );\n const result = await callAutomationTool(name, definedArgs);\n process.stdout.write(`${JSON.stringify(result, null, 2)}\\n`);\n}\n\nasync function readJsonObject(filePath: string): Promise<Record<string, unknown>> {\n const parsed: unknown = JSON.parse(await readFile(filePath, \"utf8\"));\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new InvalidArgumentError(\"본문 파일은 Tiptap JSON 객체여야 합니다.\");\n }\n return parsed as Record<string, unknown>;\n}\n\nfunction commaSeparated(value: string | undefined): string[] | undefined {\n if (value === undefined) return undefined;\n return value.split(\",\").map((item) => item.trim()).filter(Boolean);\n}\n\nfunction integer(value: string): number {\n const parsed = Number.parseInt(value, 10);\n if (!Number.isInteger(parsed) || parsed < 0) {\n throw new InvalidArgumentError(\"0 이상의 정수를 입력하세요.\");\n }\n return parsed;\n}\n","import ky from \"ky\";\n\nconst DEFAULT_API_BASE = \"https://api.roottale.com\";\n\nexport class CmsAutomationError extends Error {\n readonly name = \"CmsAutomationError\";\n}\n\nexport type ApiRequest = {\n readonly method: \"GET\" | \"POST\" | \"PATCH\" | \"DELETE\";\n readonly pathname: string;\n readonly query?: Readonly<Record<string, string | undefined>>;\n readonly body?: unknown;\n};\n\nfunction apiConfig(): { readonly apiKey: string; readonly baseUrl: string } {\n const apiKey = process.env.ROOTTALE_API_KEY;\n if (!apiKey) {\n throw new CmsAutomationError(\n \"ROOTTALE_API_KEY 환경변수가 없습니다. 어드민 설정 > 사이트 연결 키에서 read_write 키를 발급해 MCP env에 추가하세요.\",\n );\n }\n return {\n apiKey,\n baseUrl: (process.env.ROOTTALE_API_BASE ?? DEFAULT_API_BASE).replace(\n /\\/+$/,\n \"\",\n ),\n };\n}\n\nexport async function requestRootTaleApi(\n request: ApiRequest,\n): Promise<unknown> {\n const { apiKey, baseUrl } = apiConfig();\n const url = new URL(`${baseUrl}${request.pathname}`);\n for (const [key, value] of Object.entries(request.query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, value);\n }\n return ky(url, {\n method: request.method,\n headers: { authorization: `Bearer ${apiKey}` },\n json: request.body,\n retry: 0,\n timeout: 30_000,\n }).json();\n}\n\nexport async function putUpload(\n uploadUrl: string,\n bytes: Uint8Array,\n contentType: string,\n): Promise<void> {\n const body = new Uint8Array(bytes.byteLength);\n body.set(bytes);\n await ky.put(uploadUrl, {\n body: body.buffer,\n headers: { \"content-type\": contentType },\n retry: 0,\n timeout: 120_000,\n });\n}\n","type JsonSchemaProperty = Readonly<Record<string, unknown>>;\n\nexport const AUTOMATION_TOOL_DEFINITIONS = [\n tool(\"listManagedCmsPosts\", \"초안·예약·발행 글을 포함한 관리용 글 목록을 조회합니다.\", {\n siteId: stringProperty(\"사이트 ID\"),\n limit: numberProperty(\"1-100\"),\n type: enumProperty([\"post\", \"page\"]),\n status: enumProperty([\"draft\", \"published\", \"scheduled\", \"private\"]),\n cursor: stringProperty(\"이전 응답의 next_cursor\"),\n }),\n tool(\n \"createCmsPost\",\n \"Tiptap bodyJson으로 글 또는 페이지를 생성합니다. 기본은 초안이며 publish=true면 즉시 발행합니다.\",\n {\n siteId: stringProperty(\"사이트 ID\"),\n type: enumProperty([\"post\", \"page\"]),\n title: stringProperty(\"제목\"),\n slug: stringProperty(\"슬러그\"),\n bodyJson: { type: \"object\", description: \"Tiptap 문서 JSON\" },\n excerpt: nullableStringProperty(\"요약\"),\n featuredMediaId: nullableStringProperty(\"대표 이미지 media id\"),\n publish: { type: \"boolean\" },\n scheduledAt: stringProperty(\"미래 ISO 8601 발행 시각\"),\n categoryIds: arrayProperty(\"카테고리 term id\"),\n tagIds: arrayProperty(\"태그 term id\"),\n },\n [\"title\", \"slug\", \"bodyJson\"],\n ),\n tool(\"updateCmsPost\", \"기존 CMS 글을 부분 수정합니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n title: stringProperty(\"제목\"),\n slug: stringProperty(\"슬러그\"),\n bodyJson: { type: \"object\", description: \"Tiptap 문서 JSON\" },\n excerpt: nullableStringProperty(\"요약\"),\n featuredMediaId: nullableStringProperty(\"대표 이미지 media id\"),\n scheduledAt: nullableStringProperty(\"예약 시각\"),\n }, [\"postId\"]),\n tool(\"publishCmsPost\", \"초안 또는 예약 글을 즉시 발행하고 웹훅을 전송합니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n }, [\"postId\"]),\n tool(\"unpublishCmsPost\", \"발행 글을 초안으로 되돌립니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n }, [\"postId\"]),\n tool(\"setCmsPostTerms\", \"글의 카테고리 또는 태그 연결을 교체합니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n taxonomy: enumProperty([\"category\", \"tag\"]),\n termIds: arrayProperty(\"term id\"),\n }, [\"postId\", \"taxonomy\", \"termIds\"]),\n tool(\"listCmsMedia\", \"CMS 미디어 라이브러리를 조회합니다.\", {\n siteId: stringProperty(\"사이트 ID\"),\n limit: numberProperty(\"1-100\"),\n offset: numberProperty(\"0 이상\"),\n }),\n tool(\n \"uploadCmsMedia\",\n \"로컬 이미지/PDF 파일을 업로드하고 CMS media id와 공개 URL을 반환합니다.\",\n {\n filePath: stringProperty(\"로컬 파일 절대/상대 경로\"),\n siteId: stringProperty(\"사이트 ID\"),\n contentType: enumProperty([\n \"image/jpeg\",\n \"image/png\",\n \"image/webp\",\n \"image/gif\",\n \"application/pdf\",\n ]),\n alt: nullableStringProperty(\"대체 텍스트\"),\n caption: nullableStringProperty(\"캡션\"),\n },\n [\"filePath\"],\n ),\n tool(\"updateCmsMedia\", \"CMS 미디어의 대체 텍스트 또는 캡션을 수정합니다.\", {\n mediaId: stringProperty(\"media id\"),\n siteId: stringProperty(\"사이트 ID\"),\n alt: nullableStringProperty(\"대체 텍스트\"),\n caption: nullableStringProperty(\"캡션\"),\n }, [\"mediaId\"]),\n tool(\"deleteCmsMedia\", \"CMS 미디어를 R2와 라이브러리에서 삭제합니다.\", {\n mediaId: stringProperty(\"media id\"),\n siteId: stringProperty(\"사이트 ID\"),\n }, [\"mediaId\"]),\n] as const;\n\nexport type AutomationToolName =\n (typeof AUTOMATION_TOOL_DEFINITIONS)[number][\"name\"];\n\nexport function isAutomationToolName(name: string): name is AutomationToolName {\n return AUTOMATION_TOOL_DEFINITIONS.some(\n (toolDefinition) => toolDefinition.name === name,\n );\n}\n\nfunction tool<const TName extends string>(\n name: TName,\n description: string,\n properties: Readonly<Record<string, JsonSchemaProperty>>,\n required: readonly string[] = [],\n) {\n return {\n name,\n description,\n inputSchema: {\n type: \"object\",\n properties,\n ...(required.length > 0 ? { required } : {}),\n additionalProperties: false,\n },\n } as const;\n}\n\nfunction stringProperty(description: string): JsonSchemaProperty {\n return { type: \"string\", description };\n}\n\nfunction nullableStringProperty(description: string): JsonSchemaProperty {\n return { type: [\"string\", \"null\"], description };\n}\n\nfunction numberProperty(description: string): JsonSchemaProperty {\n return { type: \"number\", description };\n}\n\nfunction enumProperty(values: readonly string[]): JsonSchemaProperty {\n return { type: \"string\", enum: values };\n}\n\nfunction arrayProperty(description: string): JsonSchemaProperty {\n return { type: \"array\", items: { type: \"string\" }, description };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { basename, extname, resolve } from \"node:path\";\n\nimport { z } from \"zod\";\n\nimport { CmsAutomationError, putUpload, requestRootTaleApi } from \"./api.js\";\nimport { UploadMediaInput } from \"./automation-schemas.js\";\n\nexport async function uploadCmsMedia(args: Record<string, unknown>): Promise<unknown> {\n const input = UploadMediaInput.parse(args);\n const filePath = resolve(input.filePath);\n const bytes = await readFile(filePath);\n const contentType = input.contentType ?? inferContentType(filePath);\n if (!contentType) {\n throw new CmsAutomationError(\n \"contentType을 추론할 수 없습니다. jpg/png/webp/gif/pdf 파일이 아니면 contentType을 지정하세요.\",\n );\n }\n const requested = z\n .object({\n upload_url: z.string().url(),\n r2_key: z.string().min(1),\n })\n .parse(\n await requestRootTaleApi({\n method: \"POST\",\n pathname: \"/v1/cms/media/uploads\",\n body: {\n site_id: input.siteId,\n original_name: basename(filePath),\n content_type: contentType,\n size_bytes: bytes.byteLength,\n },\n }),\n );\n await putUpload(requested.upload_url, bytes, contentType);\n return requestRootTaleApi({\n method: \"POST\",\n pathname: \"/v1/cms/media/uploads/complete\",\n body: {\n site_id: input.siteId,\n r2_key: requested.r2_key,\n original_name: basename(filePath),\n content_type: contentType,\n size_bytes: bytes.byteLength,\n alt: input.alt,\n caption: input.caption,\n },\n });\n}\n\nfunction inferContentType(filePath: string): string | undefined {\n switch (extname(filePath).toLowerCase()) {\n case \".jpg\":\n case \".jpeg\":\n return \"image/jpeg\";\n case \".png\":\n return \"image/png\";\n case \".webp\":\n return \"image/webp\";\n case \".gif\":\n return \"image/gif\";\n case \".pdf\":\n return \"application/pdf\";\n default:\n return undefined;\n }\n}\n","import { z } from \"zod\";\n\nconst SiteId = z.string().min(1).optional();\nconst PostId = z.string().min(1);\nconst NullableText = z.string().nullable().optional();\n\nexport const CreatePostInput = z\n .object({\n siteId: SiteId,\n type: z.enum([\"post\", \"page\"]).optional(),\n title: z.string().min(1).max(500),\n slug: z.string().min(1).max(120),\n bodyJson: z.record(z.string(), z.unknown()),\n excerpt: z.string().max(1000).nullable().optional(),\n featuredMediaId: z.string().nullable().optional(),\n publish: z.boolean().optional(),\n scheduledAt: z.string().datetime({ offset: true }).optional(),\n categoryIds: z.array(z.string()).optional(),\n tagIds: z.array(z.string()).optional(),\n })\n .strict();\n\nexport const UpdatePostInput = z\n .object({\n postId: PostId,\n siteId: SiteId,\n title: z.string().min(1).max(500).optional(),\n slug: z.string().min(1).max(120).optional(),\n bodyJson: z.record(z.string(), z.unknown()).optional(),\n excerpt: NullableText,\n featuredMediaId: z.string().nullable().optional(),\n scheduledAt: z.string().datetime({ offset: true }).nullable().optional(),\n })\n .strict();\n\nexport const PostLifecycleInput = z\n .object({ postId: PostId, siteId: SiteId })\n .strict();\n\nexport const ListManagedPostsInput = z\n .object({\n siteId: SiteId,\n limit: z.number().int().min(1).max(100).optional(),\n type: z.enum([\"post\", \"page\"]).optional(),\n status: z\n .enum([\"draft\", \"published\", \"scheduled\", \"private\"])\n .optional(),\n cursor: z.string().optional(),\n })\n .strict();\n\nexport const SetTermsInput = z\n .object({\n postId: PostId,\n siteId: SiteId,\n taxonomy: z.enum([\"category\", \"tag\"]),\n termIds: z.array(z.string()),\n })\n .strict();\n\nexport const UploadMediaInput = z\n .object({\n filePath: z.string().min(1),\n siteId: SiteId,\n contentType: z\n .enum([\n \"image/jpeg\",\n \"image/png\",\n \"image/webp\",\n \"image/gif\",\n \"application/pdf\",\n ])\n .optional(),\n alt: NullableText,\n caption: NullableText,\n })\n .strict();\n\nexport const ListMediaInput = z\n .object({\n siteId: SiteId,\n limit: z.number().int().min(1).max(100).optional(),\n offset: z.number().int().nonnegative().optional(),\n })\n .strict();\n\nexport const UpdateMediaInput = z\n .object({\n mediaId: z.string().min(1),\n siteId: SiteId,\n alt: NullableText,\n caption: NullableText,\n })\n .strict()\n .refine((input) => input.alt !== undefined || input.caption !== undefined, {\n message: \"alt 또는 caption 중 하나 이상이 필요합니다.\",\n });\n\nexport const DeleteMediaInput = z\n .object({ mediaId: z.string().min(1), siteId: SiteId })\n .strict();\n","import { CmsAutomationError, requestRootTaleApi } from \"./api.js\";\nimport {\n type AutomationToolName,\n AUTOMATION_TOOL_DEFINITIONS,\n isAutomationToolName,\n} from \"./automation-definitions.js\";\nimport { uploadCmsMedia } from \"./automation-media.js\";\nimport {\n CreatePostInput,\n DeleteMediaInput,\n ListManagedPostsInput,\n ListMediaInput,\n PostLifecycleInput,\n SetTermsInput,\n UpdateMediaInput,\n UpdatePostInput,\n} from \"./automation-schemas.js\";\n\nexport {\n AUTOMATION_TOOL_DEFINITIONS,\n isAutomationToolName,\n};\n\nexport async function callAutomationTool(\n name: AutomationToolName,\n args: Record<string, unknown>,\n): Promise<unknown> {\n switch (name) {\n case \"listManagedCmsPosts\": {\n const input = ListManagedPostsInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/posts\",\n query: optionalQuery({\n site_id: input.siteId,\n limit: input.limit,\n type: input.type,\n status: input.status,\n cursor: input.cursor,\n }),\n });\n }\n case \"createCmsPost\": {\n const input = CreatePostInput.parse(args);\n return requestRootTaleApi({\n method: \"POST\",\n pathname: \"/v1/cms/posts\",\n body: {\n site_id: input.siteId,\n type: input.type,\n title: input.title,\n slug: input.slug,\n body_json: input.bodyJson,\n excerpt: input.excerpt,\n featured_media_id: input.featuredMediaId,\n publish: input.publish,\n scheduled_at: input.scheduledAt,\n category_ids: input.categoryIds,\n tag_ids: input.tagIds,\n },\n });\n }\n case \"updateCmsPost\": {\n const input = UpdatePostInput.parse(args);\n return requestRootTaleApi({\n method: \"PATCH\",\n pathname: `/v1/cms/posts/${encodeURIComponent(input.postId)}`,\n body: {\n site_id: input.siteId,\n title: input.title,\n slug: input.slug,\n body_json: input.bodyJson,\n excerpt: input.excerpt,\n featured_media_id: input.featuredMediaId,\n scheduled_at: input.scheduledAt,\n },\n });\n }\n case \"publishCmsPost\":\n case \"unpublishCmsPost\": {\n const input = PostLifecycleInput.parse(args);\n const action = name === \"publishCmsPost\" ? \"publish\" : \"unpublish\";\n return requestRootTaleApi({\n method: \"POST\",\n pathname: `/v1/cms/posts/${encodeURIComponent(input.postId)}/${action}`,\n body: { site_id: input.siteId },\n });\n }\n case \"setCmsPostTerms\": {\n const input = SetTermsInput.parse(args);\n return requestRootTaleApi({\n method: \"PATCH\",\n pathname: `/v1/cms/posts/${encodeURIComponent(input.postId)}/terms`,\n body: {\n site_id: input.siteId,\n taxonomy: input.taxonomy,\n term_ids: input.termIds,\n },\n });\n }\n case \"listCmsMedia\": {\n const input = ListMediaInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/media\",\n query: optionalQuery({\n site_id: input.siteId,\n limit: input.limit,\n offset: input.offset,\n }),\n });\n }\n case \"uploadCmsMedia\":\n return uploadCmsMedia(args);\n case \"updateCmsMedia\": {\n const input = UpdateMediaInput.parse(args);\n return requestRootTaleApi({\n method: \"PATCH\",\n pathname: `/v1/cms/media/${encodeURIComponent(input.mediaId)}`,\n body: {\n site_id: input.siteId,\n alt: input.alt,\n caption: input.caption,\n },\n });\n }\n case \"deleteCmsMedia\": {\n const input = DeleteMediaInput.parse(args);\n return requestRootTaleApi({\n method: \"DELETE\",\n pathname: `/v1/cms/media/${encodeURIComponent(input.mediaId)}`,\n query: optionalQuery({ site_id: input.siteId }),\n });\n }\n default:\n return assertNever(name);\n }\n}\n\nfunction optionalQuery(\n values: Readonly<Record<string, string | number | undefined>>,\n): Record<string, string> {\n return Object.fromEntries(\n Object.entries(values)\n .filter((entry): entry is [string, string | number] => entry[1] !== undefined)\n .map(([key, value]) => [key, String(value)]),\n );\n}\n\nfunction assertNever(value: never): never {\n throw new CmsAutomationError(`Unknown automation tool: ${String(value)}`);\n}\n","import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\n\nimport { registerTools } from \"./tools.js\";\n\n// tsup define 으로 빌드타임 주입 — 비번들 실행(vitest 등)에서는 \"dev\".\ndeclare const __CMS_MCP_VERSION__: string | undefined;\nconst VERSION =\n typeof __CMS_MCP_VERSION__ === \"string\" ? __CMS_MCP_VERSION__ : \"dev\";\n\nexport const SERVER_INSTRUCTIONS = `\nroottale-cms-mcp는 RootTale CMS를 외부 사이트(주로 Next.js)에 연동하고\n글·썸네일·본문 이미지를 자동화하기 위한 문서·예시 코드·API tool을 제공합니다.\n\n지켜야 할 규칙:\n- RootTale 연동 관련 작업은 가장 먼저 listRootTaleDocs를 호출해 문서 목록을 파악하세요.\n- 연동 코드를 작성하기 전 반드시 readRootTaleNextjsExampleCode를 호출해 예시 코드를\n 참고한 후 그 패턴을 따라 작성하세요.\n- API 키(rtlk_cust_*)는 서버 전용입니다. NEXT_PUBLIC_* 등 브라우저로 노출되는\n 환경변수에 절대 넣지 마세요.\n- 글쓰기 전에 getSiteKnowledge로 브랜드 보이스와 용어 규칙을 확인하세요.\n- 새 글은 기본적으로 draft로 만들고, 사용자가 즉시 발행을 명시했을 때만\n publish=true 또는 publishCmsPost를 사용하세요.\n- 이미지는 uploadCmsMedia로 먼저 등록한 뒤 반환된 id를 featuredMediaId에,\n 반환된 URL을 Tiptap image 노드의 attrs.src에 사용하세요.\n- 이미 학습된 내용이더라도 본 서버의 문서로 더블체크 후 작업하세요.\n`.trim();\n\nexport function createServer(): Server {\n const server = new Server(\n { name: \"roottale-cms\", version: VERSION },\n { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS },\n );\n registerTools(server);\n return server;\n}\n","import type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport {\n CallToolRequestSchema,\n ErrorCode,\n ListToolsRequestSchema,\n McpError,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\n\nimport {\n AUTOMATION_TOOL_DEFINITIONS,\n callAutomationTool,\n isAutomationToolName,\n} from \"./automation-tools.js\";\nimport { requestRootTaleApi } from \"./api.js\";\nimport { listDocs, readDoc, readNextjsExampleCode, searchDocs } from \"./docs.js\";\n\nconst ReadDocInput = z.object({ path: z.string().min(1) }).strict();\nconst SearchDocsInput = z.object({ query: z.string().min(1) }).strict();\nconst ListPostsInput = z\n .object({\n limit: z.number().int().min(1).max(100).optional(),\n type: z.enum([\"post\", \"page\"]).optional(),\n cursor: z.string().optional(),\n })\n .strict();\nconst GetPostInput = z.object({ slugOrId: z.string().min(1) }).strict();\nconst GetSiteKnowledgeInput = z\n .object({ siteId: z.string().optional() })\n .strict();\n\nconst DOCUMENTATION_TOOL_DEFINITIONS = [\n {\n name: \"listRootTaleDocs\",\n description:\n \"RootTale CMS 통합 문서 전체 목록(경로·제목·설명)을 반환합니다. \" +\n \"RootTale 연동 작업을 시작하기 전 가장 먼저 호출하세요.\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n },\n {\n name: \"readRootTaleDoc\",\n description:\n \"RootTale CMS 통합 문서 1개를 경로로 읽습니다. \" +\n \"경로는 listRootTaleDocs 가 반환한 path 값을 사용하세요.\",\n inputSchema: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"문서 경로 (예: blog.md)\" },\n },\n required: [\"path\"],\n additionalProperties: false,\n },\n },\n {\n name: \"searchRootTaleDocs\",\n description:\n \"RootTale CMS 통합 문서 전체에서 정규식으로 검색합니다 (대소문자 무시). \" +\n \"매치된 파일 경로·라인 번호·라인 내용을 반환합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n query: { type: \"string\", description: \"정규식 패턴 (예: revalidate|webhook)\" },\n },\n required: [\"query\"],\n additionalProperties: false,\n },\n },\n {\n name: \"readRootTaleNextjsExampleCode\",\n description:\n \"RootTale CMS를 외부 Next.js(App Router) 사이트에 연동하는 완전한 예시 코드 세트를 반환합니다 \" +\n \"(블로그 목록/상세, 웹훅 revalidate 라우트, RSS/사이트맵, 상담문의 서버 액션 등). \" +\n \"연동 코드를 작성하기 전 반드시 호출해 패턴을 따라 작성하세요.\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n },\n {\n name: \"listPublishedPosts\",\n description:\n \"연동 검증용 — RootTale 공개 CMS API(GET /v1/cms/public/posts)로 발행된 글 목록을 조회합니다. \" +\n \"환경변수 ROOTTALE_API_KEY(rtlk_cust_*)가 필요합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n limit: { type: \"number\", description: \"1-100, 기본 20\" },\n type: { type: \"string\", enum: [\"post\", \"page\"] },\n cursor: { type: \"string\", description: \"이전 응답의 next_cursor\" },\n },\n additionalProperties: false,\n },\n },\n {\n name: \"getPublishedPost\",\n description:\n \"연동 검증용 — RootTale 공개 CMS API(GET /v1/cms/public/posts/:identifier)로 발행된 글 1개를 \" +\n \"slug 또는 UUID로 조회합니다. 환경변수 ROOTTALE_API_KEY(rtlk_cust_*)가 필요합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n slugOrId: { type: \"string\", description: \"글 slug 또는 UUID\" },\n },\n required: [\"slugOrId\"],\n additionalProperties: false,\n },\n },\n {\n name: \"getSiteKnowledge\",\n description:\n \"이 RootTale 사이트의 브랜드 보이스(어조·톤·화자)와 용어 규칙(금지어·교정어)을 조회합니다 \" +\n \"(GET /v1/cms/public/site-knowledge). 이 사이트에 맞는 글을 쓰기 전에 호출해 톤·표현을 맞추세요. \" +\n \"금지어는 쓰지 말고, 교정어는 권장 표현으로 바꿔 쓰세요. 환경변수 ROOTTALE_API_KEY(rtlk_cust_*)가 필요합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n siteId: {\n type: \"string\",\n description: \"사이트 ID (선택, 미설정 시 기본 사이트)\",\n },\n },\n additionalProperties: false,\n },\n },\n] as const;\n\nexport const TOOL_DEFINITIONS = [\n ...DOCUMENTATION_TOOL_DEFINITIONS,\n ...AUTOMATION_TOOL_DEFINITIONS,\n] as const;\n\nfunction textResponse(text: string) {\n return { content: [{ type: \"text\" as const, text }] };\n}\n\nfunction jsonResponse(value: unknown) {\n return textResponse(JSON.stringify(value, null, 2));\n}\n\nexport function registerTools(server: Server): void {\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: TOOL_DEFINITIONS.map((t) => ({ ...t })),\n }));\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const args = (request.params.arguments ?? {}) as Record<string, unknown>;\n try {\n if (isAutomationToolName(request.params.name)) {\n return jsonResponse(\n await callAutomationTool(request.params.name, args),\n );\n }\n switch (request.params.name) {\n case \"listRootTaleDocs\":\n return jsonResponse(await listDocs());\n case \"readRootTaleDoc\": {\n const input = ReadDocInput.parse(args);\n const content = await readDoc(input.path);\n if (content === null) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `문서를 찾을 수 없습니다: ${input.path} — listRootTaleDocs로 경로를 확인하세요.`,\n );\n }\n return textResponse(content);\n }\n case \"searchRootTaleDocs\": {\n const input = SearchDocsInput.parse(args);\n return jsonResponse(await searchDocs(input.query));\n }\n case \"readRootTaleNextjsExampleCode\":\n return textResponse(await readNextjsExampleCode());\n case \"listPublishedPosts\": {\n const input = ListPostsInput.parse(args);\n const params: Record<string, string> = {};\n if (input.limit) params.limit = String(input.limit);\n if (input.type) params.type = input.type;\n if (input.cursor) params.cursor = input.cursor;\n return jsonResponse(\n await requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/public/posts\",\n query: params,\n }),\n );\n }\n case \"getPublishedPost\": {\n const input = GetPostInput.parse(args);\n return jsonResponse(\n await requestRootTaleApi({\n method: \"GET\",\n pathname: `/v1/cms/public/posts/${encodeURIComponent(input.slugOrId)}`,\n }),\n );\n }\n case \"getSiteKnowledge\": {\n const input = GetSiteKnowledgeInput.parse(args);\n const params: Record<string, string> = {};\n if (input.siteId) params.site_id = input.siteId;\n return jsonResponse(\n await requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/public/site-knowledge\",\n query: params,\n }),\n );\n }\n default:\n throw new McpError(\n ErrorCode.InvalidParams,\n `Unknown RootTale tool: ${request.params.name}`,\n );\n }\n } catch (error) {\n if (error instanceof McpError) throw error;\n const message = error instanceof Error ? error.message : String(error);\n return { isError: true, content: [{ type: \"text\" as const, text: message }] };\n }\n });\n}\n","/**\n * 패키지에 번들된 통합 문서(`docs/`)·예시 코드(`examples/`) 로더.\n *\n * dist/index.js 와 src/docs.ts 모두 패키지 루트에서 한 단계 아래이므로\n * `..` 상대 경로 해석이 빌드 전후 동일하다. npm publish 시 `files` 에\n * docs/, examples/ 가 포함되어 npx 환경에서도 같은 구조로 설치된다.\n */\nimport { readdir, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst PKG_ROOT = fileURLToPath(new URL(\"..\", import.meta.url));\nconst DOCS_ROOT = path.join(PKG_ROOT, \"docs\");\nconst EXAMPLES_ROOT = path.join(PKG_ROOT, \"examples\");\n\nexport interface DocEntry {\n /** docs/ 기준 상대 경로 (예: \"blog.md\"). */\n path: string;\n title: string;\n description: string;\n}\n\ninterface Frontmatter {\n title: string;\n description: string;\n}\n\n/** `---\\ntitle: ...\\ndescription: ...\\n---` 형태의 단순 frontmatter 파서. */\nexport function parseFrontmatter(markdown: string): Frontmatter {\n const result: Frontmatter = { title: \"\", description: \"\" };\n const match = markdown.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return result;\n for (const line of match[1].split(\"\\n\")) {\n const idx = line.indexOf(\":\");\n if (idx === -1) continue;\n const key = line.slice(0, idx).trim();\n const value = line.slice(idx + 1).trim();\n if (key === \"title\") result.title = value;\n if (key === \"description\") result.description = value;\n }\n return result;\n}\n\nasync function walkFiles(root: string, dir = root): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n const files: string[] = [];\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n files.push(...(await walkFiles(root, full)));\n } else {\n files.push(path.relative(root, full));\n }\n }\n return files.sort();\n}\n\nexport async function listDocs(): Promise<DocEntry[]> {\n const files = (await walkFiles(DOCS_ROOT)).filter((f) => f.endsWith(\".md\"));\n return Promise.all(\n files.map(async (rel) => {\n const content = await readFile(path.join(DOCS_ROOT, rel), \"utf8\");\n const fm = parseFrontmatter(content);\n return { path: rel, title: fm.title || rel, description: fm.description };\n }),\n );\n}\n\n/** 경로 탈출(../) 방지 후 단일 문서 읽기. 없으면 null. */\nexport async function readDoc(relPath: string): Promise<string | null> {\n const resolved = path.resolve(DOCS_ROOT, relPath);\n if (!resolved.startsWith(DOCS_ROOT + path.sep)) return null;\n try {\n return await readFile(resolved, \"utf8\");\n } catch {\n return null;\n }\n}\n\nexport interface SearchMatch {\n path: string;\n line: number;\n text: string;\n}\n\n/** 모든 문서에 대해 정규식 검색 (대소문자 무시). 매치 라인을 반환. */\nexport async function searchDocs(query: string): Promise<SearchMatch[]> {\n const regex = new RegExp(query, \"i\");\n const matches: SearchMatch[] = [];\n for (const doc of await listDocs()) {\n const content = await readDoc(doc.path);\n if (!content) continue;\n content.split(\"\\n\").forEach((text, idx) => {\n if (regex.test(text)) {\n matches.push({ path: doc.path, line: idx + 1, text: text.trim() });\n }\n });\n }\n return matches;\n}\n\n/**\n * Next.js 통합 예시 코드 전체를 `--- FILE: <path> ---` 헤더로 이어붙여 반환.\n * 파일 수가 적어(통합 1세트) 한 번에 주는 쪽이 에이전트 왕복을 줄인다.\n */\nexport async function readNextjsExampleCode(): Promise<string> {\n const root = path.join(EXAMPLES_ROOT, \"nextjs\");\n const files = await walkFiles(root);\n const sections = await Promise.all(\n files.map(async (rel) => {\n const content = await readFile(path.join(root, rel), \"utf8\");\n return `--- FILE: ${rel} ---\\n${content}`;\n }),\n );\n return sections.join(\"\\n\\n\");\n}\n"],"mappings":"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","names":["readFile","z","z","readFile","z","readFile","z"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/cli.ts","../src/api.ts","../src/automation-definitions.ts","../src/automation-media.ts","../src/automation-schemas.ts","../src/automation-tools.ts","../src/server.ts","../src/tools.ts","../src/docs.ts","../src/seo-tools.ts","../src/seo-definitions.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport { runCmsCli } from \"./cli.js\";\nimport { createServer } from \"./server.js\";\n\nif (process.argv[2] === \"cli\") {\n await runCmsCli(process.argv);\n} else {\n const server = createServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n","import { readFile } from \"node:fs/promises\";\n\nimport { Command, InvalidArgumentError } from \"commander\";\n\nimport { callAutomationTool } from \"./automation-tools.js\";\n\ntype CommonOptions = { readonly siteId?: string };\n\nexport function createCmsCli(): Command {\n const cli = new Command()\n .name(\"roottale-cms\")\n .description(\"RootTale CMS 글·썸네일·본문 이미지 자동화 CLI\");\n\n registerPostCommands(cli);\n registerMediaCommands(cli);\n return cli;\n}\n\nexport async function runCmsCli(argv: readonly string[]): Promise<void> {\n try {\n await createCmsCli().parseAsync([\n argv[0] ?? \"node\",\n \"roottale-cms\",\n ...argv.slice(3),\n ]);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`${message}\\n`);\n process.exitCode = 1;\n }\n}\n\nfunction registerPostCommands(cli: Command): void {\n const posts = cli.command(\"posts\").description(\"CMS 글 자동화\");\n\n posts\n .command(\"list\")\n .description(\"초안·예약·발행 글 목록 조회\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--limit <number>\", \"조회 개수\", integer, 20)\n .option(\"--cursor <cursor>\", \"다음 페이지 커서\")\n .option(\"--type <type>\", \"post 또는 page\")\n .option(\"--status <status>\", \"draft, published, scheduled, private\")\n .action(async (options) => {\n await run(\"listManagedCmsPosts\", options);\n });\n\n posts\n .command(\"create\")\n .description(\"Tiptap JSON 본문으로 새 글 생성\")\n .requiredOption(\"--title <title>\", \"글 제목\")\n .requiredOption(\"--slug <slug>\", \"URL 슬러그\")\n .requiredOption(\"--body-file <path>\", \"Tiptap 본문 JSON 파일\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--type <type>\", \"post 또는 page\", \"post\")\n .option(\"--excerpt <text>\", \"요약\")\n .option(\"--featured-media-id <id>\", \"썸네일 미디어 ID\")\n .option(\"--publish\", \"즉시 발행\")\n .option(\"--scheduled-at <iso>\", \"예약 발행 ISO 시각\")\n .option(\"--category-ids <ids>\", \"쉼표로 구분한 카테고리 ID\")\n .option(\"--tag-ids <ids>\", \"쉼표로 구분한 태그 ID\")\n .action(async (options) => {\n await run(\"createCmsPost\", {\n ...options,\n bodyJson: await readJsonObject(options.bodyFile),\n categoryIds: commaSeparated(options.categoryIds),\n tagIds: commaSeparated(options.tagIds),\n bodyFile: undefined,\n });\n });\n\n posts\n .command(\"update\")\n .description(\"기존 글의 일부 필드 수정\")\n .argument(\"<post-id>\", \"글 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--title <title>\", \"글 제목\")\n .option(\"--slug <slug>\", \"URL 슬러그\")\n .option(\"--body-file <path>\", \"Tiptap 본문 JSON 파일\")\n .option(\"--excerpt <text>\", \"요약\")\n .option(\"--featured-media-id <id>\", \"썸네일 미디어 ID\")\n .option(\"--clear-featured-media\", \"썸네일 연결 해제\")\n .option(\"--scheduled-at <iso>\", \"예약 발행 ISO 시각\")\n .option(\"--clear-schedule\", \"예약 해제\")\n .action(async (postId: string, options) => {\n await run(\"updateCmsPost\", {\n postId,\n siteId: options.siteId,\n title: options.title,\n slug: options.slug,\n bodyJson: options.bodyFile\n ? await readJsonObject(options.bodyFile)\n : undefined,\n excerpt: options.excerpt,\n featuredMediaId: options.clearFeaturedMedia\n ? null\n : options.featuredMediaId,\n scheduledAt: options.clearSchedule ? null : options.scheduledAt,\n });\n });\n\n lifecycle(posts, \"publish\", \"글을 즉시 발행\", \"publishCmsPost\");\n lifecycle(posts, \"unpublish\", \"발행 글을 초안으로 전환\", \"unpublishCmsPost\");\n\n posts\n .command(\"set-terms\")\n .description(\"글의 카테고리 또는 태그 전체 교체\")\n .argument(\"<post-id>\", \"글 ID\")\n .requiredOption(\"--taxonomy <type>\", \"category 또는 tag\")\n .requiredOption(\"--term-ids <ids>\", \"쉼표로 구분한 term ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .action(async (postId: string, options) => {\n await run(\"setCmsPostTerms\", {\n postId,\n siteId: options.siteId,\n taxonomy: options.taxonomy,\n termIds: commaSeparated(options.termIds) ?? [],\n });\n });\n}\n\nfunction registerMediaCommands(cli: Command): void {\n const media = cli.command(\"media\").description(\"CMS 미디어 자동화\");\n\n media\n .command(\"list\")\n .description(\"미디어 라이브러리 조회\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--limit <number>\", \"조회 개수\", integer, 20)\n .option(\"--offset <number>\", \"조회 시작 위치\", integer, 0)\n .action(async (options) => {\n await run(\"listCmsMedia\", options);\n });\n\n media\n .command(\"upload\")\n .description(\"로컬 파일을 업로드하고 CMS 미디어로 등록\")\n .argument(\"<file>\", \"jpg, png, webp, gif 또는 pdf 파일\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--content-type <type>\", \"MIME type 직접 지정\")\n .option(\"--alt <text>\", \"대체 텍스트\")\n .option(\"--caption <text>\", \"캡션\")\n .action(async (filePath: string, options) => {\n await run(\"uploadCmsMedia\", { filePath, ...options });\n });\n\n media\n .command(\"update\")\n .description(\"미디어 대체 텍스트 또는 캡션 수정\")\n .argument(\"<media-id>\", \"미디어 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--alt <text>\", \"대체 텍스트\")\n .option(\"--caption <text>\", \"캡션\")\n .action(async (mediaId: string, options) => {\n await run(\"updateCmsMedia\", { mediaId, ...options });\n });\n\n media\n .command(\"delete\")\n .description(\"미디어 파일과 라이브러리 항목 삭제\")\n .argument(\"<media-id>\", \"미디어 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .action(async (mediaId: string, options: CommonOptions) => {\n await run(\"deleteCmsMedia\", { mediaId, ...options });\n });\n}\n\nfunction lifecycle(\n posts: Command,\n commandName: string,\n description: string,\n toolName: \"publishCmsPost\" | \"unpublishCmsPost\",\n): void {\n posts\n .command(commandName)\n .description(description)\n .argument(\"<post-id>\", \"글 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .action(async (postId: string, options: CommonOptions) => {\n await run(toolName, { postId, ...options });\n });\n}\n\nasync function run(\n name: Parameters<typeof callAutomationTool>[0],\n args: Record<string, unknown>,\n): Promise<void> {\n const definedArgs = Object.fromEntries(\n Object.entries(args).filter(([, value]) => value !== undefined),\n );\n const result = await callAutomationTool(name, definedArgs);\n process.stdout.write(`${JSON.stringify(result, null, 2)}\\n`);\n}\n\nasync function readJsonObject(filePath: string): Promise<Record<string, unknown>> {\n const parsed: unknown = JSON.parse(await readFile(filePath, \"utf8\"));\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new InvalidArgumentError(\"본문 파일은 Tiptap JSON 객체여야 합니다.\");\n }\n return parsed as Record<string, unknown>;\n}\n\nfunction commaSeparated(value: string | undefined): string[] | undefined {\n if (value === undefined) return undefined;\n return value.split(\",\").map((item) => item.trim()).filter(Boolean);\n}\n\nfunction integer(value: string): number {\n const parsed = Number.parseInt(value, 10);\n if (!Number.isInteger(parsed) || parsed < 0) {\n throw new InvalidArgumentError(\"0 이상의 정수를 입력하세요.\");\n }\n return parsed;\n}\n","import ky from \"ky\";\n\nconst DEFAULT_API_BASE = \"https://api.roottale.com\";\n\nexport class CmsAutomationError extends Error {\n readonly name = \"CmsAutomationError\";\n}\n\nexport type ApiRequest = {\n readonly method: \"GET\" | \"POST\" | \"PATCH\" | \"DELETE\";\n readonly pathname: string;\n readonly query?: Readonly<Record<string, string | undefined>>;\n readonly body?: unknown;\n};\n\nfunction apiConfig(): { readonly apiKey: string; readonly baseUrl: string } {\n const apiKey = process.env.ROOTTALE_API_KEY;\n if (!apiKey) {\n throw new CmsAutomationError(\n \"ROOTTALE_API_KEY 환경변수가 없습니다. 어드민 설정 > 사이트 연결 키에서 read_write 키를 발급해 MCP env에 추가하세요.\",\n );\n }\n return {\n apiKey,\n baseUrl: (process.env.ROOTTALE_API_BASE ?? DEFAULT_API_BASE).replace(\n /\\/+$/,\n \"\",\n ),\n };\n}\n\nexport async function requestRootTaleApi(\n request: ApiRequest,\n): Promise<unknown> {\n const { apiKey, baseUrl } = apiConfig();\n const url = new URL(`${baseUrl}${request.pathname}`);\n for (const [key, value] of Object.entries(request.query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, value);\n }\n return ky(url, {\n method: request.method,\n headers: { authorization: `Bearer ${apiKey}` },\n json: request.body,\n retry: 0,\n timeout: 30_000,\n }).json();\n}\n\nexport async function putUpload(\n uploadUrl: string,\n bytes: Uint8Array,\n contentType: string,\n): Promise<void> {\n const body = new Uint8Array(bytes.byteLength);\n body.set(bytes);\n await ky.put(uploadUrl, {\n body: body.buffer,\n headers: { \"content-type\": contentType },\n retry: 0,\n timeout: 120_000,\n });\n}\n","type JsonSchemaProperty = Readonly<Record<string, unknown>>;\n\nexport const AUTOMATION_TOOL_DEFINITIONS = [\n tool(\"listManagedCmsPosts\", \"초안·예약·발행 글을 포함한 관리용 글 목록을 조회합니다.\", {\n siteId: stringProperty(\"사이트 ID\"),\n limit: numberProperty(\"1-100\"),\n type: enumProperty([\"post\", \"page\"]),\n status: enumProperty([\"draft\", \"published\", \"scheduled\", \"private\"]),\n cursor: stringProperty(\"이전 응답의 next_cursor\"),\n }),\n tool(\n \"createCmsPost\",\n \"Tiptap bodyJson으로 글 또는 페이지를 생성합니다. 기본은 초안이며 publish=true면 즉시 발행합니다.\",\n {\n siteId: stringProperty(\"사이트 ID\"),\n type: enumProperty([\"post\", \"page\"]),\n title: stringProperty(\"제목\"),\n slug: stringProperty(\"슬러그\"),\n bodyJson: { type: \"object\", description: \"Tiptap 문서 JSON\" },\n excerpt: nullableStringProperty(\"요약\"),\n featuredMediaId: nullableStringProperty(\"대표 이미지 media id\"),\n publish: { type: \"boolean\" },\n scheduledAt: stringProperty(\"미래 ISO 8601 발행 시각\"),\n categoryIds: arrayProperty(\"카테고리 term id\"),\n tagIds: arrayProperty(\"태그 term id\"),\n },\n [\"title\", \"slug\", \"bodyJson\"],\n ),\n tool(\"updateCmsPost\", \"기존 CMS 글을 부분 수정합니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n title: stringProperty(\"제목\"),\n slug: stringProperty(\"슬러그\"),\n bodyJson: { type: \"object\", description: \"Tiptap 문서 JSON\" },\n excerpt: nullableStringProperty(\"요약\"),\n featuredMediaId: nullableStringProperty(\"대표 이미지 media id\"),\n scheduledAt: nullableStringProperty(\"예약 시각\"),\n }, [\"postId\"]),\n tool(\"publishCmsPost\", \"초안 또는 예약 글을 즉시 발행하고 웹훅을 전송합니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n }, [\"postId\"]),\n tool(\"unpublishCmsPost\", \"발행 글을 초안으로 되돌립니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n }, [\"postId\"]),\n tool(\"setCmsPostTerms\", \"글의 카테고리 또는 태그 연결을 교체합니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n taxonomy: enumProperty([\"category\", \"tag\"]),\n termIds: arrayProperty(\"term id\"),\n }, [\"postId\", \"taxonomy\", \"termIds\"]),\n tool(\"listCmsMedia\", \"CMS 미디어 라이브러리를 조회합니다.\", {\n siteId: stringProperty(\"사이트 ID\"),\n limit: numberProperty(\"1-100\"),\n offset: numberProperty(\"0 이상\"),\n }),\n tool(\n \"uploadCmsMedia\",\n \"로컬 이미지/PDF 파일을 업로드하고 CMS media id와 공개 URL을 반환합니다.\",\n {\n filePath: stringProperty(\"로컬 파일 절대/상대 경로\"),\n siteId: stringProperty(\"사이트 ID\"),\n contentType: enumProperty([\n \"image/jpeg\",\n \"image/png\",\n \"image/webp\",\n \"image/gif\",\n \"application/pdf\",\n ]),\n alt: nullableStringProperty(\"대체 텍스트\"),\n caption: nullableStringProperty(\"캡션\"),\n },\n [\"filePath\"],\n ),\n tool(\"updateCmsMedia\", \"CMS 미디어의 대체 텍스트 또는 캡션을 수정합니다.\", {\n mediaId: stringProperty(\"media id\"),\n siteId: stringProperty(\"사이트 ID\"),\n alt: nullableStringProperty(\"대체 텍스트\"),\n caption: nullableStringProperty(\"캡션\"),\n }, [\"mediaId\"]),\n tool(\"deleteCmsMedia\", \"CMS 미디어를 R2와 라이브러리에서 삭제합니다.\", {\n mediaId: stringProperty(\"media id\"),\n siteId: stringProperty(\"사이트 ID\"),\n }, [\"mediaId\"]),\n] as const;\n\nexport type AutomationToolName =\n (typeof AUTOMATION_TOOL_DEFINITIONS)[number][\"name\"];\n\nexport function isAutomationToolName(name: string): name is AutomationToolName {\n return AUTOMATION_TOOL_DEFINITIONS.some(\n (toolDefinition) => toolDefinition.name === name,\n );\n}\n\nfunction tool<const TName extends string>(\n name: TName,\n description: string,\n properties: Readonly<Record<string, JsonSchemaProperty>>,\n required: readonly string[] = [],\n) {\n return {\n name,\n description,\n inputSchema: {\n type: \"object\",\n properties,\n ...(required.length > 0 ? { required } : {}),\n additionalProperties: false,\n },\n } as const;\n}\n\nfunction stringProperty(description: string): JsonSchemaProperty {\n return { type: \"string\", description };\n}\n\nfunction nullableStringProperty(description: string): JsonSchemaProperty {\n return { type: [\"string\", \"null\"], description };\n}\n\nfunction numberProperty(description: string): JsonSchemaProperty {\n return { type: \"number\", description };\n}\n\nfunction enumProperty(values: readonly string[]): JsonSchemaProperty {\n return { type: \"string\", enum: values };\n}\n\nfunction arrayProperty(description: string): JsonSchemaProperty {\n return { type: \"array\", items: { type: \"string\" }, description };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { basename, extname, resolve } from \"node:path\";\n\nimport { z } from \"zod\";\n\nimport { CmsAutomationError, putUpload, requestRootTaleApi } from \"./api.js\";\nimport { UploadMediaInput } from \"./automation-schemas.js\";\n\nexport async function uploadCmsMedia(args: Record<string, unknown>): Promise<unknown> {\n const input = UploadMediaInput.parse(args);\n const filePath = resolve(input.filePath);\n const bytes = await readFile(filePath);\n const contentType = input.contentType ?? inferContentType(filePath);\n if (!contentType) {\n throw new CmsAutomationError(\n \"contentType을 추론할 수 없습니다. jpg/png/webp/gif/pdf 파일이 아니면 contentType을 지정하세요.\",\n );\n }\n const requested = z\n .object({\n upload_url: z.string().url(),\n r2_key: z.string().min(1),\n })\n .parse(\n await requestRootTaleApi({\n method: \"POST\",\n pathname: \"/v1/cms/media/uploads\",\n body: {\n site_id: input.siteId,\n original_name: basename(filePath),\n content_type: contentType,\n size_bytes: bytes.byteLength,\n },\n }),\n );\n await putUpload(requested.upload_url, bytes, contentType);\n return requestRootTaleApi({\n method: \"POST\",\n pathname: \"/v1/cms/media/uploads/complete\",\n body: {\n site_id: input.siteId,\n r2_key: requested.r2_key,\n original_name: basename(filePath),\n content_type: contentType,\n size_bytes: bytes.byteLength,\n alt: input.alt,\n caption: input.caption,\n },\n });\n}\n\nfunction inferContentType(filePath: string): string | undefined {\n switch (extname(filePath).toLowerCase()) {\n case \".jpg\":\n case \".jpeg\":\n return \"image/jpeg\";\n case \".png\":\n return \"image/png\";\n case \".webp\":\n return \"image/webp\";\n case \".gif\":\n return \"image/gif\";\n case \".pdf\":\n return \"application/pdf\";\n default:\n return undefined;\n }\n}\n","import { z } from \"zod\";\n\nconst SiteId = z.string().min(1).optional();\nconst PostId = z.string().min(1);\nconst NullableText = z.string().nullable().optional();\n\nexport const CreatePostInput = z\n .object({\n siteId: SiteId,\n type: z.enum([\"post\", \"page\"]).optional(),\n title: z.string().min(1).max(500),\n slug: z.string().min(1).max(120),\n bodyJson: z.record(z.string(), z.unknown()),\n excerpt: z.string().max(1000).nullable().optional(),\n featuredMediaId: z.string().nullable().optional(),\n publish: z.boolean().optional(),\n scheduledAt: z.string().datetime({ offset: true }).optional(),\n categoryIds: z.array(z.string()).optional(),\n tagIds: z.array(z.string()).optional(),\n })\n .strict();\n\nexport const UpdatePostInput = z\n .object({\n postId: PostId,\n siteId: SiteId,\n title: z.string().min(1).max(500).optional(),\n slug: z.string().min(1).max(120).optional(),\n bodyJson: z.record(z.string(), z.unknown()).optional(),\n excerpt: NullableText,\n featuredMediaId: z.string().nullable().optional(),\n scheduledAt: z.string().datetime({ offset: true }).nullable().optional(),\n })\n .strict();\n\nexport const PostLifecycleInput = z\n .object({ postId: PostId, siteId: SiteId })\n .strict();\n\nexport const ListManagedPostsInput = z\n .object({\n siteId: SiteId,\n limit: z.number().int().min(1).max(100).optional(),\n type: z.enum([\"post\", \"page\"]).optional(),\n status: z\n .enum([\"draft\", \"published\", \"scheduled\", \"private\"])\n .optional(),\n cursor: z.string().optional(),\n })\n .strict();\n\nexport const SetTermsInput = z\n .object({\n postId: PostId,\n siteId: SiteId,\n taxonomy: z.enum([\"category\", \"tag\"]),\n termIds: z.array(z.string()),\n })\n .strict();\n\nexport const UploadMediaInput = z\n .object({\n filePath: z.string().min(1),\n siteId: SiteId,\n contentType: z\n .enum([\n \"image/jpeg\",\n \"image/png\",\n \"image/webp\",\n \"image/gif\",\n \"application/pdf\",\n ])\n .optional(),\n alt: NullableText,\n caption: NullableText,\n })\n .strict();\n\nexport const ListMediaInput = z\n .object({\n siteId: SiteId,\n limit: z.number().int().min(1).max(100).optional(),\n offset: z.number().int().nonnegative().optional(),\n })\n .strict();\n\nexport const UpdateMediaInput = z\n .object({\n mediaId: z.string().min(1),\n siteId: SiteId,\n alt: NullableText,\n caption: NullableText,\n })\n .strict()\n .refine((input) => input.alt !== undefined || input.caption !== undefined, {\n message: \"alt 또는 caption 중 하나 이상이 필요합니다.\",\n });\n\nexport const DeleteMediaInput = z\n .object({ mediaId: z.string().min(1), siteId: SiteId })\n .strict();\n","import { CmsAutomationError, requestRootTaleApi } from \"./api.js\";\nimport {\n type AutomationToolName,\n AUTOMATION_TOOL_DEFINITIONS,\n isAutomationToolName,\n} from \"./automation-definitions.js\";\nimport { uploadCmsMedia } from \"./automation-media.js\";\nimport {\n CreatePostInput,\n DeleteMediaInput,\n ListManagedPostsInput,\n ListMediaInput,\n PostLifecycleInput,\n SetTermsInput,\n UpdateMediaInput,\n UpdatePostInput,\n} from \"./automation-schemas.js\";\n\nexport {\n AUTOMATION_TOOL_DEFINITIONS,\n isAutomationToolName,\n};\n\nexport async function callAutomationTool(\n name: AutomationToolName,\n args: Record<string, unknown>,\n): Promise<unknown> {\n switch (name) {\n case \"listManagedCmsPosts\": {\n const input = ListManagedPostsInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/posts\",\n query: optionalQuery({\n site_id: input.siteId,\n limit: input.limit,\n type: input.type,\n status: input.status,\n cursor: input.cursor,\n }),\n });\n }\n case \"createCmsPost\": {\n const input = CreatePostInput.parse(args);\n return requestRootTaleApi({\n method: \"POST\",\n pathname: \"/v1/cms/posts\",\n body: {\n site_id: input.siteId,\n type: input.type,\n title: input.title,\n slug: input.slug,\n body_json: input.bodyJson,\n excerpt: input.excerpt,\n featured_media_id: input.featuredMediaId,\n publish: input.publish,\n scheduled_at: input.scheduledAt,\n category_ids: input.categoryIds,\n tag_ids: input.tagIds,\n },\n });\n }\n case \"updateCmsPost\": {\n const input = UpdatePostInput.parse(args);\n return requestRootTaleApi({\n method: \"PATCH\",\n pathname: `/v1/cms/posts/${encodeURIComponent(input.postId)}`,\n body: {\n site_id: input.siteId,\n title: input.title,\n slug: input.slug,\n body_json: input.bodyJson,\n excerpt: input.excerpt,\n featured_media_id: input.featuredMediaId,\n scheduled_at: input.scheduledAt,\n },\n });\n }\n case \"publishCmsPost\":\n case \"unpublishCmsPost\": {\n const input = PostLifecycleInput.parse(args);\n const action = name === \"publishCmsPost\" ? \"publish\" : \"unpublish\";\n return requestRootTaleApi({\n method: \"POST\",\n pathname: `/v1/cms/posts/${encodeURIComponent(input.postId)}/${action}`,\n body: { site_id: input.siteId },\n });\n }\n case \"setCmsPostTerms\": {\n const input = SetTermsInput.parse(args);\n return requestRootTaleApi({\n method: \"PATCH\",\n pathname: `/v1/cms/posts/${encodeURIComponent(input.postId)}/terms`,\n body: {\n site_id: input.siteId,\n taxonomy: input.taxonomy,\n term_ids: input.termIds,\n },\n });\n }\n case \"listCmsMedia\": {\n const input = ListMediaInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/media\",\n query: optionalQuery({\n site_id: input.siteId,\n limit: input.limit,\n offset: input.offset,\n }),\n });\n }\n case \"uploadCmsMedia\":\n return uploadCmsMedia(args);\n case \"updateCmsMedia\": {\n const input = UpdateMediaInput.parse(args);\n return requestRootTaleApi({\n method: \"PATCH\",\n pathname: `/v1/cms/media/${encodeURIComponent(input.mediaId)}`,\n body: {\n site_id: input.siteId,\n alt: input.alt,\n caption: input.caption,\n },\n });\n }\n case \"deleteCmsMedia\": {\n const input = DeleteMediaInput.parse(args);\n return requestRootTaleApi({\n method: \"DELETE\",\n pathname: `/v1/cms/media/${encodeURIComponent(input.mediaId)}`,\n query: optionalQuery({ site_id: input.siteId }),\n });\n }\n default:\n return assertNever(name);\n }\n}\n\nfunction optionalQuery(\n values: Readonly<Record<string, string | number | undefined>>,\n): Record<string, string> {\n return Object.fromEntries(\n Object.entries(values)\n .filter((entry): entry is [string, string | number] => entry[1] !== undefined)\n .map(([key, value]) => [key, String(value)]),\n );\n}\n\nfunction assertNever(value: never): never {\n throw new CmsAutomationError(`Unknown automation tool: ${String(value)}`);\n}\n","import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\n\nimport { registerTools } from \"./tools.js\";\n\n// tsup define 으로 빌드타임 주입 — 비번들 실행(vitest 등)에서는 \"dev\".\ndeclare const __CMS_MCP_VERSION__: string | undefined;\nconst VERSION =\n typeof __CMS_MCP_VERSION__ === \"string\" ? __CMS_MCP_VERSION__ : \"dev\";\n\nexport const SERVER_INSTRUCTIONS = `\nroottale-cms-mcp는 RootTale CMS를 외부 사이트(주로 Next.js)에 연동하고\n글·썸네일·본문 이미지를 자동화하기 위한 문서·예시 코드·API tool을 제공합니다.\n\n지켜야 할 규칙:\n- RootTale 연동 관련 작업은 가장 먼저 listRootTaleDocs를 호출해 문서 목록을 파악하세요.\n- 연동 코드를 작성하기 전 반드시 readRootTaleNextjsExampleCode를 호출해 예시 코드를\n 참고한 후 그 패턴을 따라 작성하세요.\n- API 키(rtlk_cust_*)는 서버 전용입니다. NEXT_PUBLIC_* 등 브라우저로 노출되는\n 환경변수에 절대 넣지 마세요.\n- 글쓰기 전에 getSiteKnowledge로 브랜드 보이스와 용어 규칙을 확인하세요.\n- 새 글은 기본적으로 draft로 만들고, 사용자가 즉시 발행을 명시했을 때만\n publish=true 또는 publishCmsPost를 사용하세요.\n- 이미지는 uploadCmsMedia로 먼저 등록한 뒤 반환된 id를 featuredMediaId에,\n 반환된 URL을 Tiptap image 노드의 attrs.src에 사용하세요.\n- 글을 수정하거나 발행하기 전 analyzeCmsPostSeo로 SEO와 가독성을 확인하세요.\n- 개선할 글을 찾을 때 getCmsPostSeoScores를, 실제 유입 검색어를 고를 때\n getTopSeoKeywords를 사용하세요.\n- 이미 학습된 내용이더라도 본 서버의 문서로 더블체크 후 작업하세요.\n`.trim();\n\nexport function createServer(): Server {\n const server = new Server(\n { name: \"roottale-cms\", version: VERSION },\n { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS },\n );\n registerTools(server);\n return server;\n}\n","import type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport {\n CallToolRequestSchema,\n ErrorCode,\n ListToolsRequestSchema,\n McpError,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\n\nimport {\n AUTOMATION_TOOL_DEFINITIONS,\n callAutomationTool,\n isAutomationToolName,\n} from \"./automation-tools.js\";\nimport { requestRootTaleApi } from \"./api.js\";\nimport { listDocs, readDoc, readNextjsExampleCode, searchDocs } from \"./docs.js\";\nimport {\n callSeoTool,\n isSeoToolName,\n SEO_TOOL_DEFINITIONS,\n} from \"./seo-tools.js\";\n\nconst ReadDocInput = z.object({ path: z.string().min(1) }).strict();\nconst SearchDocsInput = z.object({ query: z.string().min(1) }).strict();\nconst ListPostsInput = z\n .object({\n limit: z.number().int().min(1).max(100).optional(),\n type: z.enum([\"post\", \"page\"]).optional(),\n cursor: z.string().optional(),\n })\n .strict();\nconst GetPostInput = z.object({ slugOrId: z.string().min(1) }).strict();\nconst GetSiteKnowledgeInput = z\n .object({ siteId: z.string().optional() })\n .strict();\n\nconst DOCUMENTATION_TOOL_DEFINITIONS = [\n {\n name: \"listRootTaleDocs\",\n description:\n \"RootTale CMS 통합 문서 전체 목록(경로·제목·설명)을 반환합니다. \" +\n \"RootTale 연동 작업을 시작하기 전 가장 먼저 호출하세요.\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n },\n {\n name: \"readRootTaleDoc\",\n description:\n \"RootTale CMS 통합 문서 1개를 경로로 읽습니다. \" +\n \"경로는 listRootTaleDocs 가 반환한 path 값을 사용하세요.\",\n inputSchema: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"문서 경로 (예: blog.md)\" },\n },\n required: [\"path\"],\n additionalProperties: false,\n },\n },\n {\n name: \"searchRootTaleDocs\",\n description:\n \"RootTale CMS 통합 문서 전체에서 정규식으로 검색합니다 (대소문자 무시). \" +\n \"매치된 파일 경로·라인 번호·라인 내용을 반환합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n query: { type: \"string\", description: \"정규식 패턴 (예: revalidate|webhook)\" },\n },\n required: [\"query\"],\n additionalProperties: false,\n },\n },\n {\n name: \"readRootTaleNextjsExampleCode\",\n description:\n \"RootTale CMS를 외부 Next.js(App Router) 사이트에 연동하는 완전한 예시 코드 세트를 반환합니다 \" +\n \"(블로그 목록/상세, 웹훅 revalidate 라우트, RSS/사이트맵, 상담문의 서버 액션 등). \" +\n \"연동 코드를 작성하기 전 반드시 호출해 패턴을 따라 작성하세요.\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n },\n {\n name: \"listPublishedPosts\",\n description:\n \"연동 검증용 — RootTale 공개 CMS API(GET /v1/cms/public/posts)로 발행된 글 목록을 조회합니다. \" +\n \"환경변수 ROOTTALE_API_KEY(rtlk_cust_*)가 필요합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n limit: { type: \"number\", description: \"1-100, 기본 20\" },\n type: { type: \"string\", enum: [\"post\", \"page\"] },\n cursor: { type: \"string\", description: \"이전 응답의 next_cursor\" },\n },\n additionalProperties: false,\n },\n },\n {\n name: \"getPublishedPost\",\n description:\n \"연동 검증용 — RootTale 공개 CMS API(GET /v1/cms/public/posts/:identifier)로 발행된 글 1개를 \" +\n \"slug 또는 UUID로 조회합니다. 환경변수 ROOTTALE_API_KEY(rtlk_cust_*)가 필요합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n slugOrId: { type: \"string\", description: \"글 slug 또는 UUID\" },\n },\n required: [\"slugOrId\"],\n additionalProperties: false,\n },\n },\n {\n name: \"getSiteKnowledge\",\n description:\n \"이 RootTale 사이트의 브랜드 보이스(어조·톤·화자)와 용어 규칙(금지어·교정어)을 조회합니다 \" +\n \"(GET /v1/cms/public/site-knowledge). 이 사이트에 맞는 글을 쓰기 전에 호출해 톤·표현을 맞추세요. \" +\n \"금지어는 쓰지 말고, 교정어는 권장 표현으로 바꿔 쓰세요. 환경변수 ROOTTALE_API_KEY(rtlk_cust_*)가 필요합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n siteId: {\n type: \"string\",\n description: \"사이트 ID (선택, 미설정 시 기본 사이트)\",\n },\n },\n additionalProperties: false,\n },\n },\n] as const;\n\nexport const TOOL_DEFINITIONS = [\n ...DOCUMENTATION_TOOL_DEFINITIONS,\n ...AUTOMATION_TOOL_DEFINITIONS,\n ...SEO_TOOL_DEFINITIONS,\n] as const;\n\nfunction textResponse(text: string) {\n return { content: [{ type: \"text\" as const, text }] };\n}\n\nfunction jsonResponse(value: unknown) {\n return textResponse(JSON.stringify(value, null, 2));\n}\n\nexport function registerTools(server: Server): void {\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: TOOL_DEFINITIONS.map((t) => ({ ...t })),\n }));\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const args = (request.params.arguments ?? {}) as Record<string, unknown>;\n try {\n if (isAutomationToolName(request.params.name)) {\n return jsonResponse(\n await callAutomationTool(request.params.name, args),\n );\n }\n if (isSeoToolName(request.params.name)) {\n return jsonResponse(await callSeoTool(request.params.name, args));\n }\n switch (request.params.name) {\n case \"listRootTaleDocs\":\n return jsonResponse(await listDocs());\n case \"readRootTaleDoc\": {\n const input = ReadDocInput.parse(args);\n const content = await readDoc(input.path);\n if (content === null) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `문서를 찾을 수 없습니다: ${input.path} — listRootTaleDocs로 경로를 확인하세요.`,\n );\n }\n return textResponse(content);\n }\n case \"searchRootTaleDocs\": {\n const input = SearchDocsInput.parse(args);\n return jsonResponse(await searchDocs(input.query));\n }\n case \"readRootTaleNextjsExampleCode\":\n return textResponse(await readNextjsExampleCode());\n case \"listPublishedPosts\": {\n const input = ListPostsInput.parse(args);\n const params: Record<string, string> = {};\n if (input.limit) params.limit = String(input.limit);\n if (input.type) params.type = input.type;\n if (input.cursor) params.cursor = input.cursor;\n return jsonResponse(\n await requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/public/posts\",\n query: params,\n }),\n );\n }\n case \"getPublishedPost\": {\n const input = GetPostInput.parse(args);\n return jsonResponse(\n await requestRootTaleApi({\n method: \"GET\",\n pathname: `/v1/cms/public/posts/${encodeURIComponent(input.slugOrId)}`,\n }),\n );\n }\n case \"getSiteKnowledge\": {\n const input = GetSiteKnowledgeInput.parse(args);\n const params: Record<string, string> = {};\n if (input.siteId) params.site_id = input.siteId;\n return jsonResponse(\n await requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/public/site-knowledge\",\n query: params,\n }),\n );\n }\n default:\n throw new McpError(\n ErrorCode.InvalidParams,\n `Unknown RootTale tool: ${request.params.name}`,\n );\n }\n } catch (error) {\n if (error instanceof McpError) throw error;\n const message = error instanceof Error ? error.message : String(error);\n return { isError: true, content: [{ type: \"text\" as const, text: message }] };\n }\n });\n}\n","/**\n * 패키지에 번들된 통합 문서(`docs/`)·예시 코드(`examples/`) 로더.\n *\n * dist/index.js 와 src/docs.ts 모두 패키지 루트에서 한 단계 아래이므로\n * `..` 상대 경로 해석이 빌드 전후 동일하다. npm publish 시 `files` 에\n * docs/, examples/ 가 포함되어 npx 환경에서도 같은 구조로 설치된다.\n */\nimport { readdir, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst PKG_ROOT = fileURLToPath(new URL(\"..\", import.meta.url));\nconst DOCS_ROOT = path.join(PKG_ROOT, \"docs\");\nconst EXAMPLES_ROOT = path.join(PKG_ROOT, \"examples\");\n\nexport interface DocEntry {\n /** docs/ 기준 상대 경로 (예: \"blog.md\"). */\n path: string;\n title: string;\n description: string;\n}\n\ninterface Frontmatter {\n title: string;\n description: string;\n}\n\n/** `---\\ntitle: ...\\ndescription: ...\\n---` 형태의 단순 frontmatter 파서. */\nexport function parseFrontmatter(markdown: string): Frontmatter {\n const result: Frontmatter = { title: \"\", description: \"\" };\n const match = markdown.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return result;\n for (const line of match[1].split(\"\\n\")) {\n const idx = line.indexOf(\":\");\n if (idx === -1) continue;\n const key = line.slice(0, idx).trim();\n const value = line.slice(idx + 1).trim();\n if (key === \"title\") result.title = value;\n if (key === \"description\") result.description = value;\n }\n return result;\n}\n\nasync function walkFiles(root: string, dir = root): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n const files: string[] = [];\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n files.push(...(await walkFiles(root, full)));\n } else {\n files.push(path.relative(root, full));\n }\n }\n return files.sort();\n}\n\nexport async function listDocs(): Promise<DocEntry[]> {\n const files = (await walkFiles(DOCS_ROOT)).filter((f) => f.endsWith(\".md\"));\n return Promise.all(\n files.map(async (rel) => {\n const content = await readFile(path.join(DOCS_ROOT, rel), \"utf8\");\n const fm = parseFrontmatter(content);\n return { path: rel, title: fm.title || rel, description: fm.description };\n }),\n );\n}\n\n/** 경로 탈출(../) 방지 후 단일 문서 읽기. 없으면 null. */\nexport async function readDoc(relPath: string): Promise<string | null> {\n const resolved = path.resolve(DOCS_ROOT, relPath);\n if (!resolved.startsWith(DOCS_ROOT + path.sep)) return null;\n try {\n return await readFile(resolved, \"utf8\");\n } catch {\n return null;\n }\n}\n\nexport interface SearchMatch {\n path: string;\n line: number;\n text: string;\n}\n\n/** 모든 문서에 대해 정규식 검색 (대소문자 무시). 매치 라인을 반환. */\nexport async function searchDocs(query: string): Promise<SearchMatch[]> {\n const regex = new RegExp(query, \"i\");\n const matches: SearchMatch[] = [];\n for (const doc of await listDocs()) {\n const content = await readDoc(doc.path);\n if (!content) continue;\n content.split(\"\\n\").forEach((text, idx) => {\n if (regex.test(text)) {\n matches.push({ path: doc.path, line: idx + 1, text: text.trim() });\n }\n });\n }\n return matches;\n}\n\n/**\n * Next.js 통합 예시 코드 전체를 `--- FILE: <path> ---` 헤더로 이어붙여 반환.\n * 파일 수가 적어(통합 1세트) 한 번에 주는 쪽이 에이전트 왕복을 줄인다.\n */\nexport async function readNextjsExampleCode(): Promise<string> {\n const root = path.join(EXAMPLES_ROOT, \"nextjs\");\n const files = await walkFiles(root);\n const sections = await Promise.all(\n files.map(async (rel) => {\n const content = await readFile(path.join(root, rel), \"utf8\");\n return `--- FILE: ${rel} ---\\n${content}`;\n }),\n );\n return sections.join(\"\\n\\n\");\n}\n","import { z } from \"zod\";\n\nimport { requestRootTaleApi } from \"./api.js\";\nimport {\n isSeoToolName,\n SEO_TOOL_DEFINITIONS,\n type SeoToolName,\n} from \"./seo-definitions.js\";\n\nconst SiteIdInput = z.object({ siteId: z.string().optional() });\n\nconst TopKeywordsInput = SiteIdInput.extend({\n dateRange: z\n .enum([\n \"last_7_days\",\n \"last_30_days\",\n \"last_3_months\",\n \"last_6_months\",\n \"last_12_months\",\n ])\n .optional(),\n limit: z.number().int().min(1).max(100).optional(),\n}).strict();\n\nconst AnalyzePostInput = SiteIdInput.extend({\n postId: z.string().min(1),\n focusKeyword: z.string().max(200).optional(),\n}).strict();\n\nconst ScoresInput = SiteIdInput.extend({\n limit: z.number().int().min(1).max(100).optional(),\n type: z.enum([\"post\", \"page\"]).optional(),\n seoFilter: z.enum([\"bad\", \"ok\", \"good\", \"empty_keyword\"]).optional(),\n}).strict();\n\nexport { isSeoToolName, SEO_TOOL_DEFINITIONS };\n\nexport async function callSeoTool(\n name: SeoToolName,\n args: Record<string, unknown>,\n): Promise<unknown> {\n switch (name) {\n case \"getTopSeoKeywords\": {\n const input = TopKeywordsInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/seo/keywords\",\n query: optionalQuery({\n site_id: input.siteId,\n date_range: input.dateRange,\n limit: input.limit,\n }),\n });\n }\n case \"analyzeCmsPostSeo\": {\n const input = AnalyzePostInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname:\n `/v1/cms/seo/posts/${encodeURIComponent(input.postId)}/analysis`,\n query: optionalQuery({\n site_id: input.siteId,\n focus_keyword: input.focusKeyword,\n }),\n });\n }\n case \"getCmsPostSeoScores\": {\n const input = ScoresInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/seo/scores\",\n query: optionalQuery({\n site_id: input.siteId,\n limit: input.limit,\n type: input.type,\n seo_filter: input.seoFilter,\n }),\n });\n }\n }\n}\n\nfunction optionalQuery(\n values: Readonly<Record<string, string | number | undefined>>,\n): Record<string, string> {\n return Object.fromEntries(\n Object.entries(values)\n .filter(\n (entry): entry is [string, string | number] =>\n entry[1] !== undefined,\n )\n .map(([key, value]) => [key, String(value)]),\n );\n}\n","type JsonSchemaProperty = Readonly<Record<string, unknown>>;\n\nconst DATE_RANGES = [\n \"last_7_days\",\n \"last_30_days\",\n \"last_3_months\",\n \"last_6_months\",\n \"last_12_months\",\n] as const;\n\nexport const SEO_TOOL_DEFINITIONS = [\n tool(\n \"getTopSeoKeywords\",\n \"Google Search Console에서 사이트의 상위 검색어와 클릭·노출·CTR·평균 순위를 조회합니다.\",\n {\n siteId: stringProperty(\"사이트 ID\"),\n dateRange: enumProperty(DATE_RANGES, \"조회 기간, 기본 last_30_days\"),\n limit: numberProperty(\"1-100, 기본 25\"),\n },\n ),\n tool(\n \"analyzeCmsPostSeo\",\n \"ROOT-ADMIN과 같은 규칙으로 글의 SEO와 한국어 가독성을 다시 검사합니다. 글을 수정하거나 발행하기 전에 사용하세요.\",\n {\n postId: stringProperty(\"분석할 글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n focusKeyword: stringProperty(\"이번 분석에 사용할 집중 키워드\"),\n },\n [\"postId\"],\n ),\n tool(\n \"getCmsPostSeoScores\",\n \"발행된 글의 최신 SEO 점수와 등급을 조회합니다. 개선이 필요한 글이나 집중 키워드가 없는 글을 찾을 수 있습니다.\",\n {\n siteId: stringProperty(\"사이트 ID\"),\n limit: numberProperty(\"1-100, 기본 10\"),\n type: enumProperty([\"post\", \"page\"], \"콘텐츠 유형\"),\n seoFilter: enumProperty(\n [\"bad\", \"ok\", \"good\", \"empty_keyword\"],\n \"SEO 상태 필터\",\n ),\n },\n ),\n] as const;\n\nexport type SeoToolName = (typeof SEO_TOOL_DEFINITIONS)[number][\"name\"];\n\nexport function isSeoToolName(name: string): name is SeoToolName {\n return SEO_TOOL_DEFINITIONS.some((definition) => definition.name === name);\n}\n\nfunction tool<const TName extends string>(\n name: TName,\n description: string,\n properties: Readonly<Record<string, JsonSchemaProperty>>,\n required: readonly string[] = [],\n) {\n return {\n name,\n description,\n inputSchema: {\n type: \"object\",\n properties,\n ...(required.length > 0 ? { required } : {}),\n additionalProperties: false,\n },\n } as const;\n}\n\nfunction stringProperty(description: string): JsonSchemaProperty {\n return { type: \"string\", description };\n}\n\nfunction numberProperty(description: string): JsonSchemaProperty {\n return { type: \"number\", description };\n}\n\nfunction enumProperty(\n values: readonly string[],\n description: string,\n): JsonSchemaProperty {\n return { type: \"string\", enum: values, description };\n}\n"],"mappings":"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","names":["readFile","z","z","readFile","z","readFile","z","tool","stringProperty","enumProperty","numberProperty","z","optionalQuery","z"]}
@@ -262,6 +262,51 @@ fallback 네비를 렌더하세요 (`menus.md` 참고).
262
262
  settings.siteProfile.defaultOgImageUrl` 순으로 우선합니다. `logo_url`·
263
263
  `favicon_url`·`site_description` 은 사이트 `<head>` 에 적용합니다.
264
264
 
265
+ ## GET /v1/cms/public/categories
266
+
267
+ 분류(카테고리)와 **분류별 발행 글 수**. 글 수는 서버가 SQL로 세므로 글이 몇 건이든
268
+ 정확합니다 — 분류 모음 페이지의 "글 0건이면 404" 판정과 사이트맵 포함 여부(색인
269
+ 위생)를 계산하는 데 씁니다. `@roottale/cms-renderer-next` 의
270
+ `fetchBlogArchiveCategories` 가 이 API를 자동으로 씁니다(직접 부를 일은 드뭅니다).
271
+
272
+ 쿼리(선택):
273
+
274
+ | 이름 | 값 | 설명 |
275
+ | --- | --- | --- |
276
+ | `exclude_collections` | `true` | 어느 콘텐츠 유형에도 속하지 않은 글만 셉니다 — `/blog` 계열 모음 페이지가 다루는 범위와 같습니다. |
277
+ | `collection_key` | 유형 key | 그 유형에 속한 글만 셉니다. `exclude_collections` 와 동시 지정 시 400. |
278
+ | `locale` | BCP-47 | 미지정이면 사이트 기본 로케일(글 목록과 같은 기본값). |
279
+ | `site_id` | 사이트 id | 보통 생략. |
280
+
281
+ ```json
282
+ { "tenant_id": "…", "site_id": "…",
283
+ "categories": [
284
+ { "slug": "tax", "name": "세무", "published_post_count": 412,
285
+ "collection_key": null, "hub_promoted": true },
286
+ { "slug": "law", "name": "법률", "published_post_count": 0,
287
+ "collection_key": null, "hub_promoted": false }
288
+ ] }
289
+ ```
290
+
291
+ - 글이 0건인 분류도 그대로 내려옵니다(`published_post_count: 0`) — "그런 분류가
292
+ 없다"와 "분류는 있는데 이 범위에 글이 없다"를 구분할 수 있게.
293
+ - `hub_promoted` 는 어드민의 "검색에 이 분류 페이지 노출" 값이지만, **검색 노출
294
+ 판정에는 쓰지 마세요**. 그 판정의 단일 소스는 `/blog-settings` 의
295
+ `sitemap.promoted_category_slugs` 입니다(두 소스를 섞으면 사이트맵과 페이지가
296
+ 서로 다른 시점의 값을 볼 수 있습니다). 여기 값은 진단·표시용입니다.
297
+ - 분류가 서버 상한(500개)을 넘으면 응답에 `"truncated": true` 가 붙습니다.
298
+
299
+ `@roottale/cms-client` 로 직접 부를 때:
300
+
301
+ ```ts
302
+ import { fetchCategoryCounts } from "@roottale/cms-client/server";
303
+
304
+ const { categories, truncated } = await fetchCategoryCounts({
305
+ apiKey: process.env.ROOTTALE_API_KEY!,
306
+ excludeCollections: true,
307
+ });
308
+ ```
309
+
265
310
  ## GET /v1/cms/public/site-knowledge
266
311
 
267
312
  사이트 지식 — 브랜드 보이스(어조·톤·화자) + 용어 규칙(금지어·교정어). AI
@@ -282,17 +327,19 @@ settings.siteProfile.defaultOgImageUrl` 순으로 우선합니다. `logo_url`·
282
327
 
283
328
  ```json
284
329
  { "tenant_id": "…", "site_id": "…", "configured": true,
285
- "name": "길동세무회계", "legal_name": null,
330
+ "name": "길동세무회계", "legal_name": null, "alternate_name": "길동 세무회계",
286
331
  "business_type": "AccountingService",
287
- "telephone": "02-1234-5678", "email": null,
332
+ "telephone": "02-1234-5678", "fax_number": "02-1234-5679", "email": null,
288
333
  "address": { "street_address": "테헤란로 123", "address_locality": "강남구",
289
334
  "address_region": "서울특별시", "postal_code": "06234" },
290
335
  "geo": { "latitude": 37.5006, "longitude": 127.0364 },
291
336
  "opening_hours": [ { "days": ["Mo","Tu","We","Th","Fr"],
292
337
  "opens": "09:00", "closes": "18:00" } ],
293
338
  "price_range": "₩₩", "area_served": ["서울 강남구"],
339
+ "services": ["기장·신고대리", "세무고문"],
294
340
  "profiles": { "naver_place": "https://…", "google_business": "https://…",
295
- "kakao_channel": null, "instagram": null, "naver_blog": null },
341
+ "kakao_channel": null, "kakao_map": "https://…",
342
+ "instagram": null, "naver_blog": null },
296
343
  "updated_at": "…" }
297
344
  ```
298
345
 
@@ -300,6 +347,17 @@ settings.siteProfile.defaultOgImageUrl` 순으로 우선합니다. `logo_url`·
300
347
  `AccountingService` | `LegalService` | `MedicalClinic` | `Dentist` |
301
348
  `RealEstateAgent` | `Restaurant` | `BeautySalon`.
302
349
 
350
+ | 필드 | JSON-LD 매핑 |
351
+ |---|---|
352
+ | `alternate_name` | `alternateName` — 띄어쓰기 변형 등 같은 사업장의 다른 표기 |
353
+ | `fax_number` | `faxNumber` |
354
+ | `services` | `hasOfferCatalog` 의 `Offer.itemOffered` 목록 (최대 12개) |
355
+ | `profiles.kakao_map` | `hasMap` — `naver_place` 가 있으면 그쪽이 우선. **`sameAs` 에는 들어가지 않는다**(프로필이 아니라 위치 링크) |
356
+
357
+ > **주의**: `name`(사업장 이름)이 비어 있으면 `configured: false` 로 내려가고
358
+ > `@roottale/cms-client` 의 `fetchBusinessProfile` 은 `null` 을 반환한다 —
359
+ > 전화·주소만 채우고 이름을 비우면 나머지 입력이 전부 무시된다.
360
+
303
361
  ## GET /v1/cms/public/analytics
304
362
 
305
363
  분석 태그 설정.
package/docs/seo.md CHANGED
@@ -62,6 +62,8 @@ export default sitemap;
62
62
 
63
63
  - **블로그 이미지 색인** — 글에 대표 이미지가 있으면 `<image:image>`로 함께 색인합니다.
64
64
  어드민 **설정 > 블로그 > 사이트맵**에서 켜고 끌 수 있어요(기본 켜짐).
65
+ - **분류·작가 모음 페이지는 기본으로 사이트맵에서 빠집니다** — 어드민에서 "검색에
66
+ 노출"을 켠 것만 들어갑니다. 아래 "색인 위생" 절을 보세요.
65
67
  - `changeFrequency`/`priority`는 Google이 무시하므로 더 이상 내보내지 않습니다(`loc` +
66
68
  `lastmod` + 이미지만).
67
69
  - 단일 평면 사이트맵이 필요하면 레거시 `createSitemap`(default export 하나)도 그대로
@@ -143,6 +145,143 @@ const jsonLd = collectionPageSchema({
143
145
  });
144
146
  ```
145
147
 
148
+ ### 색인 위생 — 모음 페이지는 기본 "검색 노출 안 함"
149
+
150
+ 분류·태그·작가·검색 결과처럼 **글을 모아 보여주는 페이지**는 내용이 겹치기 쉬워,
151
+ 전부 검색에 올리면 오히려 사이트 평가가 내려갑니다. 그래서 RootTale은 모음 페이지를
152
+ **기본으로 검색에서 빼고**, 소개 글을 채워 대표로 삼은 분류만 올립니다.
153
+
154
+ - 켜는 곳: 어드민 **내 사이트 > 카테고리**에서 분류를 펼치고 "검색에 이 분류 페이지
155
+ 노출"을 체크합니다. 켠 분류만 사이트맵에 들어가고 `robots`도 색인 허용이 됩니다.
156
+ - 작가 모음은 **설정 > 블로그 > 사이트맵**의 "작가별 글 모음도 검색엔진에 알리기"가
157
+ 같은 역할을 합니다.
158
+ - 글 자체는 영향을 받지 않습니다 — 모음 페이지만 빠집니다.
159
+
160
+ 판정은 `decideArchiveIndex` 하나에서 나옵니다. 사이트맵 필터와 페이지의 `robots`가
161
+ **같은 함수**를 보므로 "검색에서 빼기로 해놓고 사이트맵에는 넣는" 어긋남이 생기지
162
+ 않습니다. 단, 같은 함수를 쓰는 것만으로는 부족하고 **판정에 넣는 값도 같아야**
163
+ 합니다. 그래서 입력을 만드는 세 가지도 함께 제공합니다:
164
+
165
+ - `resolveCategoryPromotion(slugs, slug)` — 노출 설정을 3단계
166
+ (`promoted` / `not-promoted` / `unknown`)로 바꿉니다. 설정을 못 읽었을 때
167
+ `unknown` 을 그대로 넘기면 정책이 **기존 상태를 보존**합니다. 여기서 임의로
168
+ "안 켬"으로 바꾸면, 설정 조회가 한 번 실패했다는 이유만으로 이미 검색에 올라가
169
+ 있던 주소에 noindex 가 붙습니다.
170
+ - `fetchBlogArchiveCategories(config)` — `/blog` 분류 모음 페이지가 **다루는 글**
171
+ 에서 분류와 글 수를 뽑습니다. 사이트맵도 같은 함수를 씁니다. 직접 글을 받아
172
+ 세지 마세요 — 세는 글 집합이 갈리면 "사이트맵엔 주소가 있는데 페이지는 404" 가
173
+ 생깁니다.
174
+ - `ARCHIVE_POST_COUNT_LIMIT` — 아래 폴백 경로가 쓰는 글 개수 한도. 직접 셀 때도
175
+ 같은 상수를 쓰세요.
176
+
177
+ 글 수는 `GET /v1/cms/public/categories` 가 **서버에서 세어** 내려줍니다. 글이
178
+ 몇 건이든 모든 분류의 정확한 개수가 나오므로, 오래된 글에만 달린 분류가 검색에서
179
+ 사라지지 않습니다. `fetchBlogArchiveCategories` 가 이 API를 먼저 쓰고, 못 받으면
180
+ (구버전 API이거나 일시 장애) 예전처럼 최근 글 `ARCHIVE_POST_COUNT_LIMIT` 건을 받아
181
+ 세는 방식으로 되돌아갑니다 — 사이트맵과 페이지가 여전히 같은 함수를 쓰므로 어느
182
+ 경로든 두 곳의 결론은 같습니다.
183
+
184
+ > **어떤 글을 세나요**: `/blog` 계열 모음 페이지는 "콘텐츠 유형(스트림)에 속하지
185
+ > 않은 글"만 다룹니다. 증상·질환 같은 별도 유형을 쓰는 사이트에서 그 유형 글에만
186
+ > 달린 분류는 `/blog/categories/{slug}` 로 볼 수 없으므로(그 유형은 자기 주소가
187
+ > 따로 있습니다) 사이트맵에서도 빠집니다. `fetchBlogArchiveCategories` 가 이
188
+ > 범위를 서버에 물어보므로(`exclude_collections`), 어떤 유형을 만들었든 두 곳이
189
+ > 항상 같은 글을 셉니다. 유형별 모음 페이지(`{유형주소}/categories/{slug}`)는
190
+ > `collectCollectionCategories(posts, collections, key)` 가 짝입니다.
191
+
192
+ ```tsx
193
+ // app/blog/categories/[slug]/page.tsx
194
+ import { notFound } from "next/navigation";
195
+ import {
196
+ buildArchiveMetadata,
197
+ decideArchiveIndex,
198
+ fetchBlogArchiveCategories,
199
+ resolveCategoryPromotion,
200
+ } from "@roottale/cms-renderer-next/routes";
201
+ import { fetchBlogSettings } from "@roottale/cms-client/server";
202
+
203
+ const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL!;
204
+ const apiKey = process.env.ROOTTALE_API_KEY!;
205
+
206
+ async function archiveState(slug: string) {
207
+ const [settings, categories] = await Promise.all([
208
+ // 설정 조회 실패는 "안 켬"이 아니라 "모름"입니다 — undefined 로 넘깁니다.
209
+ fetchBlogSettings({ apiKey }).catch(() => null),
210
+ // 사이트맵과 같은 함수 = 같은 글 집합, 같은 한도.
211
+ fetchBlogArchiveCategories({ apiKey }),
212
+ ]);
213
+ return {
214
+ promotion: resolveCategoryPromotion(
215
+ settings?.sitemap?.promotedCategorySlugs,
216
+ slug,
217
+ ),
218
+ // 이 라우트가 모르는 분류면 0건 → 아래에서 404.
219
+ postCount: categories.find((c) => c.slug === slug)?.count ?? 0,
220
+ };
221
+ }
222
+
223
+ export async function generateMetadata({ params }) {
224
+ const { slug } = await params;
225
+ const { promotion, postCount } = await archiveState(slug);
226
+ return buildArchiveMetadata(
227
+ { kind: "category", promotion, postCount, title: `${slug} 글 모음` },
228
+ { siteUrl: SITE_URL, path: `/blog/categories/${slug}` },
229
+ );
230
+ }
231
+
232
+ export default async function CategoryArchive({ params }) {
233
+ const { slug } = await params;
234
+ const { promotion, postCount } = await archiveState(slug);
235
+ // 글이 하나도 없는 모음 페이지는 만들지 않습니다(404).
236
+ if (decideArchiveIndex({ kind: "category", promotion, postCount }).render === "notFound") {
237
+ notFound();
238
+ }
239
+ // ... 목록 렌더
240
+ }
241
+ ```
242
+
243
+ 정책 요약(`decideArchiveIndex`):
244
+
245
+ | 상황 | 화면 | 검색 노출 | 사이트맵 |
246
+ | --- | --- | --- | --- |
247
+ | 글 0건 (검색 결과 제외) | 404 | ✕ | ✕ |
248
+ | 2페이지 이상 | 정상 | ✕ | ✕ |
249
+ | 태그·날짜·사이트 내 검색 | 정상 | ✕ | ✕ |
250
+ | 분류·작가 — 노출 켬(`promoted`) | 정상 | ○ | ○ |
251
+ | 분류·작가 — 노출 끔(`not-promoted`, 기본) | 정상 | ✕ | ✕ |
252
+ | 분류·작가 — 설정 모름(`unknown`) | 정상 | ○ | ○ |
253
+
254
+ 승격 목록은 `GET /v1/cms/public/blog-settings` 응답의
255
+ `sitemap.promoted_category_slugs`(cms-client에서는 `sitemap.promotedCategorySlugs`)
256
+ 로 내려옵니다. 이 필드가 아예 없는 구버전 API에 붙어 있거나 설정 조회에 실패하면
257
+ `resolveCategoryPromotion` 이 `unknown` 을 돌려주고, 렌더러는 정책을 적용하지 않고
258
+ 기존처럼 모든 분류를 사이트맵에 넣습니다(모른다고 이미 색인된 주소를 지우지 않기
259
+ 위해서입니다). 빈 배열 `[]` 은 "승격 0건"이라는 **확정 정보**라 의미가 다릅니다.
260
+
261
+ **구버전 클라이언트 호환**: `sitemap.promoted_category_slugs` 는 응답에 새로 생긴
262
+ 키입니다. `@roottale/cms-client` 는 응답에서 아는 필드만 골라 쓰고 모르는 키는
263
+ 무시하므로(스키마 전체 검증을 하지 않습니다) 구버전 사이트를 그대로 두어도 이 키
264
+ 때문에 깨지지 않습니다 — 사이트를 올리지 않으면 이 절의 색인 정책이 적용되지 않을
265
+ 뿐입니다.
266
+
267
+ > **한도 안내**: 분류별 글 수는 `GET /v1/cms/public/categories` 가 서버에서 세므로
268
+ > 글이 아무리 많아도 누락되지 않습니다. 다만 그 API를 못 받아 폴백으로 도는 동안
269
+ > (구버전 API·일시 장애)에는 최근 `ARCHIVE_POST_COUNT_LIMIT` 건만 보게 되어, 한도
270
+ > 밖에만 글이 있는 분류가 사이트맵과 페이지 **양쪽에서** 빠집니다(페이지는 404).
271
+ > 양쪽이 같은 기준으로 판단하므로 색인 어긋남은 이때도 생기지 않습니다.
272
+ >
273
+ > 유형별 모음 페이지(`{유형주소}/categories/{slug}`)를 만드는
274
+ > `collectCollectionCategories` 는 아직 최근 글 배치만 씁니다. 서버 집계 API는
275
+ > `collection_key` 로 유형 범위를 지정할 수 있으므로 같은 방식으로 옮길 예정입니다.
276
+
277
+ > **어긋남이 잠깐 보일 수 있는 경우**: 사이트맵과 모음 페이지는 서로 다른 요청에서
278
+ > 각자 설정을 읽습니다. 한쪽만 조회에 실패하면 그 사이에는 "검색에서 뺐는데
279
+ > 사이트맵엔 남아 있는" 상태가 생길 수 있습니다. 사이트맵이 다시 만들어질 때
280
+ > 자동으로 맞춰지며, **걸리는 시간의 상한 = 사이트맵의 `revalidate` 값**입니다
281
+ > (Site Kit 기본 60초). 조회 실패 때 주소를 빼지 않고 남기는 쪽을 택한 이유는,
282
+ > 일시적인 장애 한 번으로 이미 검색에 올라간 주소를 사이트맵에서 지우는 손실이 더
283
+ > 크기 때문입니다.
284
+
146
285
  ### 다국어 (hreflang) — ADR-0052 A안, 콘텐츠 다국어
147
286
 
148
287
  번역 글이 있는 사이트는 어드민 **설정 > 블로그 > 사이트맵**의 "다국어 로케일"에
@@ -554,8 +693,14 @@ export default async function RootLayout({ children }) {
554
693
  (세무·회계 = `AccountingService`, 병원·의원 = `MedicalClinic` 등)
555
694
  - `address`(PostalAddress) / `geo`(GeoCoordinates) /
556
695
  `openingHoursSpecification` / `priceRange` / `areaServed`
696
+ - `alternateName`: 띄어쓰기 변형처럼 같은 사업장을 다르게 부르는 이름
697
+ - `faxNumber`: 팩스를 쓰는 업종(세무·법률 등)의 연락처
698
+ - `hasOfferCatalog`: 어드민 "취급 업무" 목록 — "무엇을 하는 곳인가"를 명시하는
699
+ 신호입니다. 명함·간판에 적힌 업무를 그대로 옮기면 됩니다.
700
+ - `hasMap`: 네이버플레이스 주소(없으면 카카오맵 주소)
557
701
  - `sameAs`: 어드민에 입력한 네이버플레이스·구글 비즈니스 프로필·카카오 채널
558
- 등의 URL — 검색엔진이 동일 사업장임을 연결합니다.
702
+ 등의 URL — 검색엔진이 동일 사업장임을 연결합니다. 지도 딥링크(카카오맵)는
703
+ 프로필 페이지가 아니므로 `hasMap` 으로만 나가고 여기엔 포함되지 않습니다.
559
704
 
560
705
  네이버플레이스([new.smartplace.naver.com](https://new.smartplace.naver.com))와
561
706
  구글 비즈니스 프로필([business.google.com](https://business.google.com)) 등록
@@ -616,3 +761,43 @@ export const GET = createLlmsTxtRoute({
616
761
  API 조회가 실패해도 항상 200으로 헤더 부분을 반환합니다(빌드 사고 방지).
617
762
  발행 웹훅의 기본 `alsoRevalidate`에 `/llms.txt`가 포함되어 있어, 글을
618
763
  발행·수정하면 AI 크롤러용 인덱스도 자동으로 갱신됩니다.
764
+
765
+ ## 에셋 도메인 정합 스캔 (스테이징 잔류 방지)
766
+
767
+ 빌드 산출물의 이미지·스크립트·스타일시트가 스테이징/프리뷰 호스트(예:
768
+ `*.vercel.app`, `staging.example.com`)를 그대로 참조한 채 배포되면 운영
769
+ 사이트가 죽은 링크나 깨진 자산을 서빙하게 됩니다. `scanAssetHosts`는 렌더된
770
+ HTML(+ CSS)에서 절대 URL 에셋 호스트를 추출해 허용목록 밖의 호스트, 그리고
771
+ 허용목록에 있어도 스테이징 패턴이면 무조건 위반으로 보고합니다.
772
+
773
+ ```ts
774
+ import { scanAssetHosts } from "@roottale/cms-renderer-next/seo-asset-hosts";
775
+
776
+ const violations = scanAssetHosts(
777
+ html, // 렌더된 페이지 HTML — 인라인 <style> 블록도 함께 스캔됨
778
+ { siteHost: "www.example.com", cdnHosts: [".cloudfront.net"] },
779
+ [externalCssString], // 선택: 별도로 로드되는 CSS 문자열(@import 1-depth)
780
+ );
781
+ // violations: { url, host, source, staging? }[] — 0건이면 통과
782
+ ```
783
+
784
+ - 스캔 대상: `<img src>`/`<img srcset>`, `<source src>`(media)/`<source
785
+ srcset>`, `<script src>`, `<video poster>`/`<video src>`, `<audio src>`,
786
+ `<track src>`, `<base href>`(가장 위험 — 이후 모든 상대 URL이 이 호스트
787
+ 기준으로 요청됨), `<iframe src>`, `<embed src>`/`<object data>`, `<input
788
+ type="image" src>`, `<link href>`(단 `rel`이 실제로 에셋을 로드하는
789
+ 값일 때만 — `stylesheet`/`preload`/`modulepreload`/`prefetch`/
790
+ `preconnect`/`dns-prefetch`/`icon`류/`manifest`. `canonical`/`alternate`/
791
+ `author`/`me` 등은 검사하지 않음), `<link imagesrcset>`(srcset 문법), 인라인·
792
+ 외부 CSS의 `url()`/`@import`/`image-set()`(`-webkit-image-set()` 포함).
793
+ - 절대 URL(`https://`/`http://`/`//host/...`)만 판정합니다 — 상대경로·
794
+ `data:`/`blob:`는 통과. 속성값의 HTML 엔티티(`&amp;`, `&#NN;` 등)는 URL
795
+ 파싱 전에 디코드하고, URL 안의 백슬래시는 브라우저와 동일하게 슬래시로
796
+ 정규화합니다. `new URL()` 파싱이 실패하는 절대 URL은 호스트를 추측하지
797
+ 않고 그 자체로 위반 처리합니다(fail-closed, `host: "(unparseable)"`).
798
+ - **한계**: HTML 태그는 quote-aware 토크나이저로 스캔하지만 완전한 DOM
799
+ 파서는 아니라 스크립트 문자열 리터럴 안의 가짜 태그는 걸러내지 못하고, CSS
800
+ `@import`는 1-depth만 보며(중첩 `@import`는 범위 밖), CSS escape 시퀀스
801
+ (`\3a ` 등)까지는 디코드하지 않습니다. 런타임에 JS로 삽입되는 요청은 배포
802
+ 후 실제 네트워크 요청을 검사하는 E2E 프로브로 보완하세요.
803
+
@@ -47,8 +47,9 @@ const display = resolvePostDisplay(settings, post);
47
47
 
48
48
  ## 비즈니스 프로필 (로컬 SEO) — fetchBusinessProfile
49
49
 
50
- 어드민 **운영 > 비즈니스 프로필**에서 저장한 사업장 정보(이름·업종·주소·
51
- 좌표·영업시간·네이버플레이스/구글 비즈니스 프로필 URL)를 조회합니다.
50
+ 어드민 **운영 > 비즈니스 프로필**에서 저장한 사업장 정보(이름·별칭·업종·
51
+ 연락처·팩스·주소·좌표·영업시간·취급 업무·네이버플레이스/구글/카카오 URL)를
52
+ 조회합니다.
52
53
 
53
54
  ```ts
54
55
  import {
@@ -59,8 +60,9 @@ import {
59
60
  const business = await fetchBusinessProfile({
60
61
  apiKey: process.env.ROOTTALE_API_KEY!,
61
62
  });
62
- // 미설정이면 null. 설정돼 있으면 name, businessType, address, geo,
63
- // openingHours, priceRange, areaServed, profiles(sameAs용 URL) 포함.
63
+ // 미설정이면 null. 설정돼 있으면 name, alternateName, businessType, telephone,
64
+ // faxNumber, address, geo, openingHours, priceRange, areaServed, services,
65
+ // profiles 포함.
64
66
 
65
67
  if (business) {
66
68
  const jsonLd = localBusinessSchema(business, {
@@ -70,6 +72,20 @@ if (business) {
70
72
  }
71
73
  ```
72
74
 
75
+ `localBusinessSchema` 가 만들어 주는 매핑:
76
+
77
+ | 프로필 값 | JSON-LD |
78
+ |---|---|
79
+ | `alternateName` | `alternateName` — 띄어쓰기 변형 등 같은 사업장의 다른 표기 |
80
+ | `faxNumber` | `faxNumber` |
81
+ | `services` | `hasOfferCatalog` (각 항목이 `Offer` → `Service`) |
82
+ | `profiles.naverPlace` \| `profiles.kakaoMap` | `hasMap` (네이버 플레이스 우선) |
83
+ | 그 밖의 `profiles` 값 | `sameAs` — 지도 딥링크(`kakaoMap`)는 제외 |
84
+
85
+ > **주의**: `name`(사업장 이름)이 비어 있으면 `fetchBusinessProfile` 이 `null` 을
86
+ > 반환합니다. 전화·주소만 채우고 이름을 비워두면 **나머지 입력이 전부 무시**되니
87
+ > 고객 안내 시 이름을 필수로 안내하세요.
88
+
73
89
  ## 분석 태그 — fetchAnalyticsConfig
74
90
 
75
91
  어드민에서 등록한 외부 분석 태그(GA4, Microsoft Clarity, Meta Pixel, 네이버)
@@ -8,8 +8,12 @@ import type { Metadata } from "next";
8
8
  import Link from "next/link";
9
9
  import { notFound } from "next/navigation";
10
10
  import { collectionPageSchema } from "@roottale/cms-client/server";
11
+ import {
12
+ buildArchiveMetadata,
13
+ decideArchiveIndex,
14
+ } from "@roottale/cms-renderer-next/routes";
11
15
 
12
- import { getPostsByCategory } from "@/lib/blog";
16
+ import { getCategoryPromotion, getPostsByCategory } from "@/lib/blog";
13
17
 
14
18
  const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL!;
15
19
 
@@ -17,20 +21,45 @@ type Props = { params: Promise<{ slug: string }> };
17
21
 
18
22
  export const revalidate = 1800;
19
23
 
24
+ // 색인 위생(seo.md "색인 위생") — 사이트맵 포함 여부와 robots 를 같은
25
+ // decideArchiveIndex 판정에서 파생합니다. 어드민에서 "검색에 이 분류 페이지
26
+ // 노출" 을 켠 분류만 색인되고, 나머지는 화면은 정상이되 noindex 입니다.
27
+ //
28
+ // 판정 입력(승격 상태·글 수)은 **사이트맵과 같은 소스·같은 한도**에서 얻어야
29
+ // 합니다. 여기서 갈리면 "사이트맵엔 있는데 페이지는 404" 가 생깁니다.
30
+ async function loadArchive(slug: string) {
31
+ const [posts, promotion] = await Promise.all([
32
+ getPostsByCategory(slug),
33
+ getCategoryPromotion(slug),
34
+ ]);
35
+ return { posts, promotion, postCount: posts.length };
36
+ }
37
+
20
38
  export async function generateMetadata({ params }: Props): Promise<Metadata> {
21
39
  const { slug } = await params;
22
- const posts = await getPostsByCategory(slug);
23
- if (posts.length === 0) return { title: "카테고리" };
24
- return {
25
- title: `${posts[0]!.category} 글 모음`,
26
- alternates: { canonical: `/blog/categories/${slug}` },
27
- };
40
+ const { posts, promotion, postCount } = await loadArchive(slug);
41
+ if (postCount === 0) return { title: "카테고리" };
42
+ return buildArchiveMetadata(
43
+ {
44
+ kind: "category",
45
+ promotion,
46
+ postCount,
47
+ title: `${posts[0]!.category} 글 모음`,
48
+ },
49
+ { siteUrl: SITE_URL, path: `/blog/categories/${slug}` },
50
+ );
28
51
  }
29
52
 
30
53
  export default async function CategoryArchivePage({ params }: Props) {
31
54
  const { slug } = await params;
32
- const posts = await getPostsByCategory(slug);
33
- if (posts.length === 0) notFound();
55
+ const { posts, promotion, postCount } = await loadArchive(slug);
56
+ // 글 0건 모음 페이지는 만들지 않습니다(404) — 정책이 판정합니다.
57
+ if (
58
+ decideArchiveIndex({ kind: "category", promotion, postCount }).render ===
59
+ "notFound"
60
+ ) {
61
+ notFound();
62
+ }
34
63
 
35
64
  const categoryName = posts[0]!.category;
36
65
  const url = `${SITE_URL}/blog/categories/${slug}`;
@@ -13,6 +13,7 @@ export function AttributionField() {
13
13
  const [value, setValue] = useState("");
14
14
  useEffect(() => {
15
15
  const attribution = readAttribution();
16
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- 어트리뷰션은 브라우저 저장소에서만 읽을 수 있어 마운트 후에야 알 수 있다.
16
17
  if (attribution) setValue(JSON.stringify(attribution));
17
18
  }, []);
18
19
  return <input type="hidden" name="attribution" value={value} />;
@@ -1,10 +1,15 @@
1
1
  // RootTale CMS 블로그 데이터 레이어 — 서버 전용.
2
2
  // 사이트 UI에 맞는 메타 형태로 변환하는 wrapper. API 키는 env에서만 읽는다.
3
3
  import {
4
- fetchPosts,
4
+ fetchBlogSettings,
5
5
  fetchPost,
6
6
  type CmsPostContent,
7
7
  } from "@roottale/cms-client/server";
8
+ import {
9
+ type ArchivePromotion,
10
+ fetchBlogArchivePosts,
11
+ resolveCategoryPromotion,
12
+ } from "@roottale/cms-renderer-next/routes";
8
13
 
9
14
  function getApiKey(): string {
10
15
  const key = process.env.ROOTTALE_API_KEY;
@@ -60,14 +65,15 @@ function toMeta(post: CmsPostContent): BlogPostMeta {
60
65
  };
61
66
  }
62
67
 
68
+ // `/blog` 계열 표면(목록·분류 모음)이 다루는 글 = 콘텐츠 유형(스트림)에 속하지
69
+ // 않은 글. 사이트맵도 **같은 함수**로 같은 글을 세므로 "사이트맵엔 주소가 있는데
70
+ // 페이지는 404" 가 생기지 않습니다(한도 상수도 함께 따라옵니다).
63
71
  export async function getAllPosts(): Promise<BlogPostMeta[]> {
64
- const page = await fetchPosts({
72
+ const posts = await fetchBlogArchivePosts({
65
73
  apiKey: getApiKey(),
66
- baseUrl,
67
- type: "post",
68
- limit: 100,
74
+ apiBase: baseUrl,
69
75
  });
70
- return page.items.map(toMeta);
76
+ return posts.map(toMeta);
71
77
  }
72
78
 
73
79
  export async function getPost(
@@ -88,3 +94,25 @@ export async function getPostsByCategory(
88
94
  const posts = await getAllPosts();
89
95
  return posts.filter((p) => p.categorySlug === categorySlug);
90
96
  }
97
+
98
+ // 색인 위생 — 어드민 "내 사이트 > 카테고리 > 검색에 이 분류 페이지 노출" 을 켠
99
+ // 분류인지. 켜지 않은 분류의 모음 페이지는 noindex + 사이트맵 제외가 됩니다
100
+ // (seo.md "색인 위생" 절).
101
+ //
102
+ // 반환은 boolean 이 아니라 3-state(`promoted` / `not-promoted` / `unknown`)입니다.
103
+ // 설정을 못 읽었을 때(구버전 API·조회 실패) `not-promoted` 로 바꿔 넘기면, 설정
104
+ // 조회가 한 번 실패했다는 이유만으로 이미 검색에 올라가 있던 주소에 noindex 가
105
+ // 붙습니다. `unknown` 을 그대로 넘기면 정책이 알아서 기존 상태를 보존합니다.
106
+ export async function getCategoryPromotion(
107
+ categorySlug: string,
108
+ ): Promise<ArchivePromotion> {
109
+ try {
110
+ const settings = await fetchBlogSettings({ apiKey: getApiKey(), baseUrl });
111
+ return resolveCategoryPromotion(
112
+ settings.sitemap?.promotedCategorySlugs,
113
+ categorySlug,
114
+ );
115
+ } catch {
116
+ return "unknown";
117
+ }
118
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-mcp",
3
- "version": "0.48.0",
3
+ "version": "0.50.0",
4
4
  "type": "module",
5
5
  "description": "RootTale CMS MCP server and CLI for post publishing, media uploads, integration docs, and public API access.",
6
6
  "bin": {