@roottale/cms-mcp 0.42.0 → 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/CHANGELOG.md +14 -0
- package/README.md +41 -7
- package/dist/index.js +589 -58
- package/dist/index.js.map +1 -1
- package/docs/api-reference.md +124 -2
- package/docs/blog.md +5 -0
- package/docs/collections.md +38 -3
- package/docs/getting-started.md +52 -1
- package/docs/overview.md +9 -9
- package/examples/nextjs/app/blog/[slug]/page.tsx +2 -0
- package/package.json +4 -2
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"]}
|
package/docs/api-reference.md
CHANGED
|
@@ -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
|
|
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
|
@@ -90,6 +90,11 @@ export default async function PostPage({
|
|
|
90
90
|
}
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
`RootTaleBlogPost`는 기본적으로 글 제목을 `<h1>`으로 출력합니다. 공통 배너나
|
|
94
|
+
페이지 전용 헤더에서 같은 제목을 직접 마크업한다면 `showTitle={false}`를
|
|
95
|
+
넘기세요. 이 옵션은 CMS 제목만 생략하며 요약·발행일·작성자와 본문은 그대로
|
|
96
|
+
렌더합니다.
|
|
97
|
+
|
|
93
98
|
`relatedPostsCount`(기본 0=off)를 주면 글 하단에 **같은 카테고리 최근 글**을 N개
|
|
94
99
|
`<nav class="rt-cms-related">` 로 노출합니다(현재 글 제외, 발행일 내림차순).
|
|
95
100
|
제목은 `relatedPostsTitle`(기본 "관련 글"), 링크는 목록과 동일하게 `postHref`
|
package/docs/collections.md
CHANGED
|
@@ -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) |
|
|
@@ -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";
|
package/docs/getting-started.md
CHANGED
|
@@ -11,7 +11,7 @@ description: API 키 발급, 환경변수 설정, 패키지 설치, 첫 콘텐
|
|
|
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/overview.md
CHANGED
|
@@ -9,6 +9,15 @@ RootTale CMS는 어드민(`admin.roottale.com`)에서 콘텐츠를 작성·발
|
|
|
9
9
|
고객 사이트(자체 도메인의 Next.js/Astro 등)가 공개 API(`api.roottale.com`)로
|
|
10
10
|
콘텐츠를 가져가는 헤드리스 구조입니다.
|
|
11
11
|
|
|
12
|
+
## 권장 연동 순서
|
|
13
|
+
|
|
14
|
+
1. `getting-started.md` — 키 발급 + 환경 설정
|
|
15
|
+
2. `blog.md` — `/blog` 목록·상세 페이지
|
|
16
|
+
3. `revalidation-webhooks.md` — 웹훅 등록 (발행 → 즉시 반영)
|
|
17
|
+
4. `seo.md` — RSS·사이트맵·동적 OG 이미지
|
|
18
|
+
5. `inquiries.md` — 상담문의 폼 (선택)
|
|
19
|
+
6. `menus.md` — 어드민 관리 네비게이션 (선택)
|
|
20
|
+
|
|
12
21
|
```
|
|
13
22
|
어드민 (admin.roottale.com) 고객 사이트 (예: example.com)
|
|
14
23
|
글 작성·발행 ──────────┐
|
|
@@ -53,12 +62,3 @@ RootTale CMS는 어드민(`admin.roottale.com`)에서 콘텐츠를 작성·발
|
|
|
53
62
|
| `seo.md` | RSS 피드, 사이트맵, JSON-LD, 동적 OG 이미지, 공개 검색, fleet 프로브 |
|
|
54
63
|
| `theme-and-settings.md` | 디자인 토큰, 블로그 표시 설정, 분석 태그 |
|
|
55
64
|
| `api-reference.md` | HTTP API 레퍼런스 (비 JS 스택용 raw 엔드포인트) |
|
|
56
|
-
|
|
57
|
-
## 권장 연동 순서
|
|
58
|
-
|
|
59
|
-
1. `getting-started.md` — 키 발급 + 환경 설정
|
|
60
|
-
2. `blog.md` — `/blog` 목록·상세 페이지
|
|
61
|
-
3. `revalidation-webhooks.md` — 웹훅 등록 (발행 → 즉시 반영)
|
|
62
|
-
4. `seo.md` — RSS·사이트맵·동적 OG 이미지
|
|
63
|
-
5. `inquiries.md` — 상담문의 폼 (선택)
|
|
64
|
-
6. `menus.md` — 어드민 관리 네비게이션 (선택)
|
|
@@ -41,10 +41,12 @@ export default async function PostPage({ params }: Props) {
|
|
|
41
41
|
|
|
42
42
|
return (
|
|
43
43
|
<main>
|
|
44
|
+
<h1>{post.title}</h1>
|
|
44
45
|
<RootTaleBlogPost
|
|
45
46
|
apiKey={process.env.ROOTTALE_API_KEY!}
|
|
46
47
|
baseUrl={process.env.ROOTTALE_API_BASE}
|
|
47
48
|
slugOrId={post.id}
|
|
49
|
+
showTitle={false}
|
|
48
50
|
/>
|
|
49
51
|
</main>
|
|
50
52
|
);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roottale/cms-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "RootTale CMS
|
|
5
|
+
"description": "RootTale CMS MCP server and CLI for post publishing, media uploads, integration docs, and public API access.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"roottale-cms-mcp": "dist/index.js"
|
|
8
8
|
},
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
19
|
+
"commander": "^13.0.0",
|
|
20
|
+
"ky": "^1.14.0",
|
|
19
21
|
"zod": "^3.23.8"
|
|
20
22
|
},
|
|
21
23
|
"devDependencies": {
|