From d3ff70f6d3c98af61c94faec52540f9228975c24 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Sat, 14 Mar 2026 14:48:26 +0200 Subject: [PATCH] fix: add get_channel_messages MCP tool and fix search to include channel messages Agents could send channel messages but had no MCP tool to read them. Also, search_messages only searched DMs (to/from agent), missing channel messages entirely. Now SearchMessages includes channel messages where the agent is a member. Co-Authored-By: Claude Opus 4.6 --- internal/channels/service.go | 5 +++ internal/mcp/channel_tools.go | 67 +++++++++++++++++++++++++++++- internal/mcp/channel_tools_test.go | 2 +- internal/mcp/server.go | 2 +- internal/messaging/store.go | 8 ++-- 5 files changed, 78 insertions(+), 6 deletions(-) diff --git a/internal/channels/service.go b/internal/channels/service.go index 3ea25cb..2b53aaa 100644 --- a/internal/channels/service.go +++ b/internal/channels/service.go @@ -332,6 +332,11 @@ func (s *Service) GetChannel(ctx context.Context, id int64) (*Channel, error) { return s.store.GetChannel(ctx, id) } +// IsMember checks if an agent is a member of a channel. +func (s *Service) IsMember(ctx context.Context, channelID int64, agentName string) (bool, error) { + return s.store.IsMember(ctx, channelID, agentName) +} + // GetChannelByName returns a channel by name. func (s *Service) GetChannelByName(ctx context.Context, name string) (*Channel, error) { return s.store.GetChannelByName(ctx, NormalizeChannelName(name)) diff --git a/internal/mcp/channel_tools.go b/internal/mcp/channel_tools.go index 9b33fe2..34d9212 100644 --- a/internal/mcp/channel_tools.go +++ b/internal/mcp/channel_tools.go @@ -8,17 +8,20 @@ import ( "github.com/mark3labs/mcp-go/server" "github.com/synapbus/synapbus/internal/channels" + "github.com/synapbus/synapbus/internal/messaging" ) // ChannelToolRegistrar registers channel MCP tools on the server. type ChannelToolRegistrar struct { channelService *channels.Service + msgService *messaging.MessagingService } // NewChannelToolRegistrar creates a new channel tool registrar. -func NewChannelToolRegistrar(channelService *channels.Service) *ChannelToolRegistrar { +func NewChannelToolRegistrar(channelService *channels.Service, msgService *messaging.MessagingService) *ChannelToolRegistrar { return &ChannelToolRegistrar{ channelService: channelService, + msgService: msgService, } } @@ -30,6 +33,7 @@ func (ctr *ChannelToolRegistrar) RegisterAll(s *server.MCPServer) { s.AddTool(ctr.listChannelsTool(), ctr.handleListChannels) s.AddTool(ctr.inviteToChannelTool(), ctr.handleInviteToChannel) s.AddTool(ctr.kickFromChannelTool(), ctr.handleKickFromChannel) + s.AddTool(ctr.getChannelMessagesTool(), ctr.handleGetChannelMessages) s.AddTool(ctr.sendChannelMessageTool(), ctr.handleSendChannelMessage) s.AddTool(ctr.updateChannelTool(), ctr.handleUpdateChannel) } @@ -87,6 +91,15 @@ func (ctr *ChannelToolRegistrar) kickFromChannelTool() mcp.Tool { ) } +func (ctr *ChannelToolRegistrar) getChannelMessagesTool() mcp.Tool { + return mcp.NewTool("get_channel_messages", + mcp.WithDescription("Get recent messages from a channel you are a member of"), + mcp.WithNumber("channel_id", mcp.Description("ID of the channel")), + mcp.WithString("channel_name", mcp.Description("Name of the channel (alternative to channel_id)")), + mcp.WithNumber("limit", mcp.Description("Max number of messages to return (default 50, max 200)")), + ) +} + func (ctr *ChannelToolRegistrar) sendChannelMessageTool() mcp.Tool { return mcp.NewTool("send_channel_message", mcp.WithDescription("Send a message to all members of a channel"), @@ -281,6 +294,58 @@ func (ctr *ChannelToolRegistrar) handleKickFromChannel(ctx context.Context, req }) } +func (ctr *ChannelToolRegistrar) handleGetChannelMessages(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + agentName, ok := extractAgentName(ctx) + if !ok { + return mcp.NewToolResultError("authentication required"), nil + } + + channelID, err := ctr.resolveChannelID(ctx, req) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("get_channel_messages failed: %s", err)), nil + } + + // Verify the agent is a member of the channel + isMember, err := ctr.channelService.IsMember(ctx, channelID, agentName) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("get_channel_messages failed: %s", err)), nil + } + if !isMember { + return mcp.NewToolResultError("you are not a member of this channel"), nil + } + + limit := req.GetInt("limit", 50) + if limit > 200 { + limit = 200 + } + + messages, err := ctr.msgService.GetChannelMessages(ctx, channelID, limit) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("get_channel_messages failed: %s", err)), nil + } + + result := make([]map[string]any, len(messages)) + for i, msg := range messages { + result[i] = map[string]any{ + "id": msg.ID, + "from": msg.FromAgent, + "body": msg.Body, + "priority": msg.Priority, + "status": msg.Status, + "created_at": msg.CreatedAt, + } + if len(msg.Metadata) > 0 { + result[i]["metadata"] = msg.Metadata + } + } + + return resultJSON(map[string]any{ + "channel_id": channelID, + "messages": result, + "count": len(result), + }) +} + func (ctr *ChannelToolRegistrar) handleSendChannelMessage(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { agentName, ok := extractAgentName(ctx) if !ok { diff --git a/internal/mcp/channel_tools_test.go b/internal/mcp/channel_tools_test.go index 7b1ea8c..0557160 100644 --- a/internal/mcp/channel_tools_test.go +++ b/internal/mcp/channel_tools_test.go @@ -31,7 +31,7 @@ func newTestChannelRegistrar(t *testing.T) (*ChannelToolRegistrar, *channels.Ser db.Exec(`INSERT OR IGNORE INTO agents (name, display_name, type, capabilities, owner_id, api_key_hash, status) VALUES ('agent-b', 'Agent B', 'ai', '{}', 1, 'hash', 'active')`) db.Exec(`INSERT OR IGNORE INTO agents (name, display_name, type, capabilities, owner_id, api_key_hash, status) VALUES ('agent-c', 'Agent C', 'ai', '{}', 1, 'hash', 'active')`) - registrar := NewChannelToolRegistrar(channelService) + registrar := NewChannelToolRegistrar(channelService, msgService) return registrar, channelService } diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 4d072b3..cb785fd 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -146,7 +146,7 @@ func NewMCPServer( // Register channel tools if channelService != nil { - channelRegistrar := NewChannelToolRegistrar(channelService) + channelRegistrar := NewChannelToolRegistrar(channelService, msgService) channelRegistrar.RegisterAll(mcpSrv) } diff --git a/internal/messaging/store.go b/internal/messaging/store.go index 4cb43d4..6a34c1a 100644 --- a/internal/messaging/store.go +++ b/internal/messaging/store.go @@ -332,9 +332,11 @@ func (s *SQLiteMessageStore) SearchMessages(ctx context.Context, agentName, quer var conditions []string var args []any - // Scope to messages accessible by this agent - conditions = append(conditions, "(m.to_agent = ? OR m.from_agent = ?)") - args = append(args, agentName, agentName) + // Scope to messages accessible by this agent: + // - DMs where agent is sender or recipient + // - Channel messages where agent is a member + conditions = append(conditions, "(m.to_agent = ? OR m.from_agent = ? OR (m.channel_id IS NOT NULL AND m.to_agent = '' AND EXISTS (SELECT 1 FROM channel_members cm WHERE cm.channel_id = m.channel_id AND cm.agent_name = ?)))") + args = append(args, agentName, agentName, agentName) var joinClause string var orderClause string