UNPKG

@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
#!/usr/bin/env node // 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