#
tokens: 4972/50000 7/7 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── config.json
├── Dockerfile
├── go.mod
├── go.sum
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── smithery.yaml
├── wecom_test.go
└── wecom.go
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
mcp-webcombot-server
dist/
docs/
```

--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------

```json
{
    "mcpServers": {
      "mcp-difyworkflow-server": {
        "command": "mcp-wecombot-server", // mcp-wecombot-server bainary Path or mcp-wecombot-server 
        "env": {
          "WECOM_BOT_WEBHOOK_KEY":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx"
         }
      }
    }
}
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/deployments

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    required:
      - wecomBotWebhookKey
    properties:
      wecomBotWebhookKey:
        type: string
        description: The webhook key for the WeCom Bot server.
  commandFunction:
    # A function that produces the CLI command to start the MCP on stdio.
    |-
    config => ({ command: 'mcp-wecombot-server', env: { WECOM_BOT_WEBHOOK_KEY: config.wecomBotWebhookKey } })
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Build stage
FROM golang:1.21-alpine AS builder

# Set the working directory inside the container
WORKDIR /app

# Copy go.mod and go.sum files first for dependency resolution
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download

# Copy the entire source code into the container
COPY . .

# Build the binary for Linux
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/mcp-wecombot-server_linux_amd64 .

# Final stage
FROM alpine:latest

# Set working directory
WORKDIR /app

# Copy the compiled binary from the builder stage
COPY --from=builder /app/dist/mcp-wecombot-server_linux_amd64 /usr/local/bin/mcp-wecombot-server

# Expose any ports the application requires, if necessary
# EXPOSE <port>

# Set the entrypoint to the compiled binary
ENTRYPOINT ["mcp-wecombot-server"]
```

--------------------------------------------------------------------------------
/wecom_test.go:
--------------------------------------------------------------------------------

```go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"
)

// MockWeComServer is a mock server for testing WeComBot.
type MockWeComServer struct {
	*httptest.Server
}

// test upload file need to setting your's test key
var testkey = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx"

// NewMockWeComServer creates a new mock WeCom server.
func NewMockWeComServer() *MockWeComServer {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "POST" {
			body, _ := io.ReadAll(r.Body)
			fmt.Println(string(body))
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			json.NewEncoder(w).Encode(map[string]interface{}{
				"errcode": 0,
				"errmsg":  "ok",
			})
		}
	})

	server := httptest.NewServer(handler)
	return &MockWeComServer{Server: server}
}

// TestSendText tests the SendText method.
func TestSendText(t *testing.T) {
	mockServer := NewMockWeComServer()
	defer mockServer.Close()

	bot := NewWeComBot(mockServer.URL, "")
	err := bot.SendText("Hello, WeCom!", []string{}, []string{})
	if err != nil {
		t.Errorf("SendText failed: %v", err)
	}
}

// TestSendMarkdown tests the SendMarkdown method.
func TestSendMarkdown(t *testing.T) {
	mockServer := NewMockWeComServer()
	defer mockServer.Close()

	bot := NewWeComBot(mockServer.URL, "")
	err := bot.SendMarkdown("## Hello, WeCom!\nThis is a markdown message.")
	if err != nil {
		t.Errorf("SendMarkdown failed: %v", err)
	}
}

// TestSendImage tests the SendImage method.
func TestSendImage(t *testing.T) {
	mockServer := NewMockWeComServer()
	defer mockServer.Close()

	bot := NewWeComBot(mockServer.URL, "")
	err := bot.SendImage("SGVsbG8sIFdlQ29tIQ==", "d41d8cd98f00b204e9800998ecf8427e")
	if err != nil {
		t.Errorf("SendImage failed: %v", err)
	}
}

// TestSendNews tests the SendNews method.
func TestSendNews(t *testing.T) {
	mockServer := NewMockWeComServer()
	defer mockServer.Close()

	bot := NewWeComBot(mockServer.URL, "")
	articles := []NewsArticle{
		{
			Title:       "News Title",
			Description: "News Description",
			URL:         "https://example.com",
			PicURL:      "https://example.com/image.jpg",
		},
	}
	err := bot.SendNews(articles)
	if err != nil {
		t.Errorf("SendNews failed: %v", err)
	}
}

// TestSendTemplateCard tests the SendTemplateCard method.
func TestSendTemplateCard(t *testing.T) {
	mockServer := NewMockWeComServer()
	defer mockServer.Close()

	bot := NewWeComBot(mockServer.URL, "")
	err := bot.SendTemplateCard("text_notice", "Main Title", "Main Description", 1, "https://example.com", "", "")
	if err != nil {
		t.Errorf("SendTemplateCard failed: %v", err)
	}
}

// TestUploadFile tests the UploadFile method.
func TestUploadFile(t *testing.T) {
	mockServer := NewMockWeComServer()
	defer mockServer.Close()

	mockServer.URL = WECOM_BOT_UPLOAD_URL
	bot := NewWeComBot(mockServer.URL, testkey)

	// Create a temporary file for testing
	tempFile, err := os.CreateTemp("", "testfile")
	if err != nil {
		t.Fatalf("Failed to create temporary file: %v", err)
	}
	defer os.Remove(tempFile.Name())

	// Write some content to the temporary file
	_, err = tempFile.WriteString("Hello, WeCom!")
	if err != nil {
		t.Fatalf("Failed to write to temporary file: %v", err)
	}
	tempFile.Close()

	mediaID, err := bot.UploadFile(tempFile.Name())
	if err != nil {
		t.Errorf("UploadFile failed: %v", err)
	} else {
		t.Logf("UploadFile succeeded, media ID: %s", mediaID)
	}
}

```

--------------------------------------------------------------------------------
/wecom.go:
--------------------------------------------------------------------------------

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
)

const (
	WECOM_BOT_BASE_URL   = "https://qyapi.weixin.qq.com/cgi-bin/webhook"
	WECOM_BOT_SEND_URL   = WECOM_BOT_BASE_URL + "/send?key="
	WECOM_BOT_UPLOAD_URL = WECOM_BOT_BASE_URL + "/upload_media?key="
)

type WeComBot struct {
	WebhookURL string
	WebhookKey string
}

func NewWeComBot(webhookURL, webhookKey string) *WeComBot {
	return &WeComBot{
		WebhookURL: webhookURL,
		WebhookKey: webhookKey,
	}
}

// SendText sends a text message to the WeCom group.
func (bot *WeComBot) SendText(content string, mentionedList []string, mentionedMobileList []string) error {
	payload := map[string]interface{}{
		"msgtype": "text",
		"text": map[string]interface{}{
			"content":               content,
			"mentioned_list":        mentionedList,
			"mentioned_mobile_list": mentionedMobileList,
		},
	}

	return bot.sendRequest(payload)
}

// SendMarkdown sends a markdown message to the WeCom group.
func (bot *WeComBot) SendMarkdown(content string) error {
	payload := map[string]interface{}{
		"msgtype": "markdown",
		"markdown": map[string]interface{}{
			"content": content,
		},
	}

	return bot.sendRequest(payload)
}

// SendImage sends an image message to the WeCom group.
func (bot *WeComBot) SendImage(base64Data string, md5 string) error {
	payload := map[string]interface{}{
		"msgtype": "image",
		"image": map[string]interface{}{
			"base64": base64Data,
			"md5":    md5,
		},
	}

	return bot.sendRequest(payload)
}

// SendNews sends a news message to the WeCom group.
func (bot *WeComBot) SendNews(articles []NewsArticle) error {
	payload := map[string]interface{}{
		"msgtype": "news",
		"news": map[string]interface{}{
			"articles": articles,
		},
	}

	return bot.sendRequest(payload)
}

// NewsArticle represents a news article in a news message.
type NewsArticle struct {
	Title       string `json:"title"`
	Description string `json:"description"`
	URL         string `json:"url"`
	PicURL      string `json:"picurl"`
}

// SendTemplateCard sends a template card message to the WeCom group.
func (bot *WeComBot) SendTemplateCard(cardType string, mainTitle string, mainDesc string, cardActionType int, cardActionURL string, cardActionAppID string, cardActionPagePath string) error {
	payload := map[string]interface{}{
		"msgtype": "template_card",
		"template_card": map[string]interface{}{
			"card_type": cardType,
			"main_title": map[string]interface{}{
				"title": mainTitle,
				"desc":  mainDesc,
			},
			"card_action": map[string]interface{}{
				"type":     cardActionType,
				"url":      cardActionURL,
				"appid":    cardActionAppID,
				"pagepath": cardActionPagePath,
			},
		},
	}

	return bot.sendRequest(payload)
}

// UploadFile uploads a file to WeCom and returns the media ID.
func (bot *WeComBot) UploadFile(filePath string) (string, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return "", err
	}
	defer file.Close()

	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	part, err := writer.CreateFormFile("media", filePath)
	if err != nil {
		return "", err
	}

	_, err = io.Copy(part, file)
	if err != nil {
		return "", err
	}

	err = writer.Close()
	if err != nil {
		return "", err
	}

	resp, err := http.Post(fmt.Sprintf("%s%s&type=file", bot.WebhookURL, bot.WebhookKey), writer.FormDataContentType(), body)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	var result map[string]interface{}
	err = json.NewDecoder(resp.Body).Decode(&result)
	if err != nil {
		return "", err
	}

	if result["errcode"].(float64) != 0 {
		return "", fmt.Errorf("WeCom API error: %s", result["errmsg"].(string))
	}

	return result["media_id"].(string), nil
}

// sendRequest sends a request to the WeCom API with the given payload.
func (bot *WeComBot) sendRequest(payload map[string]interface{}) error {
	jsonPayload, err := json.Marshal(payload)
	if err != nil {
		return err
	}

	url := bot.WebhookURL + bot.WebhookKey
	resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonPayload))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	var result map[string]interface{}
	err = json.NewDecoder(resp.Body).Decode(&result)
	if err != nil {
		return err
	}

	if result["errcode"].(float64) != 0 {
		return fmt.Errorf("WeCom API error: %s", result["errmsg"].(string))
	}

	return nil
}

```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

func main() {
	webhookKey := os.Getenv("WECOM_BOT_WEBHOOK_KEY")
	if webhookKey == "" {
		log.Println("WECOM_BOT_WEBHOOK_KEY environment variable is required")
		return
	}

	bot := NewWeComBot(WECOM_BOT_SEND_URL, webhookKey)

	s := server.NewMCPServer(
		"mcp-wecombot-server",
		"1.0.0",
		server.WithResourceCapabilities(true, true),
		server.WithLogging(),
	)

	sendTextTool := mcp.NewTool("send_text",
		mcp.WithDescription("Send a text message to WeCom group"),
		mcp.WithString("content",
			mcp.Required(),
			mcp.Description("Text content to send"),
		),
		mcp.WithString("mentioned_list",
			mcp.Description("List of user IDs to mention,Multiple people use commas to separate, such as @xiaoyang, @wike."),
		),
		mcp.WithString("mentioned_mobile_list",
			mcp.Description("List of mobile numbers to mention,Multiple people use commas to separate, such as @xiaoyang, @wike."),
		),
	)
	s.AddTool(sendTextTool, sendTextHandler(bot))

	sendMarkdownTool := mcp.NewTool("send_markdown",
		mcp.WithDescription("Send a markdown message to WeCom group"),
		mcp.WithString("content",
			mcp.Required(),
			mcp.Description("Markdown content to send"),
		),
	)
	s.AddTool(sendMarkdownTool, sendMarkdownHandler(bot))

	sendImageTool := mcp.NewTool("send_image",
		mcp.WithDescription("Send an image message to WeCom group"),
		mcp.WithString("base64_data",
			mcp.Required(),
			mcp.Description("Base64 encoded image data"),
		),
		mcp.WithString("md5",
			mcp.Required(),
			mcp.Description("MD5 hash of the image"),
		),
	)
	s.AddTool(sendImageTool, sendImageHandler(bot))

	sendNewsTool := mcp.NewTool("send_news",
		mcp.WithDescription("Send a news message to WeCom group"),
		mcp.WithString("title", mcp.Required(), mcp.Description("Title of the news article")),
		mcp.WithString("description", mcp.Description("Description of the news article")),
		mcp.WithString("url", mcp.Required(), mcp.Description("URL of the news article")),
		mcp.WithString("picurl", mcp.Description("Picture URL of the news article")),
	)
	s.AddTool(sendNewsTool, sendNewsHandler(bot))

	sendTemplateCardTool := mcp.NewTool("send_template_card",
		mcp.WithDescription("Send a template card message to WeCom group"),
		mcp.WithString("card_type",
			mcp.Required(),
			mcp.Description("Type of the template card"),
		),
		mcp.WithString("main_title",
			mcp.Required(),
			mcp.Description("Main title of the template card"),
		),
		mcp.WithString("main_desc",
			mcp.Description("Main description of the template card"),
		),
		mcp.WithNumber("card_action_type",
			mcp.Required(),
			mcp.Description("Type of the card action"),
		),
		mcp.WithString("card_action_url",
			mcp.Description("URL for the card action"),
		),
		mcp.WithString("card_action_appid",
			mcp.Description("App ID for the card action"),
		),
		mcp.WithString("card_action_pagepath",
			mcp.Description("Page path for the card action"),
		),
	)
	s.AddTool(sendTemplateCardTool, sendTemplateCardHandler(bot))

	uploadFileTool := mcp.NewTool("upload_file",
		mcp.WithDescription("Upload a file to WeCom"),
		mcp.WithString("file_path",
			mcp.Required(),
			mcp.Description("Path to the file to upload"),
		),
	)
	s.AddTool(uploadFileTool, uploadFileHandler(bot))

	if err := server.ServeStdio(s); err != nil {
		log.Printf("Server error: %v\n", err)
	}
}
func sendTextHandler(bot *WeComBot) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		var mentionedListStr string
		var mentionedMobileListStr string
		var mentionedList []string
		var mentionedMobileList []string

		content := request.Params.Arguments["content"].(string)

		if request.Params.Arguments["mentioned_list"] == nil && request.Params.Arguments["mentioned_mobile_list"] == nil {
			mentionedListStr = ""
			mentionedMobileListStr = ""
		} else {
			mentionedListStr = request.Params.Arguments["mentioned_list"].(string)
			mentionedMobileListStr = request.Params.Arguments["mentioned_mobile_list"].(string)
		}

		if mentionedListStr != "" && mentionedMobileListStr != "" {
			mentionedList = strings.Split(mentionedListStr, ",")
			mentionedMobileList = strings.Split(mentionedMobileListStr, ",")
		} else {
			mentionedList = []string{}
			mentionedMobileList = []string{}
		}

		err := bot.SendText(content, mentionedList, mentionedMobileList)
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("Failed to send text message: %v", err)), nil
		}

		return mcp.NewToolResultText("Text message sent successfully"), nil
	}
}

func sendMarkdownHandler(bot *WeComBot) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		content := request.Params.Arguments["content"].(string)

		bot.WebhookURL = WECOM_BOT_SEND_URL
		err := bot.SendMarkdown(content)
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("Failed to send markdown message: %v", err)), nil
		}

		return mcp.NewToolResultText("Markdown message sent successfully"), nil
	}
}

func sendImageHandler(bot *WeComBot) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		base64Data := request.Params.Arguments["base64_data"].(string)
		md5 := request.Params.Arguments["md5"].(string)

		bot.WebhookURL = WECOM_BOT_SEND_URL
		err := bot.SendImage(base64Data, md5)
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("Failed to send image message: %v", err)), nil
		}

		return mcp.NewToolResultText("Image message sent successfully"), nil
	}
}

func sendNewsHandler(bot *WeComBot) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		// Extract the parameters from the request
		title := request.Params.Arguments["title"].(string)
		description := request.Params.Arguments["description"].(string)
		url := request.Params.Arguments["url"].(string)
		picurl := request.Params.Arguments["picurl"].(string)

		// Create a single NewsArticle
		newsArticle := NewsArticle{
			Title:       title,
			Description: description,
			URL:         url,
			PicURL:      picurl,
		}

		// Send the news article
		bot.WebhookURL = WECOM_BOT_SEND_URL
		err := bot.SendNews([]NewsArticle{newsArticle})
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("Failed to send news message: %v", err)), nil
		}

		// Return success result
		return mcp.NewToolResultText("News message sent successfully"), nil
	}
}

func sendTemplateCardHandler(bot *WeComBot) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		cardType := request.Params.Arguments["card_type"].(string)
		mainTitle := request.Params.Arguments["main_title"].(string)
		mainDesc := request.Params.Arguments["main_desc"].(string)
		cardActionType := request.Params.Arguments["card_action_type"].(int)
		cardActionURL := request.Params.Arguments["card_action_url"].(string)
		cardActionAppID := request.Params.Arguments["card_action_appid"].(string)
		cardActionPagePath := request.Params.Arguments["card_action_pagepath"].(string)

		bot.WebhookURL = WECOM_BOT_SEND_URL
		err := bot.SendTemplateCard(cardType, mainTitle, mainDesc, cardActionType, cardActionURL, cardActionAppID, cardActionPagePath)
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("Failed to send template card message: %v", err)), nil
		}

		return mcp.NewToolResultText("Template card message sent successfully"), nil
	}
}

func uploadFileHandler(bot *WeComBot) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		filePath := request.Params.Arguments["file_path"].(string)

		bot.WebhookURL = WECOM_BOT_UPLOAD_URL
		mediaID, err := bot.UploadFile(filePath)
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("Failed to upload file: %v", err)), nil
		}

		return mcp.NewToolResultText(fmt.Sprintf("File uploaded successfully, media ID: %s", mediaID)), nil
	}
}

```