Skip to main content
Glama
BobGod

WeChat Publisher MCP

by BobGod

wechat_publish_article

Publish articles to WeChat Official Accounts using Markdown format, converting content to HTML with image handling and preview options.

Instructions

将文章发布到微信公众号,支持Markdown格式

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
titleYes文章标题
contentYesMarkdown格式的文章内容
authorYes作者名称
appIdYes微信公众号AppID
appSecretYes微信公众号AppSecret
coverImagePathNo封面图片路径
previewModeNo是否为预览模式
previewOpenIdNo预览用户OpenID

Implementation Reference

  • Core handler function WeChatPublisher.publish() that implements the complete tool logic: parameter validation, Markdown to WeChat HTML conversion, automatic cover image generation/upload, article publishing or previewing via WeChatAPI.
    static async publish(params) {
      const startTime = Date.now();
      
      try {
        // 详细记录调用参数(隐藏敏感信息)
        const logParams = {
          title: params.title,
          author: params.author,
          contentLength: params.content ? params.content.length : 0,
          contentPreview: params.content ? params.content.substring(0, 100) + '...' : '',
          appId: params.appId ? params.appId.substring(0, 8) + '***' : 'undefined',
          appSecret: params.appSecret ? '***已提供***' : 'undefined',
          coverImagePath: params.coverImagePath || 'undefined',
          previewMode: params.previewMode || false,
          previewOpenId: params.previewOpenId || 'undefined'
        };
        
        logger.info('=== MCP调用开始 ===');
        logger.info('调用参数详情', logParams);
        logger.info('开始发布流程', { title: params.title });
        
        // 1. 参数验证
        const validation = validatePublishParams(params);
        if (!validation.valid) {
          throw new Error(`参数验证失败: ${validation.errors.join(', ')}`);
        }
    
        const {
          title,
          content, 
          author,
          appId,
          appSecret,
          coverImagePath,
          previewMode = false,
          previewOpenId
        } = params;
    
        // 2. 初始化微信API
        logger.debug('初始化微信API');
        const wechatAPI = new WeChatAPI(appId, appSecret);
    
        // 3. 转换Markdown为微信HTML
        logger.debug('转换Markdown内容');
        const htmlContent = MarkdownConverter.convertToWeChatHTML(content);
        logger.debug('Markdown转换完成', { 
          originalLength: content.length, 
          htmlLength: htmlContent.length 
        });
    
        // 4. 处理封面图 - 如果没有提供封面图,则自动生成
        let thumbMediaId = null;
        let coverPath = coverImagePath;
        
        if (!coverPath) {
          // 自动生成封面图
          logger.info('未提供封面图,正在根据文章内容自动生成封面图...');
          coverPath = await WeChatPublisher.generateCoverImage(title, content);
        }
        
        if (coverPath) {
          try {
            logger.debug('开始上传封面图', { path: coverPath });
            thumbMediaId = await wechatAPI.uploadCoverImage(coverPath);
            logger.info('封面图上传成功', { mediaId: thumbMediaId });
            
            // 如果是自动生成的封面图,上传后删除临时文件
            if (!coverImagePath && coverPath) {
              try {
                const fs = await import('fs/promises');
                await fs.unlink(coverPath);
                logger.debug('临时封面图文件已清理', { coverPath });
              } catch (cleanupError) {
                logger.warn('清理临时封面图文件失败', { error: cleanupError.message });
              }
            }
          } catch (error) {
            logger.warn('封面图上传失败,将继续发布', { error: error.message });
            // 不抛出错误,继续发布流程
          }
        }
    
        // 5. 发布或预览文章
        let result;
        if (previewMode) {
          if (!previewOpenId) {
            throw new Error('预览模式需要提供previewOpenId参数');
          }
          
          logger.debug('开始预览文章', { previewOpenId });
          result = await wechatAPI.previewArticle({
            title,
            content: htmlContent,
            author,
            thumbMediaId,
            previewOpenId
          });
          
        } else {
          logger.debug('开始正式发布文章');
          result = await wechatAPI.publishArticle({
            title,
            content: htmlContent, 
            author,
            thumbMediaId
          });
        }
    
        const executionTime = Date.now() - startTime;
        logger.info(`文章${previewMode ? '预览' : '发布'}成功`, { 
          ...result, 
          executionTime: `${executionTime}ms` 
        });
    
        // 6. 构建成功响应
        const successMessage = this.buildSuccessMessage({
          title,
          author,
          result,
          previewMode,
          executionTime,
          thumbMediaId
        });
    
        return {
          content: [{
            type: "text",
            text: successMessage
          }]
        };
    
      } catch (error) {
        const executionTime = Date.now() - startTime;
        logger.error('发布流程失败', {
          error: error.message,
          executionTime: `${executionTime}ms`,
          stack: error.stack
        });
        
        return {
          content: [{
            type: "text",
            text: this.buildErrorMessage(error, params)
          }],
          isError: true
        };
      }
    }
  • src/server.js:27-71 (registration)
    MCP tool registration for 'wechat_publish_article' using server.registerTool(), including input schema and async handler that invokes WeChatPublisher.publish().
    server.registerTool(
      "wechat_publish_article",
      {
        description: "将文章发布到微信公众号,支持Markdown格式",
        inputSchema: {
          title: z.string().describe("文章标题"),
          content: z.string().describe("Markdown格式的文章内容"),
          author: z.string().describe("作者名称"),
          appId: z.string().describe("微信公众号AppID"),
          appSecret: z.string().describe("微信公众号AppSecret"),
          coverImagePath: z.string().optional().describe("封面图片路径"),
          previewMode: z.boolean().default(false).describe("是否为预览模式"),
          previewOpenId: z.string().optional().describe("预览用户OpenID")
        }
      },
      async (params) => {
        const { title, content, author, appId, appSecret, coverImagePath, previewMode, previewOpenId } = params;
        logger.info(`Publishing article: ${title}`);
        
        try {
          // 调用实际的发布逻辑
          const result = await WeChatPublisher.publish({
            title,
            content,
            author,
            appId,
            appSecret,
            coverImagePath,
            previewMode,
            previewOpenId
          });
          
          return result;
        } catch (error) {
          logger.error(`发布失败: ${error.message}`);
          return {
            content: [{
              type: "text",
              text: `❌ 发布失败: ${error.message}`
            }],
            isError: true
          };
        }
      }
    );
  • Zod-based input schema defining parameters for the tool: title, content, author, appId, appSecret, coverImagePath, previewMode, previewOpenId.
    description: "将文章发布到微信公众号,支持Markdown格式",
    inputSchema: {
      title: z.string().describe("文章标题"),
      content: z.string().describe("Markdown格式的文章内容"),
      author: z.string().describe("作者名称"),
      appId: z.string().describe("微信公众号AppID"),
      appSecret: z.string().describe("微信公众号AppSecret"),
      coverImagePath: z.string().optional().describe("封面图片路径"),
      previewMode: z.boolean().default(false).describe("是否为预览模式"),
      previewOpenId: z.string().optional().describe("预览用户OpenID")
    }
  • Comprehensive parameter validation logic validatePublishParams() enforcing types, lengths, formats (e.g., AppID starts with 'wx', length 18), and business rules (e.g., previewMode requires previewOpenId).
    function validatePublishParams(params) {
      const errors = [];
      
      // 必需参数检查
      if (!params.title || typeof params.title !== 'string' || params.title.trim() === '') {
        errors.push('title参数是必需的,且不能为空字符串');
      }
      
      if (!params.content || typeof params.content !== 'string' || params.content.trim() === '') {
        errors.push('content参数是必需的,且不能为空字符串');
      }
      
      if (!params.appId || typeof params.appId !== 'string' || params.appId.trim() === '') {
        errors.push('appId参数是必需的,且不能为空字符串');
      }
      
      if (!params.appSecret || typeof params.appSecret !== 'string' || params.appSecret.trim() === '') {
        errors.push('appSecret参数是必需的,且不能为空字符串');
      }
      
      // 可选参数类型检查
      if (params.author && typeof params.author !== 'string') {
        errors.push('author参数必须是字符串类型');
      }
      
      if (params.coverImagePath && typeof params.coverImagePath !== 'string') {
        errors.push('coverImagePath参数必须是字符串类型');
      }
      
      if (params.previewMode !== undefined && typeof params.previewMode !== 'boolean') {
        errors.push('previewMode参数必须是布尔值类型');
      }
      
      if (params.previewOpenId && typeof params.previewOpenId !== 'string') {
        errors.push('previewOpenId参数必须是字符串类型');
      }
      
      // 业务规则验证
      if (params.title && params.title.length > 64) {
        errors.push('标题长度不能超过64个字符');
      }
      
      if (params.author && params.author.length > 8) {
        errors.push('作者名称长度不能超过8个字符');
      }
      
      if (params.content && params.content.length > 200000) {
        errors.push('文章内容长度不能超过200,000个字符');
      }
      
      // AppID格式验证
      if (params.appId && !params.appId.startsWith('wx')) {
        errors.push('AppID格式错误,应该以"wx"开头');
      }
      
      if (params.appId && params.appId.length !== 18) {
        errors.push('AppID长度应该为18个字符');
      }
      
      // AppSecret格式验证
      if (params.appSecret && params.appSecret.length !== 32) {
        errors.push('AppSecret长度应该为32个字符');
      }
      
      // 预览模式验证
      if (params.previewMode === true && !params.previewOpenId) {
        errors.push('预览模式下必须提供previewOpenId参数');
      }
      
      // OpenID格式验证(如果提供了)
      if (params.previewOpenId && !isValidOpenId(params.previewOpenId)) {
        errors.push('previewOpenId格式不正确');
      }
      
      return {
        valid: errors.length === 0,
        errors
      };
    }
  • Helper function generateCoverImage() that automatically creates attractive cover images from title/content if none provided, supporting PNG via canvas or SVG fallback, with themed designs.
    static async generateCoverImage(title, content) {
      try {
        const path = await import('path');
        const fs = await import('fs/promises');
        
        // 提取文章关键信息
        const cleanTitle = title.replace(/[#*`]/g, '').trim();
        const shortTitle = cleanTitle.length > 20 ? cleanTitle.substring(0, 20) + '...' : cleanTitle;
        
        // 从内容中提取关键词或副标题
        const contentLines = content.split('\n').filter(line => line.trim());
        let subtitle = '';
        for (const line of contentLines) {
          const cleanLine = line.replace(/[#*`<>]/g, '').trim();
          if (cleanLine.length > 10 && cleanLine.length < 50 && !cleanLine.includes('http')) {
            subtitle = cleanLine;
            break;
          }
        }
        
        if (!subtitle) {
          subtitle = '精彩内容,值得一读';
        }
        
        // 选择背景颜色(根据标题内容智能选择)
        const colors = [
          { bg: '#3498db', text: '#ffffff', accent: '#2980b9' }, // 蓝色主题
          { bg: '#e74c3c', text: '#ffffff', accent: '#c0392b' }, // 红色主题
          { bg: '#2ecc71', text: '#ffffff', accent: '#27ae60' }, // 绿色主题
          { bg: '#9b59b6', text: '#ffffff', accent: '#8e44ad' }, // 紫色主题
          { bg: '#f39c12', text: '#ffffff', accent: '#e67e22' }, // 橙色主题
          { bg: '#1abc9c', text: '#ffffff', accent: '#16a085' }, // 青色主题
        ];
        
        // 根据标题内容选择颜色
        let colorIndex = 0;
        if (title.includes('AI') || title.includes('技术')) colorIndex = 0;
        else if (title.includes('重要') || title.includes('紧急')) colorIndex = 1;
        else if (title.includes('成功') || title.includes('增长')) colorIndex = 2;
        else if (title.includes('创新') || title.includes('未来')) colorIndex = 3;
        else if (title.includes('警告') || title.includes('注意')) colorIndex = 4;
        else colorIndex = Math.floor(Math.random() * colors.length);
        
        const theme = colors[colorIndex];
        
        // 创建Canvas并生成PNG图片
        const timestamp = Date.now();
        const coverPath = path.default.join(process.cwd(), `auto-cover-${timestamp}.png`);
        
        // 使用Canvas API生成PNG图片
        await WeChatPublisher.createPngCover({
          title: shortTitle,
          subtitle: subtitle.substring(0, 30),
          theme,
          outputPath: coverPath
        });
        
        // 检查文件大小
        const stats = await fs.stat(coverPath);
        const fileSizeInMB = stats.size / (1024 * 1024);
        
        if (fileSizeInMB > 1) {
          logger.warn('生成的封面图超过1MB,尝试压缩', { size: `${fileSizeInMB.toFixed(2)}MB` });
          // 如果文件过大,可以在这里添加压缩逻辑
        }
        
        logger.info('自动生成封面图成功', { 
          coverPath, 
          title: shortTitle, 
          size: `${fileSizeInMB.toFixed(2)}MB` 
        });
        return coverPath;
        
      } catch (error) {
        logger.error('自动生成封面图失败', { error: error.message });
        throw new Error(`自动生成封面图失败: ${error.message}`);
      }
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. While '发布' (publish) implies a write/mutation operation, the description doesn't disclose important behavioral aspects: whether this is a live publish or draft creation, what authentication requirements exist beyond the appId/appSecret parameters, whether there are rate limits, what happens on success/failure, or whether the operation is reversible. For a mutation tool with 8 parameters and no annotation coverage, this represents significant gaps.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise - a single Chinese sentence that communicates the core functionality efficiently. It's front-loaded with the main purpose and includes the key feature (Markdown support) without unnecessary elaboration. Every word earns its place, making this an excellent example of conciseness.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with 8 parameters, no annotations, and no output schema, the description is insufficiently complete. It doesn't explain what happens after publishing (success response, error conditions), doesn't mention the sibling tool relationship, and provides minimal behavioral context. While the schema documents parameters well, the description fails to compensate for the lack of annotations and output schema, leaving significant gaps in understanding how this tool behaves in practice.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema description coverage is 100%, meaning all parameters are documented in the schema itself. The description adds only one piece of parameter-related information: that content supports Markdown format (implied in the schema's 'Markdown格式的文章内容' but reinforced in the description). This provides minimal additional value beyond what the schema already offers, meeting the baseline expectation when schema coverage is high.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('发布' - publish) and target resource ('文章到微信公众号' - article to WeChat Official Account), making the purpose immediately understandable. It also specifies support for Markdown format, which adds useful detail. However, it doesn't explicitly differentiate from the sibling tool 'wechat_query_status' (which presumably queries status rather than publishes content).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. While it mentions Markdown support, it doesn't indicate when to use this tool over other publishing methods or when the sibling tool 'wechat_query_status' would be more appropriate. There are no prerequisites, exclusions, or contextual recommendations provided.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

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/BobGod/wechat-publisher-mcp'

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