feat: initial release with 48 Mattermost MCP tools

- 27 base tools: channels, messaging, users, reactions, files, DMs
- Team management: invite/remove users, get stats, list members
- Slash commands: execute /remind, /poll, etc.
- Webhook management: incoming and outgoing webhooks
- System tools: server config, logs, bulk status updates
- Channel admin: create, invite, leave, delete channels
- Read-only mode for safe exploration
- Dual token support (bot + PAT) for enhanced security
- Apache 2.0 licensed
This commit is contained in:
2026-04-15 21:14:47 -07:00
commit 35a0b2b715
52 changed files with 10495 additions and 0 deletions
+181
View File
@@ -0,0 +1,181 @@
package reaction
import (
"context"
"fmt"
"github.com/karti-ai/mattermost-mcp-server/pkg/log"
"github.com/karti-ai/mattermost-mcp-server/pkg/mattermost"
"github.com/karti-ai/mattermost-mcp-server/pkg/params"
"github.com/karti-ai/mattermost-mcp-server/pkg/to"
"github.com/karti-ai/mattermost-mcp-server/pkg/tool"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/mattermost/mattermost-server/v6/model"
)
var Tool = tool.New()
const (
AddReactionToolName = "mattermost_add_reaction"
RemoveReactionToolName = "mattermost_remove_reaction"
ListReactionsToolName = "mattermost_list_reactions"
)
var (
AddReactionTool = mcp.NewTool(
AddReactionToolName,
mcp.WithDescription("Add emoji reaction to post"),
mcp.WithString("post_id", mcp.Required(), mcp.Description("Post ID to add reaction to")),
mcp.WithString("emoji_name", mcp.Required(), mcp.Description("Emoji name without colons (e.g., thumbsup, not :thumbsup:)")),
)
RemoveReactionTool = mcp.NewTool(
RemoveReactionToolName,
mcp.WithDescription("Remove emoji reaction from post"),
mcp.WithString("post_id", mcp.Required(), mcp.Description("Post ID to remove reaction from")),
mcp.WithString("emoji_name", mcp.Required(), mcp.Description("Emoji name without colons")),
)
ListReactionsTool = mcp.NewTool(
ListReactionsToolName,
mcp.WithDescription("List all emoji reactions on a post"),
mcp.WithString("post_id", mcp.Required(), mcp.Description("Post ID to get reactions for")),
)
)
func init() {
registerTools()
}
func registerTools() {
tools := []server.ServerTool{
{Tool: AddReactionTool, Handler: AddReactionFn},
{Tool: RemoveReactionTool, Handler: RemoveReactionFn},
{Tool: ListReactionsTool, Handler: ListReactionsFn},
}
for _, t := range tools {
if t.Tool.Name == ListReactionsToolName {
Tool.RegisterRead(t)
} else {
Tool.RegisterWrite(t)
}
}
}
func AddReactionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("[Reaction] Called AddReactionFn")
args := req.GetArguments()
postId, err := params.GetString(args, "post_id")
if err != nil {
return to.Error(fmt.Errorf("[post_id] %v", err)), nil
}
emojiName, err := params.GetString(args, "emoji_name")
if err != nil {
return to.Error(fmt.Errorf("[emoji_name] %v", err)), nil
}
client := mattermost.GetGlobalClient()
if client == nil {
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
}
// Get current user ID
me, err := client.GetMe(ctx)
if err != nil {
return to.Error(fmt.Errorf("[user] failed to get current user: %v", err)), nil
}
reaction := &model.Reaction{
UserId: me.Id,
PostId: postId,
EmojiName: emojiName,
}
result, err := client.SaveReaction(ctx, reaction)
if err != nil {
return to.Error(fmt.Errorf("[reaction] failed to add reaction: %v", err)), nil
}
return to.Result(SlimReaction(result)), nil
}
func RemoveReactionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("[Reaction] Called RemoveReactionFn")
args := req.GetArguments()
postId, err := params.GetString(args, "post_id")
if err != nil {
return to.Error(fmt.Errorf("[post_id] %v", err)), nil
}
emojiName, err := params.GetString(args, "emoji_name")
if err != nil {
return to.Error(fmt.Errorf("[emoji_name] %v", err)), nil
}
client := mattermost.GetGlobalClient()
if client == nil {
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
}
// Get current user ID
me, err := client.GetMe(ctx)
if err != nil {
return to.Error(fmt.Errorf("[user] failed to get current user: %v", err)), nil
}
reaction := &model.Reaction{
UserId: me.Id,
PostId: postId,
EmojiName: emojiName,
}
err = client.DeleteReaction(ctx, reaction)
if err != nil {
return to.Error(fmt.Errorf("[reaction] failed to remove reaction: %v", err)), nil
}
return to.Result(map[string]interface{}{
"success": true,
"post_id": postId,
"emoji_name": emojiName,
"message": "Reaction removed successfully",
}), nil
}
func ListReactionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("[Reaction] Called ListReactionsFn")
args := req.GetArguments()
postId, err := params.GetString(args, "post_id")
if err != nil {
return to.Error(fmt.Errorf("[post_id] %v", err)), nil
}
client := mattermost.GetGlobalClient()
if client == nil {
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
}
reactions, err := client.GetReactions(ctx, postId)
if err != nil {
return to.Error(fmt.Errorf("[reaction] failed to list reactions: %v", err)), nil
}
results := make([]map[string]interface{}, 0, len(reactions))
for _, r := range reactions {
results = append(results, SlimReaction(r))
}
return to.Result(map[string]interface{}{
"reactions": results,
"count": len(results),
"post_id": postId,
}), nil
}
+78
View File
@@ -0,0 +1,78 @@
package reaction
import (
"testing"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/stretchr/testify/assert"
)
func TestSlimReaction(t *testing.T) {
tests := []struct {
name string
reaction *model.Reaction
expected map[string]interface{}
}{
{
name: "valid reaction",
reaction: &model.Reaction{
UserId: "user123",
PostId: "post456",
EmojiName: "thumbsup",
CreateAt: 1234567890,
},
expected: map[string]interface{}{
"user_id": "user123",
"post_id": "post456",
"emoji_name": "thumbsup",
"create_at": int64(1234567890),
},
},
{
name: "nil reaction",
reaction: nil,
expected: nil,
},
{
name: "reaction with different emoji",
reaction: &model.Reaction{
UserId: "user789",
PostId: "post012",
EmojiName: "rocket",
CreateAt: 9876543210,
},
expected: map[string]interface{}{
"user_id": "user789",
"post_id": "post012",
"emoji_name": "rocket",
"create_at": int64(9876543210),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SlimReaction(tt.reaction)
assert.Equal(t, tt.expected, result)
})
}
}
func TestToolConstants(t *testing.T) {
assert.Equal(t, "mattermost_add_reaction", AddReactionToolName)
assert.Equal(t, "mattermost_remove_reaction", RemoveReactionToolName)
}
func TestToolRegistration(t *testing.T) {
// Verify tools are registered
tools := Tool.Tools()
assert.Len(t, tools, 2)
toolNames := make([]string, len(tools))
for i, t := range tools {
toolNames[i] = t.Tool.Name
}
assert.Contains(t, toolNames, AddReactionToolName)
assert.Contains(t, toolNames, RemoveReactionToolName)
}
+15
View File
@@ -0,0 +1,15 @@
package reaction
import "github.com/mattermost/mattermost-server/v6/model"
func SlimReaction(r *model.Reaction) map[string]interface{} {
if r == nil {
return nil
}
return map[string]interface{}{
"user_id": r.UserId,
"post_id": r.PostId,
"emoji_name": r.EmojiName,
"create_at": r.CreateAt,
}
}