@roottale/cms-mcp 0.41.1 → 0.44.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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.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 { createServer } from \"./server.js\";\n\nconst server = createServer();\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\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- 이미 학습된 내용이더라도 본 서버의 문서로 더블체크 후 작업하세요.\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 { listDocs, readDoc, readNextjsExampleCode, searchDocs } from \"./docs.js\";\n\nconst DEFAULT_API_BASE = \"https://api.roottale.com\";\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\nexport const 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\nfunction requireApiKey(): { apiKey: string; baseUrl: string } {\n const apiKey = process.env.ROOTTALE_API_KEY;\n if (!apiKey) {\n throw new McpError(\n ErrorCode.InvalidRequest,\n \"ROOTTALE_API_KEY 환경변수가 없습니다. MCP 서버 설정의 env에 \" +\n \"rtlk_cust_* 키를 추가하세요 (발급: 어드민 설정 > 사이트 연결 키).\",\n );\n }\n const baseUrl = (process.env.ROOTTALE_API_BASE ?? DEFAULT_API_BASE).replace(/\\/+$/, \"\");\n return { apiKey, baseUrl };\n}\n\nasync function fetchPublicApi(pathname: string, params?: Record<string, string>): Promise<unknown> {\n const { apiKey, baseUrl } = requireApiKey();\n const url = new URL(`${baseUrl}${pathname}`);\n for (const [key, value] of Object.entries(params ?? {})) {\n url.searchParams.set(key, value);\n }\n const response = await fetch(url, {\n headers: { authorization: `Bearer ${apiKey}` },\n });\n const body = await response.text();\n if (!response.ok) {\n throw new Error(`RootTale API ${response.status}: ${body.slice(0, 500)}`);\n }\n return JSON.parse(body);\n}\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 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(await fetchPublicApi(\"/v1/cms/public/posts\", params));\n }\n case \"getPublishedPost\": {\n const input = GetPostInput.parse(args);\n return jsonResponse(\n await fetchPublicApi(\n `/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 fetchPublicApi(\"/v1/cms/public/site-knowledge\", params),\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":["server","server"]}
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"]}
@@ -8,8 +8,130 @@ description: 공개 API raw 엔드포인트 — JS 외 스택이나 저수준
8
8
  베이스 URL: `https://api.roottale.com`
9
9
  인증: 모든 요청에 `Authorization: Bearer rtlk_cust_...` 헤더.
10
10
 
11
- JS/TS 스택은 raw 호출 대신 `@roottale/cms-client`를 사용하세요 — 에러 처리,
12
- 브라우저 노출 방지, 전화번호 포맷 등이 포함되어 있습니다.
11
+ 공개 콘텐츠 조회를 JS/TS로 연동할 때는 raw 호출 대신
12
+ `@roottale/cms-client`를 사용하세요. 글쓰기·미디어 자동화는 아래 HTTP API,
13
+ MCP tool 또는 공개 CLI를 사용합니다.
14
+
15
+ 글쓰기·미디어 자동화는 `read_write` API 키가 필요합니다. 공개 콘텐츠 조회만
16
+ 필요하면 `read` 키를 사용하세요.
17
+
18
+ ## 관리 API 빠른 흐름
19
+
20
+ 관리 API는 초안 작성 → 미디어 연결 → 발행을 분리합니다. 자동화가 실수로
21
+ 미완성 글을 공개하지 않도록 글 생성의 기본 상태는 `draft`입니다.
22
+
23
+ ### POST /v1/cms/posts
24
+
25
+ Tiptap JSON 본문으로 글 또는 페이지를 만듭니다.
26
+
27
+ ```bash
28
+ curl https://api.roottale.com/v1/cms/posts \
29
+ -H "Authorization: Bearer $ROOTTALE_API_KEY" \
30
+ -H "Content-Type: application/json" \
31
+ -H "Idempotency-Key: post-2026-07-17-001" \
32
+ -d '{
33
+ "type": "post",
34
+ "title": "자동화로 작성한 글",
35
+ "slug": "automated-post",
36
+ "body_json": {"type":"doc","content":[]},
37
+ "featured_media_id": null,
38
+ "category_ids": [],
39
+ "tag_ids": []
40
+ }'
41
+ ```
42
+
43
+ 주요 필드:
44
+
45
+ | 필드 | 설명 |
46
+ |---|---|
47
+ | `body_json` | Tiptap 문서 JSON 객체 |
48
+ | `featured_media_id` | 미디어 업로드 완료 응답의 `id`; `null`이면 썸네일 없음 |
49
+ | `publish` | `true`면 즉시 발행. 생략하면 초안 |
50
+ | `scheduled_at` | 미래 ISO 8601 시각. `publish`와 동시 사용 불가 |
51
+ | `category_ids`, `tag_ids` | 생성과 동시에 연결할 term ID 배열 |
52
+
53
+ ### PATCH /v1/cms/posts/{post_id}
54
+
55
+ 보낸 필드만 수정합니다. `featured_media_id: null`이면 썸네일을 해제하고,
56
+ `scheduled_at: null`이면 예약을 해제합니다.
57
+
58
+ ### POST /v1/cms/posts/{post_id}/publish
59
+
60
+ 초안 또는 예약 글을 즉시 발행하고 등록된 revalidate 웹훅을 전송합니다.
61
+ 본문은 `{}` 또는 `{"site_id":"..."}`입니다.
62
+
63
+ ### POST /v1/cms/posts/{post_id}/unpublish
64
+
65
+ 발행 글을 초안으로 되돌립니다.
66
+
67
+ ### PATCH /v1/cms/posts/{post_id}/terms
68
+
69
+ 특정 taxonomy의 연결을 전체 교체합니다.
70
+
71
+ ```json
72
+ {"taxonomy":"category","term_ids":["term-id-1","term-id-2"]}
73
+ ```
74
+
75
+ ### GET /v1/cms/posts
76
+
77
+ 초안·예약·비공개·발행 글을 조회합니다. 쿼리는 `limit`, `cursor`, `type`,
78
+ `status`, `site_id`를 지원합니다.
79
+
80
+ ## 미디어 업로드 API
81
+
82
+ 파일 바이트를 API Worker에 직접 통과시키지 않고, 5분 유효한 R2 서명 URL로
83
+ 업로드한 뒤 CMS에 등록합니다. 허용 형식은 JPEG, PNG, WebP, GIF, PDF이며
84
+ 최대 10MB입니다.
85
+
86
+ ### 1. POST /v1/cms/media/uploads
87
+
88
+ ```json
89
+ {
90
+ "original_name": "hero.webp",
91
+ "content_type": "image/webp",
92
+ "size_bytes": 482031
93
+ }
94
+ ```
95
+
96
+ 응답의 `upload_url`, `r2_key`, `expires_in`을 보관합니다.
97
+
98
+ ### 2. 서명 URL에 PUT
99
+
100
+ ```bash
101
+ curl -X PUT "$UPLOAD_URL" \
102
+ -H "Content-Type: image/webp" \
103
+ --data-binary @hero.webp
104
+ ```
105
+
106
+ 1단계에서 선언한 `Content-Type`과 바이트 크기가 정확히 같아야 합니다.
107
+
108
+ ### 3. POST /v1/cms/media/uploads/complete
109
+
110
+ ```json
111
+ {
112
+ "r2_key": "tenant-id/site-id/...",
113
+ "original_name": "hero.webp",
114
+ "content_type": "image/webp",
115
+ "size_bytes": 482031,
116
+ "alt": "서비스 소개 대표 이미지",
117
+ "caption": null
118
+ }
119
+ ```
120
+
121
+ 서버가 R2 객체의 tenant 경로, 크기, 형식을 검증한 뒤 미디어를 등록합니다.
122
+ 응답의 `id`는 글의 `featured_media_id`에, `url`은 Tiptap image 노드의
123
+ `attrs.src`에 사용합니다.
124
+
125
+ ### 미디어 관리
126
+
127
+ | 메서드·경로 | 설명 |
128
+ |---|---|
129
+ | `GET /v1/cms/media` | 최신순 목록. `limit`, `offset`, `site_id` 지원 |
130
+ | `PATCH /v1/cms/media/{media_id}` | `alt`, `caption` 수정 |
131
+ | `DELETE /v1/cms/media/{media_id}` | R2 객체와 미디어 행 삭제 |
132
+
133
+ 미디어 삭제 전 해당 URL을 쓰는 본문과 `featured_media_id` 연결을 먼저
134
+ 교체하세요. 삭제 후 기존 공개 URL은 더 이상 유효하지 않습니다.
13
135
 
14
136
  ## GET /v1/cms/public/posts
15
137
 
package/docs/blog.md CHANGED
@@ -52,7 +52,16 @@ export default function BlogPage() {
52
52
  > <RootTaleBlogList apiKey={apiKey} collection="blog" collections={COLLECTIONS} showCategoryFilter />
53
53
  > ```
54
54
  >
55
- > 자세한 내용은 `collections.md` 참고.
55
+ > **`/blog` 가 선언 섹션이 아니라 "나머지 전부"인 사이트**(증상/질환/치료 같은
56
+ > 아카이브 스트림만 선언)는 반대로 `collections`+**`excludeCollections`** 로 어느
57
+ > 스트림에도 안 속한 글만 렌더하세요:
58
+ >
59
+ > ```tsx
60
+ > <RootTaleBlogList apiKey={apiKey} collections={ARCHIVE_COLLECTIONS} excludeCollections showCategoryFilter />
61
+ > ```
62
+ >
63
+ > 자세한 내용과 `excludeCollections` 오적용 주의(`blog` 자체가 선언 섹션인 사이트에
64
+ > 쓰면 목록이 빈다)는 `collections.md` 참고.
56
65
 
57
66
  ### 상세 페이지
58
67
 
@@ -81,6 +90,11 @@ export default async function PostPage({
81
90
  }
82
91
  ```
83
92
 
93
+ `RootTaleBlogPost`는 기본적으로 글 제목을 `<h1>`으로 출력합니다. 공통 배너나
94
+ 페이지 전용 헤더에서 같은 제목을 직접 마크업한다면 `showTitle={false}`를
95
+ 넘기세요. 이 옵션은 CMS 제목만 생략하며 요약·발행일·작성자와 본문은 그대로
96
+ 렌더합니다.
97
+
84
98
  `relatedPostsCount`(기본 0=off)를 주면 글 하단에 **같은 카테고리 최근 글**을 N개
85
99
  `<nav class="rt-cms-related">` 로 노출합니다(현재 글 제외, 발행일 내림차순).
86
100
  제목은 `relatedPostsTitle`(기본 "관련 글"), 링크는 목록과 동일하게 `postHref`
@@ -120,6 +134,40 @@ export default async function PostPage({
120
134
  경우 `@roottale/cms-core` 의 `extractFaqEntries(doc)` / `faqPageJsonLd(entries)`
121
135
  로 동일한 JSON-LD 를 생성할 수 있습니다.
122
136
 
137
+ ### 다국어 (ADR-0052 A안, W4-6 PR C1/C2)
138
+
139
+ 번역 사이트(어드민에서 "번역 추가"로 언어판을 만든 글)는 `RootTaleBlogList`/
140
+ `RootTaleBlogPost`/`RootTalePage` 에 `locale` prop 을 넘기면 그 언어판만
141
+ fetch 합니다(`fetchPosts`/`fetchPost` 의 `locale` 쿼리 그대로 전달). 미지정
142
+ 시 서버가 사이트 기본 locale 만 반환합니다(하위호환).
143
+
144
+ ```tsx
145
+ // app/[locale]/blog/[slug]/page.tsx — starter 참조 구현 패턴
146
+ const { locale, slug } = await params;
147
+ <RootTaleBlogPost
148
+ apiKey={process.env.ROOTTALE_API_KEY!}
149
+ slugOrId={slug}
150
+ locale={locale}
151
+ showTableOfContents
152
+ />;
153
+ ```
154
+
155
+ - **URL 전략**(권장, starter 참조 구현) — 기본 locale 은 prefix 없는 기존
156
+ 경로(`/blog/{slug}`) 그대로 두고, 나머지 locale 만 `app/[locale]/...` 로
157
+ 별도 트리를 둡니다(`app/[locale]/blog/`·`app/[locale]/blog/[slug]/`·
158
+ `app/[locale]/[slug]/`). 사이트 활성 locale 목록은 별도 API 없이
159
+ `fetchBlogSettings().sitemap.locales`(SSoT = `sites.settingsJson.locales`)
160
+ 로 얻습니다 — 첫 원소가 기본 locale.
161
+ - `<head>` hreflang · 사이트맵 hreflang 은 "SEO 연동" 문서(`seo.md`)의
162
+ "다국어 (hreflang)" 절 참고.
163
+ - **UI 문자열**(cms-i18n, ADR-0034 §3) — `RootTaleBlogList.emptyMessage`/
164
+ `allCategoriesLabel`, `RootTaleBlogPost.relatedPostsTitle`/미발견 안내,
165
+ `RootTalePage` 미발견 안내, `RootTaleTableOfContents.title` 은 명시 prop
166
+ 을 안 주면 `locale` prop 기준으로 자동 번역됩니다(`@roottale/cms-i18n`
167
+ ko/en 사전, `ja`/`zh-hans` 등 사전 없는 locale 은 ko 로 fallback). 글
168
+ 본문·admin UI 는 이 자동 번역 대상이 아닙니다(사람이 직접 작성/ADR-0052
169
+ 범위 밖).
170
+
123
171
  ### 고정 페이지 (회사소개 등)
124
172
 
125
173
  어드민의 고정 페이지(`type: "page"`)는 `RootTalePage`로 렌더링합니다 — 블로그
@@ -223,7 +271,7 @@ export async function generateMetadata({
223
271
 
224
272
  입력은 `{ title, description?, date?, image?, seo? }` 형태면 되고
225
273
  (`getPost` 의 `BlogPostMeta` 가 그대로 호환), 원본 post 를 쓸 땐
226
- `seo: (post.metaJson as { seo?: ... }).seo` 로 넘기세요.
274
+ `seo`로 넘길 값은 `(post.metaJson as { seo?: ... }).seo`입니다.
227
275
 
228
276
  SEO 오버라이드 필드: `title`, `description`, `canonical`, `ogImage`,
229
277
  `noindex`, `nofollow`.
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: 콘텐츠 유형 (Collections) — 공지·블로그 URL 분리
2
+ title: 콘텐츠 유형 (Collections)
3
3
  description: 같은 글 풀을 공지 게시판(/notice)·블로그(/blog) 등 여러 섹션으로 나누는 법. 섹션=글의 collection_key, 카테고리=섹션 안의 주제. URL 구조, 어드민 설정, 연동 코드, slug·301·OG.
4
4
  ---
5
5
 
@@ -30,10 +30,10 @@ description: 같은 글 풀을 공지 게시판(/notice)·블로그(/blog) 등
30
30
  | 경로 | 설명 |
31
31
  |---|---|
32
32
  | `{basePath}` | 섹션 목록 (예: `/notice`, `/blog`) |
33
- | `{basePath}/{slug}` | 글 상세 |
33
+ | `{basePath}/{slug}` | 글 상세 (**`detail` 켠 섹션만** — 아래 "목록 전용 유형" 참고) |
34
34
  | `{basePath}/categories/{slug}` | **주제** 아카이브 (`archives` 켠 섹션만) |
35
- | `/feed.xml` | RSS — `feed` 켠 섹션들의 통합 피드 |
36
- | `/sitemap.xml` | 글마다 **소속 섹션 basePath로** 정확히 매핑 |
35
+ | `/feed.xml` | RSS — `feed` 켠 섹션들의 통합 피드 (단 `detail:false` 섹션 글은 제외) |
36
+ | `/sitemap.xml` | 글마다 **소속 섹션 basePath로** 정확히 매핑 (`detail:false` 섹션 글은 미포함) |
37
37
  | `{basePath}/{slug}/opengraph-image` | 글별 동적 OG 카드 (배선 시) |
38
38
 
39
39
  규칙:
@@ -42,6 +42,38 @@ description: 같은 글 풀을 공지 게시판(/notice)·블로그(/blog) 등
42
42
  - **같은 글이 두 섹션에 안 뜸**: 공지 글을 `/blog/개강안내`로 열면 404, 반대도 404(가드).
43
43
  - **섹션(`collection_key`)이 없는 글**은 sitemap·상세 라우트에서 제외됩니다.
44
44
 
45
+ ## 목록 전용 유형 (`detail: false`) — 상세 페이지 없음
46
+
47
+ 글별 상세 라우트를 만들지 않고 **목록 화면에서만 보여주는** 섹션을 만들 수 있습니다.
48
+ 가장 흔한 예는 **후기**(`/reviews`) — 목록에 전문을 그대로 노출하고, 글마다 별도 URL로
49
+ 들어가는 상세 페이지는 없는 경우입니다.
50
+
51
+ 어드민 "콘텐츠 유형" 편집기에서 각 유형의 **"글별 상세 페이지"** 토글을 끄면 됩니다
52
+ (기본은 켜짐 — 기존 유형은 그대로 상세 페이지를 가집니다). 끄면:
53
+
54
+ - `resolvePostUrl`/`resolvePostPath` (cms-core) 가 이 섹션 글에 대해 **`null`** 을 반환 —
55
+ 글쓰기 화면의 "어디에 올릴까요?" 는 그대로 노출되지만(소속 판정과 상세 URL 존재 여부는
56
+ 별개), sitemap·feed·llms.txt 는 이 글의 **상세 URL 을 발행하지 않습니다**(존재하지 않는
57
+ 페이지로 404 유발 방지).
58
+ - `RootTaleBlogList`/관련 글 카드의 링크는 상세 URL 대신 **그 섹션의 목록 페이지
59
+ (`basePath`)** 로 향합니다 — 카드가 이미 목록에서 전문을 보여주므로 상세 이동 없이도
60
+ 의미가 있습니다.
61
+ - 사이트 코드 변경은 필요 없습니다 — 이 값은 route 팩토리(`createSitemapIndex`/
62
+ `createFeedRoute`/`createLlmsTxtRoute`)가 넘겨받는 `collections` 배열의 값일 뿐입니다.
63
+
64
+ ```ts
65
+ export const COLLECTIONS: RouteCollection[] = [
66
+ { key: "notice", basePath: "/notice", categories: [] },
67
+ { key: "blog", basePath: "/blog", categories: ["column", "news"], feed: true, archives: true },
68
+ // 목록 전용 — 후기는 /reviews 목록에서 전문 노출, 글별 상세 URL 없음.
69
+ { key: "reviews", basePath: "/reviews", categories: [], detail: false },
70
+ ];
71
+ ```
72
+
73
+ > 필드명은 `detail` (camelCase, cms-core `RouteCollection`), 공개 API 응답은
74
+ > `detail`(snake_case 와 동일 철자). 미설정 시 기본값은 `true`(하위 호환 — 기존 사이트는
75
+ > 아무 것도 안 바꿔도 그대로 상세 페이지를 가집니다).
76
+
45
77
  ## 섹션 vs 주제 (자주 헷갈리는 부분)
46
78
 
47
79
  | | 섹션 (collection) | 주제 (category) |
@@ -55,7 +87,7 @@ description: 같은 글 풀을 공지 게시판(/notice)·블로그(/blog) 등
55
87
  > 이전 버전은 *카테고리로 섹션을 추론*했지만, 지금은 섹션이 글에 **명시**됩니다.
56
88
  > 카테고리는 더 이상 어느 섹션에 속하는지를 결정하지 않습니다(주제 아카이브 전용).
57
89
 
58
- ## 어드민에서 설정 (`mysite.roottale.com`)
90
+ ## 어드민에서 설정 (`admin.roottale.com`)
59
91
 
60
92
  **설정 > 콘텐츠 유형** 에서 섹션을 정의합니다. 빈 상태의 "공지 + 블로그 한 번에 만들기"
61
93
  버튼으로 표준 두 섹션을 한 번에 만들 수 있습니다. 각 섹션은:
@@ -67,6 +99,7 @@ description: 같은 글 풀을 공지 게시판(/notice)·블로그(/blog) 등
67
99
  | basePath | URL 앞부분 (`/notice`, `/blog`) |
68
100
  | 주제 | 이 섹션이 제공하는 카테고리(아카이브 범위). 비우면 주제 없음 |
69
101
  | feed / archives / og | RSS 포함 / 주제 아카이브 / 동적 OG |
102
+ | 글별 상세 페이지 (`detail`) | 기본 켜짐. 끄면 목록 전용(상세 URL 없음) — 아래 "목록 전용 유형" 참고 |
70
103
  | 순서 | 메뉴·표시 순서 |
71
104
 
72
105
  **섹션을 하나도 안 만들면 단일 블로그(`/blog`)** 로 동작합니다(설정 전 기본값).
@@ -166,6 +199,8 @@ export const GET = createFeedRoute({ apiKey, siteUrl, title, collections: getCol
166
199
  글만 보이게 합니다. **이걸 안 주면 컴포넌트가 전체 글을 렌더**하므로 공지 글이
167
200
  `/blog` 목록에 섞여 들어옵니다(섹션을 나눈 사이트의 가장 흔한 버그). `collections`
168
201
  를 주면 글 링크도 소속 섹션 basePath 로 자동 라우팅됩니다(공지→`/notice/{slug}`).
202
+ 서버(api-core `collection_key=`)가 fetch 단계에서부터 그 섹션 글만 가져오므로
203
+ `limit`(예: `limit=10`)이 다른 섹션 글에 낭비되지 않습니다.
169
204
 
170
205
  ```tsx
171
206
  import { RootTaleBlogList } from "@roottale/cms-renderer-next/server";
@@ -198,6 +233,41 @@ export default function BlogPage() {
198
233
  `RootTaleBlogCategories` 도 같은 `collection`+`collections` 를 받아 그 섹션의 주제만
199
234
  집계합니다. 카테고리 칩/사이드바를 섹션별로 나눌 때 쓰세요.
200
235
 
236
+ #### `/blog` 가 선언 섹션이 *아닌* 사이트 — `excludeCollections` (네거티브 스코프)
237
+
238
+ 증상/질환/치료처럼 **아카이브 전용 스트림만 선언하고, 나머지("미소속") 글을
239
+ `/blog` 로 보여주는** 사이트도 있습니다. 이런 사이트는 `blog` 라는 섹션 자체가
240
+ 없으므로 `collection="blog"` 로 스코프할 수 없습니다 — 대신 `collections`+
241
+ **`excludeCollections`** 로 "어느 스트림에도 안 속한 글만" 골라냅니다.
242
+
243
+ ```tsx
244
+ // app/blog/page.tsx — 증상/질환/치료 스트림은 따로, 나머지는 블로그
245
+ import { RootTaleBlogList } from "@roottale/cms-renderer-next/server";
246
+ import { ARCHIVE_COLLECTIONS } from "@/lib/collections"; // symptoms/conditions/treatments
247
+
248
+ export default function BlogPage() {
249
+ return (
250
+ <RootTaleBlogList
251
+ apiKey={process.env.ROOTTALE_API_KEY!}
252
+ collections={ARCHIVE_COLLECTIONS}
253
+ excludeCollections
254
+ showCategoryFilter
255
+ />
256
+ );
257
+ }
258
+ ```
259
+
260
+ ⚠️ **`blog` 자체가 선언 섹션인 사이트에는 쓰지 마세요.** `collections` 목록에
261
+ `{ key: "blog", ... }` 가 있고 글의 `collectionKey === "blog"` 라면, 그 글은
262
+ "소속 글"로 판정되어 `excludeCollections` 가 오히려 **전부 제외**해 버립니다(빈
263
+ 목록). 그런 사이트는 위 "섹션 목록 페이지" 예시처럼 포지티브 `collection="blog"`
264
+ 가 정경로입니다.
265
+
266
+ 규칙:
267
+
268
+ - `collection` 과 `excludeCollections` 동시 지정 = **throw**(포지티브/네거티브 배타).
269
+ - `collections` 없이 `excludeCollections` 만 지정 = **throw**(조용한 no-op 금지).
270
+
201
271
  > 동적 basePath(어드민에서 자유 편집)나 catch-all 라우트를 쓰면 `collection` 값을
202
272
  > 요청 경로에서 판정해 넘기세요(아래 "동적 basePath" 참고).
203
273
 
@@ -5,7 +5,7 @@ description: 어드민 "설정 > 주소 이동"에서 정의한 임의 경로
5
5
 
6
6
  # 주소 이동 (커스텀 리다이렉트) 연동
7
7
 
8
- 어드민(mysite.roottale.com)의 **설정 > 주소 이동**에서 운영자가 정의한 임의
8
+ 어드민(admin.roottale.com)의 **설정 > 주소 이동**에서 운영자가 정의한 임의
9
9
  경로 이동 규칙(`/old-event → /promo`)을 사이트 미들웨어로 적용합니다. 코드
10
10
  수정 없이 고객이 직접 규칙을 추가·수정·삭제할 수 있습니다.
11
11
 
@@ -7,11 +7,11 @@ description: API 키 발급, 환경변수 설정, 패키지 설치, 첫 콘텐
7
7
 
8
8
  ## 1. API 키 발급
9
9
 
10
- 1. 어드민(`mysite.roottale.com`) 로그인
10
+ 1. 어드민(`admin.roottale.com`) 로그인
11
11
  2. **설정 > 사이트 연결 키** 메뉴로 이동
12
12
  3. 새 키 발급 — 권한 선택:
13
13
  - **read** (기본): 발행된 콘텐츠 조회만. 외부 사이트 연동은 이걸로 충분
14
- - **read_write**: 쓰기 포함 (자동화 서버용 — 일반 연동에는 불필요)
14
+ - **read_write**: 글 작성·수정·발행, 카테고리/태그, 미디어 업로드 포함
15
15
  4. 발급된 키(`rtlk_cust_` + 24자)는 **발급 직후 1회만 평문 표시**됩니다. 바로
16
16
  복사해 환경변수에 저장하세요.
17
17
 
@@ -80,7 +80,58 @@ console.log(page.items.map((p) => p.slug));
80
80
 
81
81
  `401` 에러(`invalid_key`)면 키 값/환경변수 로딩을 확인하세요.
82
82
 
83
+ ## 5. MCP로 글과 이미지 자동화
84
+
85
+ MCP 설정의 `ROOTTALE_API_KEY`에 `read_write` 키를 넣으면 다음 tool을 쓸 수
86
+ 있습니다.
87
+
88
+ 1. `getSiteKnowledge`로 브랜드 보이스와 금지어 확인
89
+ 2. `uploadCmsMedia`로 썸네일 또는 본문 삽화 업로드
90
+ 3. `createCmsPost`로 Tiptap JSON 초안 생성
91
+ 4. 필요하면 `setCmsPostTerms`로 카테고리·태그 연결
92
+ 5. 검토 후 `publishCmsPost`로 발행
93
+
94
+ 썸네일은 업로드 응답의 `id`를 `featuredMediaId`에 넣습니다. 본문 삽화는
95
+ 응답의 `url`을 Tiptap image 노드에 넣습니다.
96
+
97
+ ```json
98
+ {
99
+ "type": "image",
100
+ "attrs": {
101
+ "src": "https://media.example.com/tenant/site/image.webp",
102
+ "alt": "서비스 진행 과정을 설명하는 삽화",
103
+ "title": null
104
+ }
105
+ }
106
+ ```
107
+
108
+ 안전한 기본값은 초안입니다. 사용자가 즉시 발행을 명시하지 않았다면
109
+ `createCmsPost`의 `publish`를 생략하고 검토 뒤 발행하세요.
110
+
111
+ ## 6. CLI로 자동화
112
+
113
+ 별도 전역 설치 없이 공개 MCP 패키지의 CLI 모드를 사용할 수 있습니다.
114
+
115
+ ```bash
116
+ export ROOTTALE_API_KEY=rtlk_cust_xxxxxxxxxxxxxxxxxxxxxxxx
117
+
118
+ npx -y @roottale/cms-mcp cli media upload ./thumbnail.webp \
119
+ --alt "글 대표 이미지"
120
+
121
+ npx -y @roottale/cms-mcp cli posts create \
122
+ --title "새 글" \
123
+ --slug "new-post" \
124
+ --body-file ./post.json \
125
+ --featured-media-id "<업로드 응답의 id>"
126
+
127
+ npx -y @roottale/cms-mcp cli posts publish "<글 id>"
128
+ ```
129
+
130
+ 내부 운영 저장소에서는 같은 기능을 `rt cms posts ...`,
131
+ `rt cms media ...` 명령으로도 실행할 수 있습니다.
132
+
83
133
  ## 다음 단계
84
134
 
85
135
  - 블로그 페이지 구현 → `blog.md`
86
136
  - 발행 즉시 사이트 반영 → `revalidation-webhooks.md`
137
+ - HTTP 자동화 엔드포인트 → `api-reference.md`
package/docs/menus.md CHANGED
@@ -5,7 +5,7 @@ description: 어드민 "디자인 > 메뉴"에서 관리하는 네비게이션
5
5
 
6
6
  # 메뉴 (네비게이션) 연동
7
7
 
8
- 어드민(mysite.roottale.com)의 **디자인 > 메뉴**에서 저장한 네비게이션 트리를
8
+ 어드민(admin.roottale.com)의 **디자인 > 메뉴**에서 저장한 네비게이션 트리를
9
9
  사이트의 헤더·푸터에 렌더합니다. 고객이 직접 메뉴 항목(이름·주소·순서·하위
10
10
  항목)을 바꿀 수 있어 코드 수정 없이 네비가 갱신됩니다.
11
11