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:
@@ -0,0 +1,15 @@
|
||||
package team
|
||||
|
||||
import "github.com/mattermost/mattermost-server/v6/model"
|
||||
|
||||
func SlimTeam(t *model.Team) map[string]interface{} {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": t.Id,
|
||||
"name": t.Name,
|
||||
"display_name": t.DisplayName,
|
||||
"description": t.Description,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package team
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
var Tool = tool.New()
|
||||
|
||||
const (
|
||||
ListTeamsToolName = "mattermost_list_teams"
|
||||
ListTeamMembersToolName = "mattermost_list_team_members"
|
||||
InviteUserToTeamToolName = "mattermost_invite_user_to_team"
|
||||
RemoveUserFromTeamToolName = "mattermost_remove_user_from_team"
|
||||
GetTeamStatsToolName = "mattermost_get_team_stats"
|
||||
)
|
||||
|
||||
var (
|
||||
ListTeamsTool = mcp.NewTool(
|
||||
ListTeamsToolName,
|
||||
mcp.WithDescription("List all teams the bot has access to"),
|
||||
)
|
||||
|
||||
ListTeamMembersTool = mcp.NewTool(
|
||||
ListTeamMembersToolName,
|
||||
mcp.WithDescription("List all members of a team"),
|
||||
mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to list members for")),
|
||||
mcp.WithNumber("page", mcp.Description("Page number (default 0)")),
|
||||
mcp.WithNumber("per_page", mcp.Description("Members per page (default 60, max 200)")),
|
||||
)
|
||||
|
||||
InviteUserToTeamTool = mcp.NewTool(
|
||||
InviteUserToTeamToolName,
|
||||
mcp.WithDescription("Invite/add a user to a team"),
|
||||
mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to invite user to")),
|
||||
mcp.WithString("user_id", mcp.Required(), mcp.Description("User ID to invite")),
|
||||
)
|
||||
|
||||
RemoveUserFromTeamTool = mcp.NewTool(
|
||||
RemoveUserFromTeamToolName,
|
||||
mcp.WithDescription("Remove a user from a team"),
|
||||
mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to remove user from")),
|
||||
mcp.WithString("user_id", mcp.Required(), mcp.Description("User ID to remove")),
|
||||
)
|
||||
|
||||
GetTeamStatsTool = mcp.NewTool(
|
||||
GetTeamStatsToolName,
|
||||
mcp.WithDescription("Get statistics for a team (member count, etc.)"),
|
||||
mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to get stats for")),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerTools()
|
||||
}
|
||||
|
||||
func registerTools() {
|
||||
tools := []server.ServerTool{
|
||||
{Tool: ListTeamsTool, Handler: ListTeamsFn},
|
||||
{Tool: ListTeamMembersTool, Handler: ListTeamMembersFn},
|
||||
{Tool: GetTeamStatsTool, Handler: GetTeamStatsFn},
|
||||
{Tool: InviteUserToTeamTool, Handler: InviteUserToTeamFn},
|
||||
{Tool: RemoveUserFromTeamTool, Handler: RemoveUserFromTeamFn},
|
||||
}
|
||||
for _, t := range tools {
|
||||
if t.Tool.Name == InviteUserToTeamToolName || t.Tool.Name == RemoveUserFromTeamToolName {
|
||||
Tool.RegisterWrite(t)
|
||||
} else {
|
||||
Tool.RegisterRead(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ListTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Team] Called ListTeamsFn")
|
||||
|
||||
client := mattermost.GetGlobalClient()
|
||||
if client == nil {
|
||||
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
|
||||
}
|
||||
|
||||
user, err := client.GetMe(ctx)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[user] failed to get current user: %v", err)), nil
|
||||
}
|
||||
|
||||
teams, err := client.GetTeamsForUser(ctx, user.Id)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[teams] failed to list teams: %v", err)), nil
|
||||
}
|
||||
|
||||
results := make([]map[string]interface{}, 0, len(teams))
|
||||
for _, t := range teams {
|
||||
results = append(results, SlimTeam(t))
|
||||
}
|
||||
|
||||
return to.Result(map[string]interface{}{
|
||||
"teams": results,
|
||||
"count": len(results),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func ListTeamMembersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Team] Called ListTeamMembersFn")
|
||||
|
||||
args := req.GetArguments()
|
||||
|
||||
teamID, err := params.GetString(args, "team_id")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[team_id] %v", err)), nil
|
||||
}
|
||||
|
||||
page := params.GetOptionalInt(args, "page", 0)
|
||||
perPage := params.GetOptionalInt(args, "per_page", 60)
|
||||
if perPage > 200 {
|
||||
perPage = 200
|
||||
}
|
||||
|
||||
client := mattermost.GetGlobalClient()
|
||||
if client == nil {
|
||||
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
|
||||
}
|
||||
|
||||
members, err := client.ListTeamMembers(ctx, teamID, int(page), int(perPage))
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[team] failed to list members: %v", err)), nil
|
||||
}
|
||||
|
||||
results := make([]map[string]interface{}, 0, len(members))
|
||||
for _, m := range members {
|
||||
results = append(results, map[string]interface{}{
|
||||
"user_id": m.UserId,
|
||||
"roles": m.Roles,
|
||||
})
|
||||
}
|
||||
|
||||
return to.Result(map[string]interface{}{
|
||||
"members": results,
|
||||
"count": len(results),
|
||||
"team_id": teamID,
|
||||
"page": page,
|
||||
"per_page": perPage,
|
||||
}), nil
|
||||
}
|
||||
|
||||
func InviteUserToTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Team] Called InviteUserToTeamFn")
|
||||
|
||||
args := req.GetArguments()
|
||||
|
||||
teamID, err := params.GetString(args, "team_id")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[team_id] %v", err)), nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
_, err = client.InviteUserToTeam(ctx, teamID, userID)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[team] failed to invite user: %v", err)), nil
|
||||
}
|
||||
|
||||
return to.Result(map[string]interface{}{
|
||||
"success": true,
|
||||
"team_id": teamID,
|
||||
"user_id": userID,
|
||||
"message": "User invited to team successfully",
|
||||
}), nil
|
||||
}
|
||||
|
||||
func RemoveUserFromTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Team] Called RemoveUserFromTeamFn")
|
||||
|
||||
args := req.GetArguments()
|
||||
|
||||
teamID, err := params.GetString(args, "team_id")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[team_id] %v", err)), nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
err = client.RemoveUserFromTeam(ctx, teamID, userID)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[team] failed to remove user: %v", err)), nil
|
||||
}
|
||||
|
||||
return to.Result(map[string]interface{}{
|
||||
"success": true,
|
||||
"team_id": teamID,
|
||||
"user_id": userID,
|
||||
"message": "User removed from team successfully",
|
||||
}), nil
|
||||
}
|
||||
|
||||
func GetTeamStatsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Team] Called GetTeamStatsFn")
|
||||
|
||||
args := req.GetArguments()
|
||||
|
||||
teamID, err := params.GetString(args, "team_id")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[team_id] %v", err)), nil
|
||||
}
|
||||
|
||||
client := mattermost.GetGlobalClient()
|
||||
if client == nil {
|
||||
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
|
||||
}
|
||||
|
||||
stats, err := client.GetTeamStats(ctx, teamID)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[team] failed to get stats: %v", err)), nil
|
||||
}
|
||||
|
||||
return to.Result(map[string]interface{}{
|
||||
"team_id": teamID,
|
||||
"total_members": stats.TotalMemberCount,
|
||||
"active_members": stats.ActiveMemberCount,
|
||||
}), nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package team
|
||||
|
||||
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 TestSlimTeam(t *testing.T) {
|
||||
team := &model.Team{
|
||||
Id: "team123",
|
||||
Name: "my-team",
|
||||
DisplayName: "My Team",
|
||||
Description: "A test team for unit testing",
|
||||
}
|
||||
|
||||
slim := SlimTeam(team)
|
||||
assert.NotNil(t, slim)
|
||||
assert.Equal(t, "team123", slim["id"])
|
||||
assert.Equal(t, "my-team", slim["name"])
|
||||
assert.Equal(t, "My Team", slim["display_name"])
|
||||
assert.Equal(t, "A test team for unit testing", slim["description"])
|
||||
}
|
||||
|
||||
func TestSlimTeam_Nil(t *testing.T) {
|
||||
slim := SlimTeam(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[ListTeamsToolName], "ListTeams tool should be registered")
|
||||
}
|
||||
|
||||
func TestListTeamsFn_ClientNotInitialized(t *testing.T) {
|
||||
mattermost.SetGlobalClient(nil)
|
||||
|
||||
req := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: ListTeamsToolName,
|
||||
Arguments: map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ListTeamsFn(nil, req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.True(t, result.IsError)
|
||||
}
|
||||
Reference in New Issue
Block a user