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:
Karti Tripathi
2026-04-15 21:14:47 -07:00
commit a4e51f6412
52 changed files with 10495 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
package user
import "github.com/mattermost/mattermost-server/v6/model"
func SlimUser(u *model.User) map[string]interface{} {
if u == nil {
return nil
}
return map[string]interface{}{
"id": u.Id,
"username": u.Username,
"email": u.Email,
"first_name": u.FirstName,
"last_name": u.LastName,
"roles": u.Roles,
}
}
+171
View File
@@ -0,0 +1,171 @@
package user
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/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
GetUserStatusToolName = "mattermost_get_user_status"
UpdateUserStatusToolName = "mattermost_update_user_status"
GetUsersStatusBulkToolName = "mattermost_get_users_status_bulk"
)
var (
GetUserStatusTool = mcp.NewTool(
GetUserStatusToolName,
mcp.WithDescription("Get the online status of a user (online, away, dnd, offline)"),
mcp.WithString("user_id", mcp.Required(), mcp.Description("User ID to check status for")),
)
UpdateUserStatusTool = mcp.NewTool(
UpdateUserStatusToolName,
mcp.WithDescription("Update your status (online, away, dnd, offline)"),
mcp.WithString("status", mcp.Required(), mcp.Description("Status to set: online, away, dnd, or offline")),
)
GetUsersStatusBulkTool = mcp.NewTool(
GetUsersStatusBulkToolName,
mcp.WithDescription("Get status for multiple users at once (up to 100)"),
mcp.WithString("user_ids", mcp.Required(), mcp.Description("Comma-separated list of user IDs (max 100)")),
)
)
func init() {
registerStatusTools()
}
func registerStatusTools() {
tools := []server.ServerTool{
{Tool: GetUserStatusTool, Handler: GetUserStatusFn},
{Tool: UpdateUserStatusTool, Handler: UpdateUserStatusFn},
{Tool: GetUsersStatusBulkTool, Handler: GetUsersStatusBulkFn},
}
for _, t := range tools {
if t.Tool.Name == UpdateUserStatusToolName {
Tool.RegisterWrite(t)
} else {
Tool.RegisterRead(t)
}
}
}
func GetUserStatusFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("[User] Called GetUserStatusFn")
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
}
status, err := client.GetUserStatus(ctx, userID)
if err != nil {
return to.Error(fmt.Errorf("[status] failed to get user status: %v", err)), nil
}
return to.Result(map[string]interface{}{
"user_id": status.UserId,
"status": status.Status,
"manual": status.Manual,
"last_activity_at": status.LastActivityAt,
}), nil
}
func UpdateUserStatusFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("[User] Called UpdateUserStatusFn")
args := req.GetArguments()
status, err := params.GetString(args, "status")
if err != nil {
return to.Error(fmt.Errorf("[status] %v", err)), nil
}
// Validate status value
validStatuses := map[string]bool{"online": true, "away": true, "dnd": true, "offline": true}
if !validStatuses[status] {
return to.Error(fmt.Errorf("[status] invalid status '%s', must be one of: online, away, dnd, offline", status)), 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("[me] failed to get current user: %v", err)), nil
}
updatedStatus, err := client.UpdateUserStatus(ctx, me.Id, status)
if err != nil {
return to.Error(fmt.Errorf("[status] failed to update status: %v", err)), nil
}
return to.Result(map[string]interface{}{
"user_id": updatedStatus.UserId,
"status": updatedStatus.Status,
"message": "Status updated successfully",
}), nil
}
func GetUsersStatusBulkFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("[User] Called GetUsersStatusBulkFn")
args := req.GetArguments()
userIDsStr, err := params.GetString(args, "user_ids")
if err != nil {
return to.Error(fmt.Errorf("[user_ids] %v", err)), nil
}
userIDs := strings.Split(userIDsStr, ",")
if len(userIDs) > 100 {
return to.Error(fmt.Errorf("[user_ids] too many user IDs (max 100, got %d)", len(userIDs))), nil
}
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
}
statuses, err := client.GetUsersStatus(ctx, userIDs)
if err != nil {
return to.Error(fmt.Errorf("[status] failed to get users status: %v", err)), nil
}
results := make([]map[string]interface{}, 0, len(statuses))
for _, status := range statuses {
results = append(results, map[string]interface{}{
"user_id": status.UserId,
"status": status.Status,
"manual": status.Manual,
"last_activity_at": status.LastActivityAt,
})
}
return to.Result(map[string]interface{}{
"statuses": results,
"count": len(results),
}), nil
}
+129
View File
@@ -0,0 +1,129 @@
package user
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 (
SearchUsersToolName = "mattermost_search_users"
GetUserToolName = "mattermost_get_user"
)
var (
SearchUsersTool = mcp.NewTool(
SearchUsersToolName,
mcp.WithDescription("Search users by term"),
mcp.WithString("term", mcp.Required(), mcp.Description("Search term (username, email, name)")),
mcp.WithString("team_id", mcp.Description("Limit to team (optional)")),
mcp.WithNumber("limit", mcp.Description("Max results (default 30)")),
)
GetUserTool = mcp.NewTool(
GetUserToolName,
mcp.WithDescription("Get a specific user by ID or username"),
mcp.WithString("user_id", mcp.Description("User ID to look up (optional if username provided)")),
mcp.WithString("username", mcp.Description("Username to look up (optional if user_id provided)")),
)
)
func init() {
registerTools()
}
func registerTools() {
tools := []server.ServerTool{
{Tool: SearchUsersTool, Handler: SearchUsersFn},
{Tool: GetUserTool, Handler: GetUserFn},
}
for _, t := range tools {
Tool.RegisterRead(t)
}
}
func SearchUsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("[User] Called SearchUsersFn")
args := req.GetArguments()
term, err := params.GetString(args, "term")
if err != nil {
return to.Error(fmt.Errorf("[term] %v", err)), nil
}
teamID := params.GetOptionalString(args, "team_id", "")
limit := params.GetOptionalInt(args, "limit", 30)
client := mattermost.GetGlobalClient()
if client == nil {
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
}
search := &model.UserSearch{
Term: term,
Limit: int(limit),
}
if teamID != "" {
search.TeamId = teamID
}
users, err := client.SearchUsers(ctx, search)
if err != nil {
return to.Error(fmt.Errorf("[users] failed to search users: %v", err)), nil
}
results := make([]map[string]interface{}, 0, len(users))
for _, u := range users {
results = append(results, SlimUser(u))
}
return to.Result(map[string]interface{}{
"users": results,
"count": len(results),
}), nil
}
func GetUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("[User] Called GetUserFn")
args := req.GetArguments()
userID := params.GetOptionalString(args, "user_id", "")
username := params.GetOptionalString(args, "username", "")
if userID == "" && username == "" {
return to.Error(fmt.Errorf("[user] either user_id or username must be provided")), nil
}
client := mattermost.GetGlobalClient()
if client == nil {
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
}
var user *model.User
var err error
if userID != "" {
user, err = client.GetUser(ctx, userID)
} else {
user, err = client.GetUserByUsername(ctx, username)
}
if err != nil {
return to.Error(fmt.Errorf("[user] failed to get user: %v", err)), nil
}
return to.Result(SlimUser(user)), nil
}
+80
View File
@@ -0,0 +1,80 @@
package user
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 TestSlimUser(t *testing.T) {
u := &model.User{
Id: "user123",
Username: "johndoe",
Email: "john@example.com",
FirstName: "John",
LastName: "Doe",
Roles: "system_user",
}
slim := SlimUser(u)
assert.NotNil(t, slim)
assert.Equal(t, "user123", slim["id"])
assert.Equal(t, "johndoe", slim["username"])
assert.Equal(t, "john@example.com", slim["email"])
assert.Equal(t, "John", slim["first_name"])
assert.Equal(t, "Doe", slim["last_name"])
assert.Equal(t, "system_user", slim["roles"])
}
func TestSlimUser_Nil(t *testing.T) {
slim := SlimUser(nil)
assert.Nil(t, slim)
}
func TestToolRegistration(t *testing.T) {
tools := Tool.Tools()
assert.Len(t, tools, 2)
toolNames := make(map[string]bool)
for _, t := range tools {
toolNames[t.Tool.Name] = true
}
assert.True(t, toolNames[SearchUsersToolName], "SearchUsers tool should be registered")
assert.True(t, toolNames[GetUserStatusToolName], "GetUserStatus tool should be registered")
}
func TestSearchUsersFn_ClientNotInitialized(t *testing.T) {
mattermost.SetGlobalClient(nil)
req := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: SearchUsersToolName,
Arguments: map[string]interface{}{
"term": "john",
},
},
}
result, err := SearchUsersFn(nil, req)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.IsError)
}
func TestSearchUsersFn_MissingTerm(t *testing.T) {
req := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: SearchUsersToolName,
Arguments: map[string]interface{}{},
},
}
result, err := SearchUsersFn(nil, req)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.IsError)
}