@roottale/cms-mcp 0.22.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 ADDED
@@ -0,0 +1,15 @@
1
+ # @roottale/cms-mcp
2
+
3
+ > cms-* linked 그룹과 버전을 함께 올린다. 변경 시 `pnpm changeset` 으로
4
+ > 업데이트 로그를 남기면 Version Packages PR 이 본 파일에 자동 반영한다.
5
+
6
+ ## 0.22.0
7
+
8
+ - 최초 릴리스 — RootTale CMS 연동 MCP 서버 (stdio). cms-* 라인(renderer-next
9
+ 0.22.0)에 버전 정렬.
10
+ - `blog.md`에 고정 페이지 렌더러 `RootTalePage` (renderer-next 0.22.0) 반영
11
+ - 통합 문서 번들 (`docs/`): 개요, 시작하기, 블로그, 발행 웹훅, 상담문의,
12
+ SEO, 테마/설정, HTTP API 레퍼런스
13
+ - Next.js 연동 예시 코드 번들 (`examples/nextjs/`)
14
+ - Tools: `listRootTaleDocs`, `readRootTaleDoc`, `searchRootTaleDocs`,
15
+ `readRootTaleNextjsExampleCode`, `listPublishedPosts`, `getPublishedPost`
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # @roottale/cms-mcp
2
+
3
+ RootTale CMS 연동 MCP 서버 — AI 코딩 에이전트(Claude Code, Cursor 등)가
4
+ 통합 문서를 하나하나 복사해 붙여넣지 않고도 RootTale CMS를 외부 사이트에
5
+ 연동할 수 있게 합니다.
6
+
7
+ 번들 내용:
8
+
9
+ - **통합 문서** (`docs/`) — 시작하기, 블로그, 발행 웹훅, 상담문의, SEO,
10
+ 테마/설정, HTTP API 레퍼런스
11
+ - **Next.js 예시 코드** (`examples/nextjs/`) — 블로그 목록/상세, revalidate
12
+ 라우트, RSS/사이트맵, 상담문의 서버 액션 등 완전한 연동 1세트
13
+ - **공개 API 조회 tool** — 연동 검증용 발행 글 목록/상세 조회
14
+
15
+ ## 설정
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "roottale-cms": {
21
+ "command": "npx",
22
+ "args": ["-y", "@roottale/cms-mcp"],
23
+ "env": {
24
+ "ROOTTALE_API_KEY": "rtlk_cust_..."
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ `ROOTTALE_API_KEY`는 선택입니다 — 문서·예시 코드 tool은 키 없이 동작하고,
32
+ `listPublishedPosts` / `getPublishedPost` (연동 검증) tool만 키가 필요합니다.
33
+ 키 발급: 어드민(`mysite.roottale.com`) **설정 > 사이트 연결 키**.
34
+
35
+ Claude Code:
36
+
37
+ ```bash
38
+ claude mcp add roottale-cms -- npx -y @roottale/cms-mcp
39
+ ```
40
+
41
+ ## Tools
42
+
43
+ | Tool | 설명 |
44
+ |---|---|
45
+ | `listRootTaleDocs` | 통합 문서 목록 (경로·제목·설명) |
46
+ | `readRootTaleDoc` | 문서 1개 읽기 |
47
+ | `searchRootTaleDocs` | 문서 전체 정규식 검색 |
48
+ | `readRootTaleNextjsExampleCode` | Next.js 연동 예시 코드 전체 |
49
+ | `listPublishedPosts` | 공개 API로 발행 글 목록 조회 (키 필요) |
50
+ | `getPublishedPost` | 공개 API로 발행 글 1개 조회 (키 필요) |
51
+
52
+ ## 사람용 문서
53
+
54
+ 문서 원문은 패키지의 [`docs/`](./docs/overview.md) 폴더에서 직접 읽을 수
55
+ 있습니다. 진입점: `docs/overview.md`.
56
+
57
+ ## License
58
+
59
+ Proprietary (UNLICENSED). RootTale 고객 계약 하에 제공됩니다.
package/dist/index.js ADDED
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/server.ts
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+
9
+ // src/tools.ts
10
+ import {
11
+ CallToolRequestSchema,
12
+ ErrorCode,
13
+ ListToolsRequestSchema,
14
+ McpError
15
+ } from "@modelcontextprotocol/sdk/types.js";
16
+ import { z } from "zod";
17
+
18
+ // src/docs.ts
19
+ import { readdir, readFile } from "fs/promises";
20
+ import path from "path";
21
+ import { fileURLToPath } from "url";
22
+ var PKG_ROOT = fileURLToPath(new URL("..", import.meta.url));
23
+ var DOCS_ROOT = path.join(PKG_ROOT, "docs");
24
+ var EXAMPLES_ROOT = path.join(PKG_ROOT, "examples");
25
+ function parseFrontmatter(markdown) {
26
+ const result = { title: "", description: "" };
27
+ const match = markdown.match(/^---\n([\s\S]*?)\n---/);
28
+ if (!match) return result;
29
+ for (const line of match[1].split("\n")) {
30
+ const idx = line.indexOf(":");
31
+ if (idx === -1) continue;
32
+ const key = line.slice(0, idx).trim();
33
+ const value = line.slice(idx + 1).trim();
34
+ if (key === "title") result.title = value;
35
+ if (key === "description") result.description = value;
36
+ }
37
+ return result;
38
+ }
39
+ async function walkFiles(root, dir = root) {
40
+ const entries = await readdir(dir, { withFileTypes: true });
41
+ const files = [];
42
+ for (const entry of entries) {
43
+ const full = path.join(dir, entry.name);
44
+ if (entry.isDirectory()) {
45
+ files.push(...await walkFiles(root, full));
46
+ } else {
47
+ files.push(path.relative(root, full));
48
+ }
49
+ }
50
+ return files.sort();
51
+ }
52
+ async function listDocs() {
53
+ const files = (await walkFiles(DOCS_ROOT)).filter((f) => f.endsWith(".md"));
54
+ return Promise.all(
55
+ files.map(async (rel) => {
56
+ const content = await readFile(path.join(DOCS_ROOT, rel), "utf8");
57
+ const fm = parseFrontmatter(content);
58
+ return { path: rel, title: fm.title || rel, description: fm.description };
59
+ })
60
+ );
61
+ }
62
+ async function readDoc(relPath) {
63
+ const resolved = path.resolve(DOCS_ROOT, relPath);
64
+ if (!resolved.startsWith(DOCS_ROOT + path.sep)) return null;
65
+ try {
66
+ return await readFile(resolved, "utf8");
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ async function searchDocs(query) {
72
+ const regex = new RegExp(query, "i");
73
+ const matches = [];
74
+ for (const doc of await listDocs()) {
75
+ const content = await readDoc(doc.path);
76
+ if (!content) continue;
77
+ content.split("\n").forEach((text, idx) => {
78
+ if (regex.test(text)) {
79
+ matches.push({ path: doc.path, line: idx + 1, text: text.trim() });
80
+ }
81
+ });
82
+ }
83
+ return matches;
84
+ }
85
+ async function readNextjsExampleCode() {
86
+ const root = path.join(EXAMPLES_ROOT, "nextjs");
87
+ const files = await walkFiles(root);
88
+ const sections = await Promise.all(
89
+ files.map(async (rel) => {
90
+ const content = await readFile(path.join(root, rel), "utf8");
91
+ return `--- FILE: ${rel} ---
92
+ ${content}`;
93
+ })
94
+ );
95
+ return sections.join("\n\n");
96
+ }
97
+
98
+ // src/tools.ts
99
+ var DEFAULT_API_BASE = "https://api.roottale.com";
100
+ var ReadDocInput = z.object({ path: z.string().min(1) }).strict();
101
+ var SearchDocsInput = z.object({ query: z.string().min(1) }).strict();
102
+ var ListPostsInput = z.object({
103
+ limit: z.number().int().min(1).max(100).optional(),
104
+ type: z.enum(["post", "page"]).optional(),
105
+ cursor: z.string().optional()
106
+ }).strict();
107
+ var GetPostInput = z.object({ slugOrId: z.string().min(1) }).strict();
108
+ var TOOL_DEFINITIONS = [
109
+ {
110
+ name: "listRootTaleDocs",
111
+ description: "RootTale CMS \uD1B5\uD569 \uBB38\uC11C \uC804\uCCB4 \uBAA9\uB85D(\uACBD\uB85C\xB7\uC81C\uBAA9\xB7\uC124\uBA85)\uC744 \uBC18\uD658\uD569\uB2C8\uB2E4. RootTale \uC5F0\uB3D9 \uC791\uC5C5\uC744 \uC2DC\uC791\uD558\uAE30 \uC804 \uAC00\uC7A5 \uBA3C\uC800 \uD638\uCD9C\uD558\uC138\uC694.",
112
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
113
+ },
114
+ {
115
+ name: "readRootTaleDoc",
116
+ description: "RootTale CMS \uD1B5\uD569 \uBB38\uC11C 1\uAC1C\uB97C \uACBD\uB85C\uB85C \uC77D\uC2B5\uB2C8\uB2E4. \uACBD\uB85C\uB294 listRootTaleDocs \uAC00 \uBC18\uD658\uD55C path \uAC12\uC744 \uC0AC\uC6A9\uD558\uC138\uC694.",
117
+ inputSchema: {
118
+ type: "object",
119
+ properties: {
120
+ path: { type: "string", description: "\uBB38\uC11C \uACBD\uB85C (\uC608: blog.md)" }
121
+ },
122
+ required: ["path"],
123
+ additionalProperties: false
124
+ }
125
+ },
126
+ {
127
+ name: "searchRootTaleDocs",
128
+ description: "RootTale CMS \uD1B5\uD569 \uBB38\uC11C \uC804\uCCB4\uC5D0\uC11C \uC815\uADDC\uC2DD\uC73C\uB85C \uAC80\uC0C9\uD569\uB2C8\uB2E4 (\uB300\uC18C\uBB38\uC790 \uBB34\uC2DC). \uB9E4\uCE58\uB41C \uD30C\uC77C \uACBD\uB85C\xB7\uB77C\uC778 \uBC88\uD638\xB7\uB77C\uC778 \uB0B4\uC6A9\uC744 \uBC18\uD658\uD569\uB2C8\uB2E4.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ query: { type: "string", description: "\uC815\uADDC\uC2DD \uD328\uD134 (\uC608: revalidate|webhook)" }
133
+ },
134
+ required: ["query"],
135
+ additionalProperties: false
136
+ }
137
+ },
138
+ {
139
+ name: "readRootTaleNextjsExampleCode",
140
+ description: "RootTale CMS\uB97C \uC678\uBD80 Next.js(App Router) \uC0AC\uC774\uD2B8\uC5D0 \uC5F0\uB3D9\uD558\uB294 \uC644\uC804\uD55C \uC608\uC2DC \uCF54\uB4DC \uC138\uD2B8\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4 (\uBE14\uB85C\uADF8 \uBAA9\uB85D/\uC0C1\uC138, \uC6F9\uD6C5 revalidate \uB77C\uC6B0\uD2B8, RSS/\uC0AC\uC774\uD2B8\uB9F5, \uC0C1\uB2F4\uBB38\uC758 \uC11C\uBC84 \uC561\uC158 \uB4F1). \uC5F0\uB3D9 \uCF54\uB4DC\uB97C \uC791\uC131\uD558\uAE30 \uC804 \uBC18\uB4DC\uC2DC \uD638\uCD9C\uD574 \uD328\uD134\uC744 \uB530\uB77C \uC791\uC131\uD558\uC138\uC694.",
141
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
142
+ },
143
+ {
144
+ name: "listPublishedPosts",
145
+ description: "\uC5F0\uB3D9 \uAC80\uC99D\uC6A9 \u2014 RootTale \uACF5\uAC1C CMS API(GET /v1/cms/public/posts)\uB85C \uBC1C\uD589\uB41C \uAE00 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uD658\uACBD\uBCC0\uC218 ROOTTALE_API_KEY(rtlk_cust_*)\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
146
+ inputSchema: {
147
+ type: "object",
148
+ properties: {
149
+ limit: { type: "number", description: "1-100, \uAE30\uBCF8 20" },
150
+ type: { type: "string", enum: ["post", "page"] },
151
+ cursor: { type: "string", description: "\uC774\uC804 \uC751\uB2F5\uC758 next_cursor" }
152
+ },
153
+ additionalProperties: false
154
+ }
155
+ },
156
+ {
157
+ name: "getPublishedPost",
158
+ description: "\uC5F0\uB3D9 \uAC80\uC99D\uC6A9 \u2014 RootTale \uACF5\uAC1C CMS API(GET /v1/cms/public/posts/:identifier)\uB85C \uBC1C\uD589\uB41C \uAE00 1\uAC1C\uB97C slug \uB610\uB294 UUID\uB85C \uC870\uD68C\uD569\uB2C8\uB2E4. \uD658\uACBD\uBCC0\uC218 ROOTTALE_API_KEY(rtlk_cust_*)\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
159
+ inputSchema: {
160
+ type: "object",
161
+ properties: {
162
+ slugOrId: { type: "string", description: "\uAE00 slug \uB610\uB294 UUID" }
163
+ },
164
+ required: ["slugOrId"],
165
+ additionalProperties: false
166
+ }
167
+ }
168
+ ];
169
+ function requireApiKey() {
170
+ const apiKey = process.env.ROOTTALE_API_KEY;
171
+ if (!apiKey) {
172
+ throw new McpError(
173
+ ErrorCode.InvalidRequest,
174
+ "ROOTTALE_API_KEY \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. MCP \uC11C\uBC84 \uC124\uC815\uC758 env\uC5D0 rtlk_cust_* \uD0A4\uB97C \uCD94\uAC00\uD558\uC138\uC694 (\uBC1C\uAE09: \uC5B4\uB4DC\uBBFC \uC124\uC815 > \uC0AC\uC774\uD2B8 \uC5F0\uACB0 \uD0A4)."
175
+ );
176
+ }
177
+ const baseUrl = (process.env.ROOTTALE_API_BASE ?? DEFAULT_API_BASE).replace(/\/+$/, "");
178
+ return { apiKey, baseUrl };
179
+ }
180
+ async function fetchPublicApi(pathname, params) {
181
+ const { apiKey, baseUrl } = requireApiKey();
182
+ const url = new URL(`${baseUrl}${pathname}`);
183
+ for (const [key, value] of Object.entries(params ?? {})) {
184
+ url.searchParams.set(key, value);
185
+ }
186
+ const response = await fetch(url, {
187
+ headers: { authorization: `Bearer ${apiKey}` }
188
+ });
189
+ const body = await response.text();
190
+ if (!response.ok) {
191
+ throw new Error(`RootTale API ${response.status}: ${body.slice(0, 500)}`);
192
+ }
193
+ return JSON.parse(body);
194
+ }
195
+ function textResponse(text) {
196
+ return { content: [{ type: "text", text }] };
197
+ }
198
+ function jsonResponse(value) {
199
+ return textResponse(JSON.stringify(value, null, 2));
200
+ }
201
+ function registerTools(server2) {
202
+ server2.setRequestHandler(ListToolsRequestSchema, async () => ({
203
+ tools: TOOL_DEFINITIONS.map((t) => ({ ...t }))
204
+ }));
205
+ server2.setRequestHandler(CallToolRequestSchema, async (request) => {
206
+ const args = request.params.arguments ?? {};
207
+ try {
208
+ switch (request.params.name) {
209
+ case "listRootTaleDocs":
210
+ return jsonResponse(await listDocs());
211
+ case "readRootTaleDoc": {
212
+ const input = ReadDocInput.parse(args);
213
+ const content = await readDoc(input.path);
214
+ if (content === null) {
215
+ throw new McpError(
216
+ ErrorCode.InvalidParams,
217
+ `\uBB38\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path} \u2014 listRootTaleDocs\uB85C \uACBD\uB85C\uB97C \uD655\uC778\uD558\uC138\uC694.`
218
+ );
219
+ }
220
+ return textResponse(content);
221
+ }
222
+ case "searchRootTaleDocs": {
223
+ const input = SearchDocsInput.parse(args);
224
+ return jsonResponse(await searchDocs(input.query));
225
+ }
226
+ case "readRootTaleNextjsExampleCode":
227
+ return textResponse(await readNextjsExampleCode());
228
+ case "listPublishedPosts": {
229
+ const input = ListPostsInput.parse(args);
230
+ const params = {};
231
+ if (input.limit) params.limit = String(input.limit);
232
+ if (input.type) params.type = input.type;
233
+ if (input.cursor) params.cursor = input.cursor;
234
+ return jsonResponse(await fetchPublicApi("/v1/cms/public/posts", params));
235
+ }
236
+ case "getPublishedPost": {
237
+ const input = GetPostInput.parse(args);
238
+ return jsonResponse(
239
+ await fetchPublicApi(
240
+ `/v1/cms/public/posts/${encodeURIComponent(input.slugOrId)}`
241
+ )
242
+ );
243
+ }
244
+ default:
245
+ throw new McpError(
246
+ ErrorCode.InvalidParams,
247
+ `Unknown RootTale tool: ${request.params.name}`
248
+ );
249
+ }
250
+ } catch (error) {
251
+ if (error instanceof McpError) throw error;
252
+ const message = error instanceof Error ? error.message : String(error);
253
+ return { isError: true, content: [{ type: "text", text: message }] };
254
+ }
255
+ });
256
+ }
257
+
258
+ // src/server.ts
259
+ var VERSION = true ? "0.22.0" : "dev";
260
+ var SERVER_INSTRUCTIONS = `
261
+ roottale-cms-mcp\uB294 RootTale CMS\uB97C \uC678\uBD80 \uC0AC\uC774\uD2B8(\uC8FC\uB85C Next.js)\uC5D0 \uC5F0\uB3D9\uD558\uAE30 \uC704\uD55C
262
+ \uD1B5\uD569 \uBB38\uC11C\xB7\uC608\uC2DC \uCF54\uB4DC\xB7\uACF5\uAC1C API \uC870\uD68C tool\uC744 \uC81C\uACF5\uD569\uB2C8\uB2E4.
263
+
264
+ \uC9C0\uCF1C\uC57C \uD560 \uADDC\uCE59:
265
+ - RootTale \uC5F0\uB3D9 \uAD00\uB828 \uC791\uC5C5\uC740 \uAC00\uC7A5 \uBA3C\uC800 listRootTaleDocs\uB97C \uD638\uCD9C\uD574 \uBB38\uC11C \uBAA9\uB85D\uC744 \uD30C\uC545\uD558\uC138\uC694.
266
+ - \uC5F0\uB3D9 \uCF54\uB4DC\uB97C \uC791\uC131\uD558\uAE30 \uC804 \uBC18\uB4DC\uC2DC readRootTaleNextjsExampleCode\uB97C \uD638\uCD9C\uD574 \uC608\uC2DC \uCF54\uB4DC\uB97C
267
+ \uCC38\uACE0\uD55C \uD6C4 \uADF8 \uD328\uD134\uC744 \uB530\uB77C \uC791\uC131\uD558\uC138\uC694.
268
+ - API \uD0A4(rtlk_cust_*)\uB294 \uC11C\uBC84 \uC804\uC6A9\uC785\uB2C8\uB2E4. NEXT_PUBLIC_* \uB4F1 \uBE0C\uB77C\uC6B0\uC800\uB85C \uB178\uCD9C\uB418\uB294
269
+ \uD658\uACBD\uBCC0\uC218\uC5D0 \uC808\uB300 \uB123\uC9C0 \uB9C8\uC138\uC694.
270
+ - \uC774\uBBF8 \uD559\uC2B5\uB41C \uB0B4\uC6A9\uC774\uB354\uB77C\uB3C4 \uBCF8 \uC11C\uBC84\uC758 \uBB38\uC11C\uB85C \uB354\uBE14\uCCB4\uD06C \uD6C4 \uC791\uC5C5\uD558\uC138\uC694.
271
+ `.trim();
272
+ function createServer() {
273
+ const server2 = new Server(
274
+ { name: "roottale-cms", version: VERSION },
275
+ { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
276
+ );
277
+ registerTools(server2);
278
+ return server2;
279
+ }
280
+
281
+ // src/index.ts
282
+ var server = createServer();
283
+ var transport = new StdioServerTransport();
284
+ await server.connect(transport);
285
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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();\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] 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 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"]}
@@ -0,0 +1,117 @@
1
+ ---
2
+ title: HTTP API 레퍼런스
3
+ description: 공개 API raw 엔드포인트 — JS 외 스택이나 저수준 연동용
4
+ ---
5
+
6
+ # HTTP API 레퍼런스
7
+
8
+ 베이스 URL: `https://api.roottale.com`
9
+ 인증: 모든 요청에 `Authorization: Bearer rtlk_cust_...` 헤더.
10
+
11
+ JS/TS 스택은 raw 호출 대신 `@roottale/cms-client`를 사용하세요 — 에러 처리,
12
+ 브라우저 노출 방지, 전화번호 포맷 등이 포함되어 있습니다.
13
+
14
+ ## GET /v1/cms/public/posts
15
+
16
+ 발행된 글 목록 (커서 페이지네이션).
17
+
18
+ | 쿼리 | 설명 |
19
+ |---|---|
20
+ | `limit` | 페이지 크기 (서버에서 상한 clamp) |
21
+ | `cursor` | 이전 응답의 `next_cursor` |
22
+ | `type` | `post` \| `page` |
23
+ | `site_id` | 멀티 사이트 키일 때만 — site-scoped 키면 생략 |
24
+
25
+ ```json
26
+ {
27
+ "items": [ { "id": "…", "slug": "…", "title": "…", "body_json": {…},
28
+ "terms": [{ "taxonomy": "category", "name": "…", "slug": "…" }],
29
+ "published_at": "…" } ],
30
+ "has_more": false,
31
+ "next_cursor": null
32
+ }
33
+ ```
34
+
35
+ ## GET /v1/cms/public/posts/{identifier}
36
+
37
+ 글 1개 — `identifier`는 slug 또는 UUID. 미발행/없는 글은 `404`.
38
+
39
+ ## GET /v1/cms/public/theme
40
+
41
+ 어드민에서 설정한 디자인 토큰. 설정된 그룹만 포함됩니다.
42
+
43
+ ```json
44
+ { "tenant_id": "…", "site_id": "…",
45
+ "colors": {…}, "fonts": {…}, "radius": {…}, "updated_at": "…" }
46
+ ```
47
+
48
+ ## GET /v1/cms/public/blog-settings
49
+
50
+ 블로그 표시 설정 (TOC·작성자·발행일·작성자 카드, 저자 프로필).
51
+
52
+ ```json
53
+ { "show_table_of_contents": false, "show_author": true, "show_date": true,
54
+ "show_author_card": true, "toc_title": null,
55
+ "author_profile_name": null, "author_profile_bio": null,
56
+ "author_profile_image_url": null, "author_profile_image_radius": "circle",
57
+ "updated_at": null }
58
+ ```
59
+
60
+ ## GET /v1/cms/public/analytics
61
+
62
+ 분석 태그 설정.
63
+
64
+ ```json
65
+ { "tags": [ { "provider": "ga4", "id": "G-XXXXXXX", "enabled": true } ] }
66
+ ```
67
+
68
+ `provider`: `ga4` | `clarity` | `meta_pixel` | `naver`.
69
+
70
+ ## GET /v1/cms/public/jwks
71
+
72
+ 웹훅 서명 검증용 site-scoped JWKS 공개키. **site-scoped 키 필수** — 테넌트
73
+ 전체 키로 호출하면 `400 site_scope_required`.
74
+
75
+ ## POST /v1/public/inquiries
76
+
77
+ 상담문의(리드) 접수. `multipart/form-data`.
78
+
79
+ | 필드 | 필수 | 비고 |
80
+ |---|---|---|
81
+ | `vertical` | ✅ | `consulting` \| `medical` \| `tax` \| `legal` |
82
+ | `contact_name` | ✅ | |
83
+ | `business_name` | ✅ | |
84
+ | `email` | ✅ | `.+@.+\..+` |
85
+ | `phone` | ✅ | |
86
+ | `privacy_consent` | ✅ | 체크박스 (`on`) |
87
+ | `overseas_transfer_consent` | medical 시 ✅ | |
88
+ | `message`, `consultation_field`, `current_site_url` | | |
89
+ | `lead_kind` | | `patient`(기본) \| `sales` |
90
+ | `_redirect_url` | | 완료 후 redirect base (allowlist 검증) |
91
+ | 기타 임의 필드 | | 최대 50개, 암호화 보관, CRM 상세 노출 |
92
+
93
+ 응답: `302` redirect — 성공 `?ok=1`, 실패 `?err=<code>`
94
+ (`consent_privacy`, `consent_overseas`, `invalid_vertical`, `missing_fields`,
95
+ `invalid_email`, `internal`). 잘못된 키는 `401`.
96
+
97
+ 서버-서버 호출 시 redirect를 따라가지 말고(`redirect: "manual"`) `Location`
98
+ 헤더를 파싱하세요 — `@roottale/cms-client`의 `submitInquiry`가 이를 대신합니다.
99
+
100
+ ## POST /v1/cms/revalidate
101
+
102
+ 수동 캐시 갱신 트리거 (등록된 웹훅으로 재발송).
103
+
104
+ ```json
105
+ { "event": "post.updated", "paths": ["/blog", "/blog/my-post"], "slug": "my-post" }
106
+ ```
107
+
108
+ ## 에러 형식
109
+
110
+ 비 2xx 응답은 JSON 에러 바디(`code`, `message`)를 가집니다. 주요 코드:
111
+ `invalid_key`(401), `insufficient_scope`(403), `rate_limited`(429),
112
+ `not_found`(404).
113
+
114
+ ## 캐시 헤더
115
+
116
+ 공개 콘텐츠 응답은 private 캐시 헤더로 내려갑니다 — CDN 공유 캐시에 의존하지
117
+ 말고 사이트 측 ISR + 발행 웹훅 조합을 사용하세요.