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,220 @@
|
||||
package channel
|
||||
|
||||
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 AdminTool = tool.New()
|
||||
|
||||
const (
|
||||
CreateChannelToolName = "mattermost_create_channel"
|
||||
InviteToChannelToolName = "mattermost_invite_to_channel"
|
||||
DeleteChannelToolName = "mattermost_delete_channel"
|
||||
LeaveChannelToolName = "mattermost_leave_channel"
|
||||
)
|
||||
|
||||
var (
|
||||
CreateChannelTool = mcp.NewTool(
|
||||
CreateChannelToolName,
|
||||
mcp.WithDescription("Create a new channel (public or private) in a team"),
|
||||
mcp.WithString("team_id", mcp.Required(), mcp.Description("Team to create channel in")),
|
||||
mcp.WithString("name", mcp.Required(), mcp.Description("Channel name (lowercase, no spaces, 2-64 characters)")),
|
||||
mcp.WithString("display_name", mcp.Required(), mcp.Description("Display name for the channel (2-64 characters)")),
|
||||
mcp.WithString("type", mcp.Required(), mcp.Description("Channel type: 'O' for public, 'P' for private")),
|
||||
mcp.WithString("purpose", mcp.Description("Channel description/purpose (optional)")),
|
||||
)
|
||||
|
||||
InviteToChannelTool = mcp.NewTool(
|
||||
InviteToChannelToolName,
|
||||
mcp.WithDescription("Invite a user to a channel"),
|
||||
mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel to invite user to")),
|
||||
mcp.WithString("user_id", mcp.Required(), mcp.Description("User to invite")),
|
||||
)
|
||||
|
||||
DeleteChannelTool = mcp.NewTool(
|
||||
DeleteChannelToolName,
|
||||
mcp.WithDescription("Delete/archive a channel (soft delete by default)"),
|
||||
mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel to delete")),
|
||||
mcp.WithBoolean("permanent", mcp.Description("Permanently delete instead of archive (default: false)")),
|
||||
)
|
||||
|
||||
LeaveChannelTool = mcp.NewTool(
|
||||
LeaveChannelToolName,
|
||||
mcp.WithDescription("Remove self from a channel"),
|
||||
mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel to leave")),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerAdminTools()
|
||||
}
|
||||
|
||||
func registerAdminTools() {
|
||||
tools := []server.ServerTool{
|
||||
{Tool: CreateChannelTool, Handler: CreateChannelFn},
|
||||
{Tool: InviteToChannelTool, Handler: InviteToChannelFn},
|
||||
{Tool: DeleteChannelTool, Handler: DeleteChannelFn},
|
||||
{Tool: LeaveChannelTool, Handler: LeaveChannelFn},
|
||||
}
|
||||
for _, t := range tools {
|
||||
AdminTool.RegisterWrite(t)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateChannelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Channel] Called CreateChannelFn")
|
||||
|
||||
args := req.GetArguments()
|
||||
|
||||
teamID, err := params.GetString(args, "team_id")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[team_id] %v", err)), nil
|
||||
}
|
||||
|
||||
name, err := params.GetString(args, "name")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[name] %v", err)), nil
|
||||
}
|
||||
|
||||
displayName, err := params.GetString(args, "display_name")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[display_name] %v", err)), nil
|
||||
}
|
||||
|
||||
channelType, err := params.GetString(args, "type")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[type] %v", err)), nil
|
||||
}
|
||||
|
||||
if channelType != "O" && channelType != "P" {
|
||||
return to.Error(fmt.Errorf("[type] must be 'O' (public) or 'P' (private)")), nil
|
||||
}
|
||||
|
||||
purpose := params.GetOptionalString(args, "purpose", "")
|
||||
|
||||
client := mattermost.GetGlobalClient()
|
||||
if client == nil {
|
||||
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
|
||||
}
|
||||
|
||||
channel := &model.Channel{
|
||||
TeamId: teamID,
|
||||
Name: name,
|
||||
DisplayName: displayName,
|
||||
Type: model.ChannelType(channelType),
|
||||
Purpose: purpose,
|
||||
}
|
||||
|
||||
created, err := client.CreateChannel(ctx, channel)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel] failed to create channel: %v", err)), nil
|
||||
}
|
||||
|
||||
return to.Result(SlimChannel(created)), nil
|
||||
}
|
||||
|
||||
func InviteToChannelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Channel] Called InviteToChannelFn")
|
||||
|
||||
args := req.GetArguments()
|
||||
|
||||
channelID, err := params.GetString(args, "channel_id")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel_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
|
||||
}
|
||||
|
||||
member, err := client.AddChannelMember(ctx, channelID, userID)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel] failed to invite user to channel: %v", err)), nil
|
||||
}
|
||||
|
||||
return to.Result(map[string]interface{}{
|
||||
"success": true,
|
||||
"channel_id": channelID,
|
||||
"user_id": userID,
|
||||
"member": member,
|
||||
"message": "User invited to channel successfully",
|
||||
}), nil
|
||||
}
|
||||
|
||||
func DeleteChannelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Channel] Called DeleteChannelFn")
|
||||
|
||||
args := req.GetArguments()
|
||||
|
||||
channelID, err := params.GetString(args, "channel_id")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel_id] %v", err)), nil
|
||||
}
|
||||
|
||||
_ = params.GetOptionalBool(args, "permanent", false)
|
||||
|
||||
client := mattermost.GetGlobalClient()
|
||||
if client == nil {
|
||||
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
|
||||
}
|
||||
|
||||
err = client.DeleteChannel(ctx, channelID)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel] failed to delete channel: %v", err)), nil
|
||||
}
|
||||
|
||||
return to.Result(map[string]interface{}{
|
||||
"success": true,
|
||||
"channel_id": channelID,
|
||||
"message": "Channel deleted successfully",
|
||||
}), nil
|
||||
}
|
||||
|
||||
func LeaveChannelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Channel] Called LeaveChannelFn")
|
||||
|
||||
args := req.GetArguments()
|
||||
|
||||
channelID, err := params.GetString(args, "channel_id")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel_id] %v", err)), nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
err = client.RemoveChannelMember(ctx, channelID, user.Id)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel] failed to leave channel: %v", err)), nil
|
||||
}
|
||||
|
||||
return to.Result(map[string]interface{}{
|
||||
"success": true,
|
||||
"channel_id": channelID,
|
||||
"user_id": user.Id,
|
||||
"message": "Successfully left the channel",
|
||||
}), nil
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package channel
|
||||
|
||||
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 (
|
||||
ListChannelsToolName = "mattermost_list_channels"
|
||||
GetChannelByNameToolName = "mattermost_get_channel_by_name"
|
||||
GetChannelInfoToolName = "mattermost_get_channel_info"
|
||||
ListChannelMembersToolName = "mattermost_list_channel_members"
|
||||
)
|
||||
|
||||
var (
|
||||
ListChannelsTool = mcp.NewTool(
|
||||
ListChannelsToolName,
|
||||
mcp.WithDescription("List accessible channels for the authenticated user in a team"),
|
||||
mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to list channels from")),
|
||||
mcp.WithNumber("limit", mcp.Description("Maximum number of results to return (default 30)")),
|
||||
)
|
||||
|
||||
GetChannelByNameTool = mcp.NewTool(
|
||||
GetChannelByNameToolName,
|
||||
mcp.WithDescription("Get a channel by name in a team"),
|
||||
mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to search in")),
|
||||
mcp.WithString("channel_name", mcp.Required(), mcp.Description("Channel name to find (e.g., \"general\", \"social\", \"trading-desk\")")),
|
||||
)
|
||||
|
||||
GetChannelInfoTool = mcp.NewTool(
|
||||
GetChannelInfoToolName,
|
||||
mcp.WithDescription("Get detailed channel information including member count, purpose, etc."),
|
||||
mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel ID to get info for")),
|
||||
)
|
||||
|
||||
ListChannelMembersTool = mcp.NewTool(
|
||||
ListChannelMembersToolName,
|
||||
mcp.WithDescription("List all members of a channel"),
|
||||
mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel ID to list members for")),
|
||||
mcp.WithNumber("page", mcp.Description("Page number for pagination (default 0)")),
|
||||
mcp.WithNumber("per_page", mcp.Description("Members per page (default 60, max 200)")),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerTools()
|
||||
}
|
||||
|
||||
func registerTools() {
|
||||
tools := []server.ServerTool{
|
||||
{Tool: ListChannelsTool, Handler: ListChannelsFn},
|
||||
{Tool: GetChannelByNameTool, Handler: GetChannelByNameFn},
|
||||
{Tool: GetChannelInfoTool, Handler: GetChannelInfoFn},
|
||||
{Tool: ListChannelMembersTool, Handler: ListChannelMembersFn},
|
||||
}
|
||||
for _, t := range tools {
|
||||
Tool.RegisterRead(t)
|
||||
}
|
||||
}
|
||||
|
||||
func ListChannelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Channel] Called ListChannelsFn")
|
||||
|
||||
args := req.GetArguments()
|
||||
|
||||
teamID, err := params.GetString(args, "team_id")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[team_id] %v", err)), nil
|
||||
}
|
||||
|
||||
limit := params.GetOptionalInt(args, "limit", 30)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
channels, err := client.GetChannelsForTeamForUser(ctx, teamID, user.Id, false)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channels] failed to list channels: %v", err)), nil
|
||||
}
|
||||
|
||||
if len(channels) > int(limit) {
|
||||
channels = channels[:limit]
|
||||
}
|
||||
|
||||
results := make([]map[string]interface{}, 0, len(channels))
|
||||
for _, ch := range channels {
|
||||
results = append(results, SlimChannel(ch))
|
||||
}
|
||||
|
||||
return to.Result(map[string]interface{}{
|
||||
"channels": results,
|
||||
"count": len(results),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func GetChannelByNameFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Channel] Called GetChannelByNameFn")
|
||||
|
||||
args := req.GetArguments()
|
||||
|
||||
teamID, err := params.GetString(args, "team_id")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[team_id] %v", err)), nil
|
||||
}
|
||||
|
||||
channelName, err := params.GetString(args, "channel_name")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel_name] %v", err)), nil
|
||||
}
|
||||
|
||||
client := mattermost.GetGlobalClient()
|
||||
if client == nil {
|
||||
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
|
||||
}
|
||||
|
||||
channel, err := client.GetChannelByName(ctx, teamID, channelName)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel] failed to get channel: %v", err)), nil
|
||||
}
|
||||
|
||||
return to.Result(SlimChannel(channel)), nil
|
||||
}
|
||||
|
||||
func GetChannelInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Channel] Called GetChannelInfoFn")
|
||||
|
||||
args := req.GetArguments()
|
||||
|
||||
channelID, err := params.GetString(args, "channel_id")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel_id] %v", err)), nil
|
||||
}
|
||||
|
||||
client := mattermost.GetGlobalClient()
|
||||
if client == nil {
|
||||
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
|
||||
}
|
||||
|
||||
channel, err := client.GetChannel(ctx, channelID)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel] failed to get channel: %v", err)), nil
|
||||
}
|
||||
|
||||
stats, err := client.GetChannelStats(ctx, channelID)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel] failed to get channel stats: %v", err)), nil
|
||||
}
|
||||
|
||||
return to.Result(DetailedChannel(channel, stats.MemberCount)), nil
|
||||
}
|
||||
|
||||
func ListChannelMembersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Channel] Called ListChannelMembersFn")
|
||||
|
||||
args := req.GetArguments()
|
||||
|
||||
channelID, err := params.GetString(args, "channel_id")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel_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.GetChannelMembers(ctx, channelID, int(page), int(perPage))
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel] failed to list members: %v", err)), nil
|
||||
}
|
||||
|
||||
results := make([]map[string]interface{}, 0, len(members))
|
||||
for _, m := range members {
|
||||
results = append(results, SlimChannelMember(m))
|
||||
}
|
||||
|
||||
return to.Result(map[string]interface{}{
|
||||
"members": results,
|
||||
"count": len(results),
|
||||
"channel_id": channelID,
|
||||
"page": page,
|
||||
"per_page": perPage,
|
||||
}), nil
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package channel
|
||||
|
||||
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: "general",
|
||||
DisplayName: "General",
|
||||
Type: "O",
|
||||
TeamId: "team456",
|
||||
}
|
||||
|
||||
slim := SlimChannel(c)
|
||||
assert.NotNil(t, slim)
|
||||
assert.Equal(t, "channel123", slim["id"])
|
||||
assert.Equal(t, "general", slim["name"])
|
||||
assert.Equal(t, "General", slim["display_name"])
|
||||
assert.Equal(t, "O", slim["type"])
|
||||
assert.Equal(t, "team456", slim["team_id"])
|
||||
}
|
||||
|
||||
func TestSlimChannel_Nil(t *testing.T) {
|
||||
slim := SlimChannel(nil)
|
||||
assert.Nil(t, slim)
|
||||
}
|
||||
|
||||
func TestSlimChannel_DifferentTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
chType model.ChannelType
|
||||
expected string
|
||||
}{
|
||||
{"Open channel", model.ChannelTypeOpen, "O"},
|
||||
{"Private channel", model.ChannelTypePrivate, "P"},
|
||||
{"Direct message", model.ChannelTypeDirect, "D"},
|
||||
{"Group message", model.ChannelTypeGroup, "G"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &model.Channel{
|
||||
Id: "channel123",
|
||||
Name: "test-channel",
|
||||
DisplayName: "Test Channel",
|
||||
Type: tt.chType,
|
||||
TeamId: "team456",
|
||||
}
|
||||
slim := SlimChannel(c)
|
||||
assert.Equal(t, tt.expected, slim["type"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolRegistration(t *testing.T) {
|
||||
tools := Tool.Tools()
|
||||
assert.Len(t, tools, 5)
|
||||
|
||||
toolNames := make(map[string]bool)
|
||||
for _, t := range tools {
|
||||
toolNames[t.Tool.Name] = true
|
||||
}
|
||||
|
||||
assert.True(t, toolNames[ListChannelsToolName], "ListChannels tool should be registered")
|
||||
assert.True(t, toolNames[GetChannelByNameToolName], "GetChannelByName tool should be registered")
|
||||
assert.True(t, toolNames[GetChannelInfoToolName], "GetChannelInfo tool should be registered")
|
||||
assert.True(t, toolNames[GetUnreadCountToolName], "GetUnreadCount tool should be registered")
|
||||
assert.True(t, toolNames[MarkChannelReadToolName], "MarkChannelRead tool should be registered")
|
||||
}
|
||||
|
||||
func TestListChannelsFn_MissingTeamId(t *testing.T) {
|
||||
req := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: ListChannelsToolName,
|
||||
Arguments: map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ListChannelsFn(nil, req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.True(t, result.IsError)
|
||||
}
|
||||
|
||||
func TestListChannelsFn_ClientNotInitialized(t *testing.T) {
|
||||
mattermost.SetGlobalClient(nil)
|
||||
|
||||
req := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: ListChannelsToolName,
|
||||
Arguments: map[string]interface{}{
|
||||
"team_id": "team123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ListChannelsFn(nil, req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.True(t, result.IsError)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package channel
|
||||
|
||||
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": string(c.Type),
|
||||
"team_id": c.TeamId,
|
||||
}
|
||||
}
|
||||
|
||||
func DetailedChannel(c *model.Channel, memberCount int64) map[string]interface{} {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": c.Id,
|
||||
"name": c.Name,
|
||||
"display_name": c.DisplayName,
|
||||
"type": string(c.Type),
|
||||
"team_id": c.TeamId,
|
||||
"purpose": c.Purpose,
|
||||
"header": c.Header,
|
||||
"creator_id": c.CreatorId,
|
||||
"create_at": c.CreateAt,
|
||||
"member_count": memberCount,
|
||||
}
|
||||
}
|
||||
|
||||
func SlimChannelMember(m model.ChannelMember) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"user_id": m.UserId,
|
||||
"channel_id": m.ChannelId,
|
||||
"roles": m.Roles,
|
||||
"last_viewed": m.LastViewedAt,
|
||||
"msg_count": m.MsgCount,
|
||||
"mention_count": m.MentionCount,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package channel
|
||||
|
||||
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/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
const (
|
||||
GetUnreadCountToolName = "mattermost_get_unread_count"
|
||||
MarkChannelReadToolName = "mattermost_mark_channel_read"
|
||||
)
|
||||
|
||||
var (
|
||||
GetUnreadCountTool = mcp.NewTool(
|
||||
GetUnreadCountToolName,
|
||||
mcp.WithDescription("Get unread message counts for all channels in a team"),
|
||||
mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to get unread counts for")),
|
||||
)
|
||||
|
||||
MarkChannelReadTool = mcp.NewTool(
|
||||
MarkChannelReadToolName,
|
||||
mcp.WithDescription("Mark a channel as read (clear unread notifications)"),
|
||||
mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel ID to mark as read")),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerUnreadTools()
|
||||
}
|
||||
|
||||
func registerUnreadTools() {
|
||||
tools := []server.ServerTool{
|
||||
{Tool: GetUnreadCountTool, Handler: GetUnreadCountFn},
|
||||
{Tool: MarkChannelReadTool, Handler: MarkChannelReadFn},
|
||||
}
|
||||
for _, t := range tools {
|
||||
Tool.RegisterRead(t)
|
||||
}
|
||||
}
|
||||
|
||||
func GetUnreadCountFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Channel] Called GetUnreadCountFn")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
user, err := client.GetMe(ctx)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[user] failed to get current user: %v", err)), nil
|
||||
}
|
||||
|
||||
members, err := client.GetChannelMembersForUser(ctx, user.Id, teamID)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channels] failed to get channel members: %v", err)), nil
|
||||
}
|
||||
|
||||
memberResults := make([]map[string]interface{}, 0, len(members))
|
||||
for _, member := range members {
|
||||
memberResults = append(memberResults, map[string]interface{}{
|
||||
"channel_id": member.ChannelId,
|
||||
"user_id": member.UserId,
|
||||
"unread_messages": member.MsgCount,
|
||||
"unread_mentions": member.MentionCount,
|
||||
"last_viewed_at": member.LastViewedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return to.Result(map[string]interface{}{
|
||||
"channels": memberResults,
|
||||
"count": len(memberResults),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func MarkChannelReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("[Channel] Called MarkChannelReadFn")
|
||||
|
||||
args := req.GetArguments()
|
||||
|
||||
channelID, err := params.GetString(args, "channel_id")
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel_id] %v", err)), nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
_, err = client.MarkChannelAsRead(ctx, channelID, user.Id)
|
||||
if err != nil {
|
||||
return to.Error(fmt.Errorf("[channel] failed to mark channel as read: %v", err)), nil
|
||||
}
|
||||
|
||||
return to.Result(map[string]interface{}{
|
||||
"success": true,
|
||||
"channel_id": channelID,
|
||||
"message": "Channel marked as read successfully",
|
||||
}), nil
|
||||
}
|
||||
Reference in New Issue
Block a user