Skip to main content
Glama

signal-mcp-server

server.py•9.57 kB
from dataclasses import asdict from pathlib import Path from datetime import datetime import sigexport.data import sigexport from typing import Optional, List, Dict, Any import platform from mcp.server.fastmcp import FastMCP mcp = FastMCP("Signal MCP Server", "0.1.0") def get_default_signal_dir() -> Path: home = Path.home() system = platform.system() if system == "Windows": return home / "AppData" / "Roaming" / "Signal" elif system == "Darwin": return home / "Library" / "Application Support" / "Signal" elif system == "Linux": # Check for Flatpak first flatpak_path = home / ".var" / "app" / "org.signal.Signal" / "config" / "Signal" if flatpak_path.exists(): return flatpak_path return home / ".config" / "Signal" else: raise RuntimeError(f"Unsupported OS: {system}") @mcp.tool() def signal_list_chats( source_dir: Path = get_default_signal_dir(), password: Optional[str] = None, key: Optional[str] = None, chats: str = "", include_empty: bool = False, include_disappearing: bool = True, ) -> List[Dict[str, Any]]: """ List all Signal chats with their details. Args: source_dir (Path): Path to the Signal data directory. password (Optional[str]): Password for encrypted data, if applicable. key (Optional[str]): Key for encrypted data, if applicable. chats (str): Comma-separated list of chat IDs to filter. include_empty (bool): Whether to include empty chats. include_disappearing (bool): Whether to include disappearing messages. Returns: List[Dict[str, Any]]: A list of dictionaries containing Signal chat details. """ convos, contacts, self_contact = sigexport.data.fetch_data( source_dir=source_dir, password=password, key=key, chats=chats, include_empty=include_empty, include_disappearing=include_disappearing, ) output = [] for chat_id in convos: contact = contacts.get(chat_id) if contact: contact_dict = asdict(contact) contact_dict["ServiceId"] = contact_dict.pop("serviceId") contact_dict["total_messages"] = len(convos[chat_id]) output.append(contact_dict) return output @mcp.tool() def signal_get_chat_messages( chat_name: str, limit: Optional[int] = None, offset: int = 0, # Add offset for pagination source_dir: Path = get_default_signal_dir(), password: Optional[str] = None, key: Optional[str] = None, chats: str = "", include_empty: bool = False, include_disappearing: bool = True, ) -> List[Dict[str, Any]]: """ Get Signal messages from a specific chat by name. Args: chat_name (str): The name of the chat to retrieve messages from. limit (Optional[int]): Maximum number of messages to return. offset (int): Number of messages to skip before starting to collect results. source_dir (Path): Path to the Signal data directory. password (Optional[str]): Password for encrypted data, if applicable. key (Optional[str]): Key for encrypted data, if applicable. chats (str): Comma-separated list of chat IDs to filter. include_empty (bool): Whether to include empty chats. include_disappearing (bool): Whether to include disappearing messages. Returns: List[Dict[str, Any]]: A list of dictionaries containing messages from the specified Signal chat. """ convos, contacts, self_contact = sigexport.data.fetch_data( source_dir=source_dir, password=password, key=key, chats=chats, include_empty=include_empty, include_disappearing=include_disappearing, ) chat_messages = [] for chat_id, messages in convos.items(): contact = contacts.get(chat_id) if contact and contact.name == chat_name: # Sort messages by timestamp (newest first) sorted_messages = sorted(messages, key=lambda m: m.get_ts(), reverse=True) # Apply offset and limit start_idx = offset end_idx = offset + limit if limit else len(sorted_messages) paginated_messages = sorted_messages[start_idx:end_idx] for msg in paginated_messages: msg_dict = asdict(msg) # ... rest of your formatting code date = msg_dict.get("sent_at") or msg_dict.get("timestamp") if isinstance(date, (int, float)): date = datetime.fromtimestamp(date / 1000) elif isinstance(date, str): date = datetime.fromisoformat(date) sender = ( "Me" if msg_dict.get("source") == self_contact.serviceId else (contact.name or contact.number or "Unknown") ) chat_messages.append( { "date": date.isoformat() if isinstance(date, datetime) else "", "sender": sender, "body": msg_dict.get("body", ""), "quote": msg_dict.get("quote", "") or "", "sticker": msg_dict.get("sticker", "") or "", "reactions": msg_dict.get("reactions", []) or [], "attachments": msg_dict.get("has_attachments", "") or "", } ) break return chat_messages @mcp.tool() def signal_search_chat( chat_name: str, query: str, limit: Optional[int] = None, source_dir: Path = get_default_signal_dir(), password: Optional[str] = None, key: Optional[str] = None, chats: str = "", include_empty: bool = False, include_disappearing: bool = True, ) -> List[Dict[str, Any]]: """ Search for specific text within a Signal chat. Args: chat_name (str): The name of the chat to search within. query (str): The text to search for in messages. limit (Optional[int]): Maximum number of matching messages to return. source_dir (Path): Path to the Signal data directory. password (Optional[str]): Password for encrypted data, if applicable. key (Optional[str]): Key for encrypted data, if applicable. chats (str): Comma-separated list of chat IDs to filter. include_empty (bool): Whether to include empty chats. include_disappearing (bool): Whether to include disappearing messages. Returns: List[Dict[str, Any]]: A list of messages that match the search query. """ convos, contacts, self_contact = sigexport.data.fetch_data( source_dir=source_dir, password=password, key=key, chats=chats, include_empty=include_empty, include_disappearing=include_disappearing, ) matched_messages = [] for chat_id, messages in convos.items(): contact = contacts.get(chat_id) if contact and contact.name == chat_name: # Sort messages by timestamp (newest first) sorted_messages = sorted(messages, key=lambda m: m.get_ts(), reverse=True) for msg in sorted_messages: msg_dict = asdict(msg) body = msg_dict.get("body", "") or "" # Search in message body if query.lower() in body.lower(): date = msg_dict.get("sent_at") or msg_dict.get("timestamp") if isinstance(date, (int, float)): date = datetime.fromtimestamp(date / 1000) elif isinstance(date, str): date = datetime.fromisoformat(date) sender = ( "Me" if msg_dict.get("source") == self_contact.serviceId else (contact.name or contact.number or "Unknown") ) matched_messages.append( { "date": ( date.isoformat() if isinstance(date, datetime) else "" ), "sender": sender, "body": body, "quote": msg_dict.get("quote", "") or "", "sticker": msg_dict.get("sticker", "") or "", "reactions": msg_dict.get("reactions", []) or [], "attachments": msg_dict.get("has_attachments", "") or "", } ) # Apply limit if specified if limit and len(matched_messages) >= limit: break break return matched_messages @mcp.prompt() def signal_summarize_chat_prompt(chat_name: str) -> str: return f"Summarize the recent messages in the Signal chat named '{chat_name}'." @mcp.prompt() def signal_chat_topic_prompt(chat_name: str) -> str: return f"What are the topics of discussion in the Signal chat named '{chat_name}'?" @mcp.prompt() def signal_chat_sentiment_prompt(chat_name: str) -> str: return f"Analyze the sentiment of messages in the Signal chat named '{chat_name}'." @mcp.prompt() def signal_search_chat_prompt(chat_name: str, query: str) -> str: return f"Search for the text '{query}' in the Signal chat named '{chat_name}'." if __name__ == "__main__": mcp.run()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/stefanstranger/signal-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server