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
+115
View File
@@ -0,0 +1,115 @@
package dm
import (
"context"
"fmt"
"strings"
"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"
)
var Tool = tool.New()
const (
CreateDMToolName = "mattermost_create_dm"
CreateGroupDMToolName = "mattermost_create_group_dm"
)
var (
CreateDMTool = mcp.NewTool(
CreateDMToolName,
mcp.WithDescription("Create direct message channel with user"),
mcp.WithString("user_id", mcp.Required(), mcp.Description("User ID to DM with")),
)
CreateGroupDMTool = mcp.NewTool(
CreateGroupDMToolName,
mcp.WithDescription("Create group DM channel with multiple users"),
mcp.WithString("user_ids", mcp.Required(), mcp.Description("Comma-separated list of user IDs to include in group DM")),
)
)
func init() {
registerTools()
}
func registerTools() {
tools := []server.ServerTool{
{Tool: CreateDMTool, Handler: CreateDMFn},
{Tool: CreateGroupDMTool, Handler: CreateGroupDMFn},
}
for _, t := range tools {
Tool.RegisterWrite(t)
}
}
func CreateDMFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("[DM] Called CreateDMFn")
args := req.GetArguments()
userId, err := params.GetString(args, "user_id")
if err != nil {
return to.Error(fmt.Errorf("[user_id] %v", err)), nil
}
client := mattermost.GetGlobalClient()
if client == nil {
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
}
// Get current user to get our own user ID
me, err := client.GetMe(ctx)
if err != nil {
return to.Error(fmt.Errorf("[me] failed to get current user: %v", err)), nil
}
// Create direct message channel between current user and target user
channel, err := client.CreateDirectChannel(ctx, me.Id, userId)
if err != nil {
return to.Error(fmt.Errorf("[channel] failed to create DM: %v", err)), nil
}
return to.Result(SlimChannel(channel)), nil
}
func CreateGroupDMFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("[DM] Called CreateGroupDMFn")
args := req.GetArguments()
userIDsStr, err := params.GetString(args, "user_ids")
if err != nil {
return to.Error(fmt.Errorf("[user_ids] %v", err)), nil
}
// Parse comma-separated user IDs
userIDs := strings.Split(userIDsStr, ",")
if len(userIDs) < 2 {
return to.Error(fmt.Errorf("[user_ids] at least 2 user IDs required for group DM (got %d)", len(userIDs))), nil
}
// Trim whitespace from each ID
for i, id := range userIDs {
userIDs[i] = strings.TrimSpace(id)
}
client := mattermost.GetGlobalClient()
if client == nil {
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
}
// Create group DM channel
channel, err := client.CreateGroupChannel(ctx, userIDs)
if err != nil {
return to.Error(fmt.Errorf("[channel] failed to create group DM: %v", err)), nil
}
return to.Result(SlimChannel(channel)), nil
}
+77
View File
@@ -0,0 +1,77 @@
package dm
import (
"testing"
"github.com/karti-ai/mattermost-mcp-server/pkg/mattermost"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/stretchr/testify/assert"
)
func TestSlimChannel(t *testing.T) {
c := &model.Channel{
Id: "channel123",
Name: "user1__user2",
DisplayName: "user1, user2",
Type: model.ChannelTypeDirect,
TeamId: "",
}
slim := SlimChannel(c)
assert.NotNil(t, slim)
assert.Equal(t, "channel123", slim["id"])
assert.Equal(t, "user1__user2", slim["name"])
assert.Equal(t, "user1, user2", slim["display_name"])
assert.Equal(t, model.ChannelTypeDirect, slim["type"])
assert.Equal(t, "", slim["team_id"])
}
func TestSlimChannel_Nil(t *testing.T) {
slim := SlimChannel(nil)
assert.Nil(t, slim)
}
func TestToolRegistration(t *testing.T) {
tools := Tool.Tools()
assert.Len(t, tools, 1)
toolNames := make(map[string]bool)
for _, t := range tools {
toolNames[t.Tool.Name] = true
}
assert.True(t, toolNames[CreateDMToolName], "CreateDM tool should be registered")
}
func TestCreateDMFn_ClientNotInitialized(t *testing.T) {
mattermost.SetGlobalClient(nil)
req := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: CreateDMToolName,
Arguments: map[string]interface{}{
"user_id": "user123",
},
},
}
result, err := CreateDMFn(nil, req)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.IsError)
}
func TestCreateDMFn_MissingUserId(t *testing.T) {
req := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: CreateDMToolName,
Arguments: map[string]interface{}{},
},
}
result, err := CreateDMFn(nil, req)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.IsError)
}
+16
View File
@@ -0,0 +1,16 @@
package dm
import "github.com/mattermost/mattermost-server/v6/model"
func SlimChannel(c *model.Channel) map[string]interface{} {
if c == nil {
return nil
}
return map[string]interface{}{
"id": c.Id,
"name": c.Name,
"display_name": c.DisplayName,
"type": c.Type,
"team_id": c.TeamId,
}
}