Skip to main content
Glama

Emlog MCP Server

index.ts19.2 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { EmlogClient } from "./emlog-client.js"; // Get API configuration from environment const EMLOG_API_URL = process.env.EMLOG_API_URL; const EMLOG_API_KEY = process.env.EMLOG_API_KEY; if (!EMLOG_API_URL || !EMLOG_API_KEY) { console.error("Missing required environment variables: EMLOG_API_URL and EMLOG_API_KEY"); process.exit(1); } // Initialize Emlog client const emlogClient = new EmlogClient(EMLOG_API_URL, EMLOG_API_KEY); // Create MCP server const server = new McpServer({ name: "emlog-mcp", version: "1.0.2", }); // Register resources server.registerResource( "articles", "emlog://articles", { description: "All articles from the Emlog blog" }, async (uri: URL) => { try { const result = await emlogClient.getArticleList({}); return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify(result, null, 2) }] }; } catch (error) { throw new Error(`Failed to fetch articles: ${error}`); } } ); server.registerResource( "categories", "emlog://categories", { description: "All categories from the Emlog blog" }, async (uri: URL) => { try { const result = await emlogClient.getSortList(); return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify(result, null, 2) }] }; } catch (error) { throw new Error(`Failed to fetch categories: ${error}`); } } ); server.registerResource( "comments", "emlog://comments/{id}", { description: "Comments for a specific article. Use emlog://comments/{article_id} to get comments for a specific article." }, async (uri: URL) => { try { // 从URI路径中解析文章ID,例如: emlog://comments/123 const pathParts = uri.pathname.split('/').filter(part => part); const articleId = pathParts.length > 0 ? parseInt(pathParts[0]) : 1; if (isNaN(articleId)) { throw new Error('Invalid article ID in URI. Use format: emlog://comments/{article_id}'); } const result = await emlogClient.getCommentListSimple(articleId); return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify({ articleId: articleId, comments: result.comments, totalComments: result.comments.length }, null, 2) }] }; } catch (error) { throw new Error(`Failed to fetch comments: ${error}`); } } ); server.registerResource( "notes", "emlog://notes", { description: "All notes from the Emlog blog" }, async (uri: URL) => { try { const result = await emlogClient.getNoteList({ page: 1, count: 20 }); return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify(result, null, 2) }] }; } catch (error) { throw new Error(`Failed to fetch notes: ${error instanceof Error ? error.message : String(error)}`); } } ); server.registerResource( "users", "emlog://users", { description: "All users from the Emlog blog" }, async (uri: URL) => { try { const result = await emlogClient.getCurrentUser(); return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify(result, null, 2) }] }; } catch (error) { throw new Error(`Failed to fetch user info: ${error instanceof Error ? error.message : String(error)}`); } } ); server.registerResource( "drafts", "emlog://drafts", { description: "All drafts from the Emlog blog" }, async (uri: URL) => { try { const result = await emlogClient.getDraftList({ count: 20 }); return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify(result, null, 2) }] }; } catch (error) { throw new Error(`Failed to fetch drafts: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register tools server.registerTool( "create_article", { title: "Create Article", description: "Create a new blog article", inputSchema: { title: z.string().describe("The title of the article"), content: z.string().describe("The content of the article"), excerpt: z.string().optional().describe("The excerpt/summary of the article"), cover: z.string().optional().describe("The cover image URL"), sort_id: z.number().optional().describe("The category ID for the article"), tags: z.string().optional().describe("Comma-separated tags for the article"), draft: z.enum(["y", "n"]).optional().describe("Whether to save as draft (y) or publish (n)"), top: z.enum(["y", "n"]).optional().describe("Whether to pin to homepage"), allow_remark: z.enum(["y", "n"]).optional().describe("Whether to allow comments") } }, async ({ title, content, excerpt, cover, sort_id, tags, draft, top, allow_remark }) => { try { const result = await emlogClient.createArticle({ title, content, excerpt, cover, sort_id, tags, draft, top, allow_remark }); return { content: [{ type: "text", text: `Successfully created article: ${title} (ID: ${result.article_id || 'unknown'})` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.registerTool( "update_article", { title: "Update Article", description: "Update an existing blog article. If the article is currently a draft and no draft parameter is specified, it will remain as a draft.", inputSchema: { id: z.number().describe("The ID of the article to update"), title: z.string().describe("The new title of the article"), content: z.string().optional().describe("The new content of the article"), excerpt: z.string().optional().describe("The new excerpt/summary"), cover: z.string().optional().describe("The new cover image URL"), sort_id: z.number().optional().describe("The new category ID"), tags: z.string().optional().describe("New comma-separated tags"), draft: z.enum(["y", "n"]).optional().describe("Whether to save as draft (y) or publish (n). If not specified and the article is currently a draft, it will remain as a draft.") } }, async ({ id, title, content, excerpt, cover, sort_id, tags, draft }) => { try { // 如果用户没有明确指定draft参数,需要检查当前文章状态 let finalDraft = draft; if (draft === undefined) { try { // 首先尝试从草稿中获取 const draftResult = await emlogClient.getDraftDetail(id); if (draftResult && draftResult.draft) { // 如果在草稿中找到,保持草稿状态 finalDraft = "y"; } } catch (draftError) { // 如果不是草稿,尝试从已发布文章中获取 try { await emlogClient.getArticleDetail(id); // 如果是已发布文章且用户没有指定draft,保持发布状态 finalDraft = "n"; } catch (articleError) { // 如果都找不到,可能是文章不存在 console.error(`Article ${id} not found in drafts or published articles`); } } } await emlogClient.updateArticle(id, { title, content, excerpt, cover, sort_id, tags, draft: finalDraft }); const statusText = finalDraft === "y" ? "(saved as draft)" : finalDraft === "n" ? "(published)" : ""; return { content: [{ type: "text", text: `Successfully updated article: ${title} (ID: ${id}) ${statusText}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.registerTool( "get_article", { title: "Get Article", description: "Get a specific article by ID", inputSchema: { id: z.number().describe("The ID of the article to retrieve"), password: z.string().optional().describe("Password for protected articles") } }, async ({ id, password }) => { try { const article = await emlogClient.getArticleDetail(id, password); return { content: [{ type: "text", text: `Article: ${article.title}\n\nContent: ${article.content}\n\nExcerpt: ${article.excerpt || 'N/A'}\nCategory: ${article.sort_id}\nTags: ${article.tags || 'N/A'}\nViews: ${article.views}\nComments: ${article.comnum}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.registerTool( "search_articles", { title: "Search Articles", description: "Search articles by keyword, tag, or category", inputSchema: { keyword: z.string().optional().describe("Search keyword for article titles"), tag: z.string().optional().describe("Filter by tag"), sort_id: z.number().optional().describe("Filter by category ID"), page: z.number().optional().describe("Page number (default: 1)"), count: z.number().optional().describe("Number of articles per page"), order: z.enum(["views", "comnum"]).optional().describe("Sort order: views (by view count) or comnum (by comment count)") } }, async ({ keyword, tag, sort_id, page, count, order }) => { try { const result = await emlogClient.getArticleList({ keyword, tag, sort_id, page, count, order }); const articles = result.articles; const articleList = articles.map((article: any) => `- ${article.title} (ID: ${article.id}) - Views: ${article.views}, Comments: ${article.comnum}` ).join('\n'); return { content: [{ type: "text", text: `Found ${articles.length} articles (Page ${result.page}/${result.total_pages}):\n\n${articleList || 'No articles found'}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.registerTool( "like_article", { title: "Like Article", description: "Like an article", inputSchema: { gid: z.number().describe("The ID of the article to like"), name: z.string().optional().describe("Name of the person liking"), avatar: z.string().optional().describe("Avatar URL of the person liking") } }, async ({ gid, name, avatar }) => { try { await emlogClient.likeArticle(gid, name, avatar); return { content: [{ type: "text", text: `Successfully liked article with ID: ${gid}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.registerTool( "add_comment", { title: "Add Comment", description: "Add a comment to an article", inputSchema: { gid: z.number().describe("The ID of the article to comment on"), comname: z.string().describe("Name of the commenter"), comment: z.string().describe("The comment content"), commail: z.string().optional().describe("Email of the commenter"), comurl: z.string().optional().describe("Website URL of the commenter"), pid: z.number().optional().describe("Parent comment ID for replies") } }, async ({ gid, comname, comment, commail, comurl, pid }) => { try { await emlogClient.addComment({ gid, comname, comment, commail, comurl, pid }); return { content: [{ type: "text", text: `Successfully added comment to article ${gid} by ${comname}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.registerTool( "get_comments", { title: "Get Comments", description: "Get comments for an article (with pagination support)", inputSchema: { id: z.number().describe("The ID of the article"), page: z.number().optional().describe("Page number for paginated comments (requires backend pagination enabled)") } }, async ({ id, page }) => { try { // 如果提供了page参数,使用支持分页的comment_list接口 // 否则使用简化的comment_list_simple接口 if (page !== undefined) { const result = await emlogClient.getCommentList(id, page); const comments = Object.values(result.comments); const commentList = comments.map((comment: any) => `- ${comment.poster}: ${comment.comment} (${comment.date})` ).join('\n'); return { content: [{ type: "text", text: `Comments for article ${id} (page ${page}):\n\n${commentList || 'No comments found'}\n\nPage URL: ${result.commentPageUrl || 'N/A'}` }] }; } else { const result = await emlogClient.getCommentListSimple(id); const comments = result.comments; const commentList = comments.map((comment: any) => `- ${comment.poster}: ${comment.comment} (${comment.date})` ).join('\n'); return { content: [{ type: "text", text: `Comments for article ${id}:\n\n${commentList || 'No comments found'}` }] }; } } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.registerTool( "create_note", { title: "Create Note", description: "Create a new micro-note", inputSchema: { t: z.string().describe("The content of the micro-note"), private: z.enum(["y", "n"]).optional().describe("Whether the note is private (y) or public (n)") } }, async ({ t, private: isPrivate }) => { try { await emlogClient.publishNote(t, isPrivate); return { content: [{ type: "text", text: `Successfully created micro-note: ${t.substring(0, 50)}${t.length > 50 ? '...' : ''}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.registerTool( "upload_file", { title: "Upload File", description: "Upload a file (image, document, etc.)", inputSchema: { file_path: z.string().describe("Local path to the file to upload"), sid: z.number().optional().describe("Resource category ID") } }, async ({ file_path, sid }) => { try { const result = await emlogClient.uploadFile(file_path, sid); return { content: [{ type: "text", text: `Successfully uploaded file: ${result.url}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.registerTool( "get_user_info", { title: "Get User Info", description: "Get current user information", inputSchema: {} }, async () => { try { const result = await emlogClient.getCurrentUser(); const user = result.userinfo; return { content: [{ type: "text", text: `User: ${user.nickname}\nEmail: ${user.email}\nUID: ${user.uid}\nDescription: ${user.description || 'N/A'}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.registerTool( "get_draft_list", { title: "Get Draft List", description: "Get list of draft articles", inputSchema: { count: z.number().optional().describe("Number of drafts to retrieve") } }, async ({ count }) => { try { const result = await emlogClient.getDraftList({ count }); const drafts = result.drafts; const draftList = drafts.map((draft: any) => `- ID: ${draft.id}, Title: ${draft.title || 'Untitled'}, Date: ${draft.date}` ).join('\n'); return { content: [{ type: "text", text: `Draft articles (${drafts.length} found):\n\n${draftList || 'No drafts found'}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.registerTool( "get_draft_detail", { title: "Get Draft Detail", description: "Get details of a specific draft", inputSchema: { id: z.number().describe("The ID of the draft to retrieve") } }, async ({ id }) => { try { const result = await emlogClient.getDraftDetail(id); const draft = result.draft; return { content: [{ type: "text", text: `Draft Details:\n\nTitle: ${draft.title}\nID: ${draft.id}\nDate: ${draft.date}\nAuthor: ${draft.author_name}\nCategory: ${draft.sort_name || 'Uncategorized'}\nExcerpt: ${draft.excerpt || 'No excerpt'}\n\n--- Content ---\n${draft.content}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Emlog MCP server running on stdio'); } main().catch(console.error);

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eraincc/emlog-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server