@roottale/cms-mcp
Version:
RootTale CMS integration MCP server — bundled integration docs, Next.js example code, and public API lookup tools. Run with: npx @roottale/cms-mcp
285 lines (277 loc) • 12 kB
JavaScript
// src/index.ts
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// src/server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
// src/tools.ts
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
// src/docs.ts
import { readdir, readFile } from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
var PKG_ROOT = fileURLToPath(new URL("..", import.meta.url));
var DOCS_ROOT = path.join(PKG_ROOT, "docs");
var EXAMPLES_ROOT = path.join(PKG_ROOT, "examples");
function parseFrontmatter(markdown) {
const result = { title: "", description: "" };
const match = markdown.match(/^---\n([\s\S]*?)\n---/);
if (!match) return result;
for (const line of match[1].split("\n")) {
const idx = line.indexOf(":");
if (idx === -1) continue;
const key = line.slice(0, idx).trim();
const value = line.slice(idx + 1).trim();
if (key === "title") result.title = value;
if (key === "description") result.description = value;
}
return result;
}
async function walkFiles(root, dir = root) {
const entries = await readdir(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...await walkFiles(root, full));
} else {
files.push(path.relative(root, full));
}
}
return files.sort();
}
async function listDocs() {
const files = (await walkFiles(DOCS_ROOT)).filter((f) => f.endsWith(".md"));
return Promise.all(
files.map(async (rel) => {
const content = await readFile(path.join(DOCS_ROOT, rel), "utf8");
const fm = parseFrontmatter(content);
return { path: rel, title: fm.title || rel, description: fm.description };
})
);
}
async function readDoc(relPath) {
const resolved = path.resolve(DOCS_ROOT, relPath);
if (!resolved.startsWith(DOCS_ROOT + path.sep)) return null;
try {
return await readFile(resolved, "utf8");
} catch {
return null;
}
}
async function searchDocs(query) {
const regex = new RegExp(query, "i");
const matches = [];
for (const doc of await listDocs()) {
const content = await readDoc(doc.path);
if (!content) continue;
content.split("\n").forEach((text, idx) => {
if (regex.test(text)) {
matches.push({ path: doc.path, line: idx + 1, text: text.trim() });
}
});
}
return matches;
}
async function readNextjsExampleCode() {
const root = path.join(EXAMPLES_ROOT, "nextjs");
const files = await walkFiles(root);
const sections = await Promise.all(
files.map(async (rel) => {
const content = await readFile(path.join(root, rel), "utf8");
return `--- FILE: ${rel} ---
${content}`;
})
);
return sections.join("\n\n");
}
// src/tools.ts
var DEFAULT_API_BASE = "https://api.roottale.com";
var ReadDocInput = z.object({ path: z.string().min(1) }).strict();
var SearchDocsInput = z.object({ query: z.string().min(1) }).strict();
var ListPostsInput = z.object({
limit: z.number().int().min(1).max(100).optional(),
type: z.enum(["post", "page"]).optional(),
cursor: z.string().optional()
}).strict();
var GetPostInput = z.object({ slugOrId: z.string().min(1) }).strict();
var TOOL_DEFINITIONS = [
{
name: "listRootTaleDocs",
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.",
inputSchema: { type: "object", properties: {}, additionalProperties: false }
},
{
name: "readRootTaleDoc",
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.",
inputSchema: {
type: "object",
properties: {
path: { type: "string", description: "\uBB38\uC11C \uACBD\uB85C (\uC608: blog.md)" }
},
required: ["path"],
additionalProperties: false
}
},
{
name: "searchRootTaleDocs",
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.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "\uC815\uADDC\uC2DD \uD328\uD134 (\uC608: revalidate|webhook)" }
},
required: ["query"],
additionalProperties: false
}
},
{
name: "readRootTaleNextjsExampleCode",
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.",
inputSchema: { type: "object", properties: {}, additionalProperties: false }
},
{
name: "listPublishedPosts",
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.",
inputSchema: {
type: "object",
properties: {
limit: { type: "number", description: "1-100, \uAE30\uBCF8 20" },
type: { type: "string", enum: ["post", "page"] },
cursor: { type: "string", description: "\uC774\uC804 \uC751\uB2F5\uC758 next_cursor" }
},
additionalProperties: false
}
},
{
name: "getPublishedPost",
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.",
inputSchema: {
type: "object",
properties: {
slugOrId: { type: "string", description: "\uAE00 slug \uB610\uB294 UUID" }
},
required: ["slugOrId"],
additionalProperties: false
}
}
];
function requireApiKey() {
const apiKey = process.env.ROOTTALE_API_KEY;
if (!apiKey) {
throw new McpError(
ErrorCode.InvalidRequest,
"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)."
);
}
const baseUrl = (process.env.ROOTTALE_API_BASE ?? DEFAULT_API_BASE).replace(/\/+$/, "");
return { apiKey, baseUrl };
}
async function fetchPublicApi(pathname, params) {
const { apiKey, baseUrl } = requireApiKey();
const url = new URL(`${baseUrl}${pathname}`);
for (const [key, value] of Object.entries(params ?? {})) {
url.searchParams.set(key, value);
}
const response = await fetch(url, {
headers: { authorization: `Bearer ${apiKey}` }
});
const body = await response.text();
if (!response.ok) {
throw new Error(`RootTale API ${response.status}: ${body.slice(0, 500)}`);
}
return JSON.parse(body);
}
function textResponse(text) {
return { content: [{ type: "text", text }] };
}
function jsonResponse(value) {
return textResponse(JSON.stringify(value, null, 2));
}
function registerTools(server2) {
server2.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOL_DEFINITIONS.map((t) => ({ ...t }))
}));
server2.setRequestHandler(CallToolRequestSchema, async (request) => {
const args = request.params.arguments ?? {};
try {
switch (request.params.name) {
case "listRootTaleDocs":
return jsonResponse(await listDocs());
case "readRootTaleDoc": {
const input = ReadDocInput.parse(args);
const content = await readDoc(input.path);
if (content === null) {
throw new McpError(
ErrorCode.InvalidParams,
`\uBB38\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path} \u2014 listRootTaleDocs\uB85C \uACBD\uB85C\uB97C \uD655\uC778\uD558\uC138\uC694.`
);
}
return textResponse(content);
}
case "searchRootTaleDocs": {
const input = SearchDocsInput.parse(args);
return jsonResponse(await searchDocs(input.query));
}
case "readRootTaleNextjsExampleCode":
return textResponse(await readNextjsExampleCode());
case "listPublishedPosts": {
const input = ListPostsInput.parse(args);
const params = {};
if (input.limit) params.limit = String(input.limit);
if (input.type) params.type = input.type;
if (input.cursor) params.cursor = input.cursor;
return jsonResponse(await fetchPublicApi("/v1/cms/public/posts", params));
}
case "getPublishedPost": {
const input = GetPostInput.parse(args);
return jsonResponse(
await fetchPublicApi(
`/v1/cms/public/posts/${encodeURIComponent(input.slugOrId)}`
)
);
}
default:
throw new McpError(
ErrorCode.InvalidParams,
`Unknown RootTale tool: ${request.params.name}`
);
}
} catch (error) {
if (error instanceof McpError) throw error;
const message = error instanceof Error ? error.message : String(error);
return { isError: true, content: [{ type: "text", text: message }] };
}
});
}
// src/server.ts
var VERSION = true ? "0.25.0" : "dev";
var SERVER_INSTRUCTIONS = `
roottale-cms-mcp\uB294 RootTale CMS\uB97C \uC678\uBD80 \uC0AC\uC774\uD2B8(\uC8FC\uB85C Next.js)\uC5D0 \uC5F0\uB3D9\uD558\uAE30 \uC704\uD55C
\uD1B5\uD569 \uBB38\uC11C\xB7\uC608\uC2DC \uCF54\uB4DC\xB7\uACF5\uAC1C API \uC870\uD68C tool\uC744 \uC81C\uACF5\uD569\uB2C8\uB2E4.
\uC9C0\uCF1C\uC57C \uD560 \uADDC\uCE59:
- 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.
- \uC5F0\uB3D9 \uCF54\uB4DC\uB97C \uC791\uC131\uD558\uAE30 \uC804 \uBC18\uB4DC\uC2DC readRootTaleNextjsExampleCode\uB97C \uD638\uCD9C\uD574 \uC608\uC2DC \uCF54\uB4DC\uB97C
\uCC38\uACE0\uD55C \uD6C4 \uADF8 \uD328\uD134\uC744 \uB530\uB77C \uC791\uC131\uD558\uC138\uC694.
- API \uD0A4(rtlk_cust_*)\uB294 \uC11C\uBC84 \uC804\uC6A9\uC785\uB2C8\uB2E4. NEXT_PUBLIC_* \uB4F1 \uBE0C\uB77C\uC6B0\uC800\uB85C \uB178\uCD9C\uB418\uB294
\uD658\uACBD\uBCC0\uC218\uC5D0 \uC808\uB300 \uB123\uC9C0 \uB9C8\uC138\uC694.
- \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.
`.trim();
function createServer() {
const server2 = new Server(
{ name: "roottale-cms", version: VERSION },
{ capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
);
registerTools(server2);
return server2;
}
// src/index.ts
var server = createServer();
var transport = new StdioServerTransport();
await server.connect(transport);
//# sourceMappingURL=index.js.map