package tools import ( "context" "strconv" "strings" "time" "github.com/modelcontextprotocol/go-sdk/mcp" "isphere-ai-bridge/internal/isphere" ) const ToolNameReceiveMessages = "isphere_receive_messages" type ReceiveMessagesSource interface { ReceiveMessages(ctx context.Context, query isphere.ReceiveMessagesQuery) (isphere.ReceiveMessagesResult, error) } type ReceiveMessagesArgs struct { Limit int `json:"limit,omitempty" jsonschema:"maximum number of messages to return; zero or negative returns all available messages"` } func RegisterISphereReadTools(server *mcp.Server, source ReceiveMessagesSource) { if source == nil { source = isphere.EncryptedPacketLogSource{} } mcp.AddTool[ReceiveMessagesArgs, map[string]any](server, &mcp.Tool{ Name: ToolNameReceiveMessages, Description: "Return iSphere messages from the configured read-only log-backed source.", }, func(ctx context.Context, _ *mcp.CallToolRequest, input ReceiveMessagesArgs) (*mcp.CallToolResult, map[string]any, error) { started := time.Now().UTC() result, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{Limit: input.Limit}) if err != nil { return nil, nil, err } return nil, receiveMessagesResultToMap(result, started, time.Now().UTC()), nil }) } func receiveMessagesResultToMap(result isphere.ReceiveMessagesResult, started time.Time, finished time.Time) map[string]any { messages := make([]map[string]any, 0, len(result.Messages)) for _, message := range result.Messages { attachments := receiveMessageAttachments(message) messages = append(messages, map[string]any{ "id": message.ID, "message_id": message.ID, "conversation_id": message.ConversationID, "conversation_type": message.ConversationType, "sender_id": message.SenderID, "sender_name": nil, "receiver_id": message.ReceiverID, "text": message.Text, "content_type": receiveMessageContentType(message, attachments), "content_text": message.Text, "attachments": attachments, "timestamp": message.Timestamp, "created_at": packetTimestampToRFC3339(message.Timestamp), "received_at": nil, "subject": message.Subject, "receipt_id": message.ReceiptID, "read": message.Read, "source": "local_readonly", "raw_ref": "packetlog_message", }) } return map[string]any{ "ok": true, "conversation": receiveMessagesConversation(result.Messages), "messages": messages, "next_cursor": nil, "audit": map[string]any{ "tool": ToolNameReceiveMessages, "source": "local_readonly", "execution_mode": "read", "started_at": started.Format(time.RFC3339Nano), "finished_at": finished.Format(time.RFC3339Nano), "result": "ok", }, } } func receiveMessagesConversation(messages []isphere.Message) any { if len(messages) == 0 { return nil } first := messages[0] return map[string]any{ "conversation_id": first.ConversationID, "conversation_type": contractConversationType(first.ConversationType), "display_name": first.ConversationID, } } func contractConversationType(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "groupchat", "group": return "group" case "chat", "direct": return "direct" case "system": return "system" default: return "unknown" } } func receiveMessageAttachments(message isphere.Message) []map[string]any { files := isphere.ListFilesFromMessages([]isphere.Message{message}, isphere.ListFilesQuery{}) attachments := make([]map[string]any, 0, len(files.Files)) for _, file := range files.Files { attachments = append(attachments, map[string]any{ "file_id": file.FileID, "file_name": file.FileName, "size_bytes": file.SizeBytes, "download_ref": nullableString(file.DownloadRef), }) } return attachments } func receiveMessageContentType(message isphere.Message, attachments []map[string]any) string { if len(attachments) > 0 { return "file" } if strings.TrimSpace(message.Text) != "" { return "text" } if strings.TrimSpace(message.Subject) != "" { return "notify" } return "unknown" } func packetTimestampToRFC3339(value string) any { trimmed := strings.TrimSpace(value) if trimmed == "" { return nil } epoch, err := strconv.ParseInt(trimmed, 10, 64) if err != nil { return nil } var ts time.Time if epoch > 1_000_000_000_000 { ts = time.UnixMilli(epoch) } else { ts = time.Unix(epoch, 0) } formatted := ts.UTC().Format(time.RFC3339) return formatted }