universal-mcp-applications 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. universal_mcp/applications/airtable/app.py +1 -0
  2. universal_mcp/applications/apollo/app.py +1 -0
  3. universal_mcp/applications/{aws-s3 → aws_s3}/app.py +4 -5
  4. universal_mcp/applications/bill/app.py +3 -3
  5. universal_mcp/applications/box/app.py +2 -6
  6. universal_mcp/applications/braze/app.py +2 -6
  7. universal_mcp/applications/cal_com_v2/__init__.py +1 -0
  8. universal_mcp/applications/{cal-com-v2 → cal_com_v2}/app.py +138 -182
  9. universal_mcp/applications/clickup/app.py +2 -2
  10. universal_mcp/applications/confluence/app.py +1 -0
  11. universal_mcp/applications/contentful/app.py +8 -19
  12. universal_mcp/applications/digitalocean/app.py +9 -27
  13. universal_mcp/applications/{domain-checker → domain_checker}/app.py +2 -1
  14. universal_mcp/applications/elevenlabs/app.py +98 -3188
  15. universal_mcp/applications/falai/app.py +1 -0
  16. universal_mcp/applications/file_system/__init__.py +1 -0
  17. universal_mcp/applications/file_system/app.py +96 -0
  18. universal_mcp/applications/fireflies/app.py +4 -3
  19. universal_mcp/applications/fpl/app.py +1 -0
  20. universal_mcp/applications/fpl/utils/fixtures.py +1 -1
  21. universal_mcp/applications/fpl/utils/helper.py +1 -1
  22. universal_mcp/applications/fpl/utils/position_utils.py +0 -1
  23. universal_mcp/applications/{ghost-content → ghost_content}/app.py +2 -1
  24. universal_mcp/applications/github/app.py +4 -3
  25. universal_mcp/applications/{google-calendar → google_calendar}/app.py +2 -1
  26. universal_mcp/applications/{google-docs → google_docs}/app.py +1 -1
  27. universal_mcp/applications/{google-drive → google_drive}/app.py +2 -1
  28. universal_mcp/applications/google_gemini/app.py +183 -0
  29. universal_mcp/applications/{google-mail → google_mail}/app.py +2 -1
  30. universal_mcp/applications/{google-searchconsole → google_searchconsole}/app.py +1 -1
  31. universal_mcp/applications/{google-sheet → google_sheet}/app.py +3 -2
  32. universal_mcp/applications/google_sheet/helper.py +385 -0
  33. universal_mcp/applications/hashnode/app.py +2 -1
  34. universal_mcp/applications/{http-tools → http_tools}/app.py +2 -1
  35. universal_mcp/applications/hubspot/app.py +16 -2
  36. universal_mcp/applications/jira/app.py +7 -18
  37. universal_mcp/applications/markitdown/app.py +2 -3
  38. universal_mcp/applications/{ms-teams → ms_teams}/app.py +1 -1
  39. universal_mcp/applications/openai/app.py +2 -3
  40. universal_mcp/applications/outlook/app.py +1 -3
  41. universal_mcp/applications/pipedrive/app.py +2 -6
  42. universal_mcp/applications/reddit/app.py +1 -0
  43. universal_mcp/applications/replicate/app.py +3 -3
  44. universal_mcp/applications/resend/app.py +1 -2
  45. universal_mcp/applications/rocketlane/app.py +1 -0
  46. universal_mcp/applications/semrush/app.py +478 -1467
  47. universal_mcp/applications/sentry/README.md +20 -20
  48. universal_mcp/applications/sentry/app.py +40 -40
  49. universal_mcp/applications/serpapi/app.py +2 -2
  50. universal_mcp/applications/sharepoint/app.py +2 -1
  51. universal_mcp/applications/shopify/app.py +1 -0
  52. universal_mcp/applications/slack/app.py +3 -3
  53. universal_mcp/applications/trello/app.py +9 -27
  54. universal_mcp/applications/twilio/__init__.py +1 -0
  55. universal_mcp/applications/{twillo → twilio}/app.py +2 -2
  56. universal_mcp/applications/twitter/README.md +1 -1
  57. universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +2 -2
  58. universal_mcp/applications/twitter/api_segments/lists_api.py +1 -1
  59. universal_mcp/applications/unipile/app.py +5 -1
  60. universal_mcp/applications/whatsapp/app.py +18 -17
  61. universal_mcp/applications/whatsapp/audio.py +110 -0
  62. universal_mcp/applications/whatsapp/whatsapp.py +398 -0
  63. universal_mcp/applications/{whatsapp-business → whatsapp_business}/app.py +1 -1
  64. universal_mcp/applications/youtube/app.py +195 -191
  65. universal_mcp/applications/zenquotes/app.py +1 -1
  66. {universal_mcp_applications-0.1.1.dist-info → universal_mcp_applications-0.1.3.dist-info}/METADATA +4 -2
  67. {universal_mcp_applications-0.1.1.dist-info → universal_mcp_applications-0.1.3.dist-info}/RECORD +97 -95
  68. universal_mcp/applications/cal-com-v2/__init__.py +0 -1
  69. universal_mcp/applications/google-ads/__init__.py +0 -1
  70. universal_mcp/applications/google-ads/app.py +0 -23
  71. universal_mcp/applications/google-gemini/app.py +0 -663
  72. universal_mcp/applications/twillo/README.md +0 -0
  73. universal_mcp/applications/twillo/__init__.py +0 -1
  74. /universal_mcp/applications/{aws-s3 → aws_s3}/README.md +0 -0
  75. /universal_mcp/applications/{aws-s3 → aws_s3}/__init__.py +0 -0
  76. /universal_mcp/applications/{cal-com-v2 → cal_com_v2}/README.md +0 -0
  77. /universal_mcp/applications/{domain-checker → domain_checker}/README.md +0 -0
  78. /universal_mcp/applications/{domain-checker → domain_checker}/__init__.py +0 -0
  79. /universal_mcp/applications/{ghost-content → ghost_content}/README.md +0 -0
  80. /universal_mcp/applications/{ghost-content → ghost_content}/__init__.py +0 -0
  81. /universal_mcp/applications/{google-calendar → google_calendar}/README.md +0 -0
  82. /universal_mcp/applications/{google-calendar → google_calendar}/__init__.py +0 -0
  83. /universal_mcp/applications/{google-docs → google_docs}/README.md +0 -0
  84. /universal_mcp/applications/{google-docs → google_docs}/__init__.py +0 -0
  85. /universal_mcp/applications/{google-drive → google_drive}/README.md +0 -0
  86. /universal_mcp/applications/{google-drive → google_drive}/__init__.py +0 -0
  87. /universal_mcp/applications/{google-gemini → google_gemini}/README.md +0 -0
  88. /universal_mcp/applications/{google-gemini → google_gemini}/__init__.py +0 -0
  89. /universal_mcp/applications/{google-mail → google_mail}/README.md +0 -0
  90. /universal_mcp/applications/{google-mail → google_mail}/__init__.py +0 -0
  91. /universal_mcp/applications/{google-searchconsole → google_searchconsole}/README.md +0 -0
  92. /universal_mcp/applications/{google-searchconsole → google_searchconsole}/__init__.py +0 -0
  93. /universal_mcp/applications/{google-sheet → google_sheet}/README.md +0 -0
  94. /universal_mcp/applications/{google-sheet → google_sheet}/__init__.py +0 -0
  95. /universal_mcp/applications/{http-tools → http_tools}/README.md +0 -0
  96. /universal_mcp/applications/{http-tools → http_tools}/__init__.py +0 -0
  97. /universal_mcp/applications/{ms-teams → ms_teams}/README.md +0 -0
  98. /universal_mcp/applications/{ms-teams → ms_teams}/__init__.py +0 -0
  99. /universal_mcp/applications/{google-ads → twilio}/README.md +0 -0
  100. /universal_mcp/applications/{whatsapp-business → whatsapp_business}/README.md +0 -0
  101. /universal_mcp/applications/{whatsapp-business → whatsapp_business}/__init__.py +0 -0
  102. {universal_mcp_applications-0.1.1.dist-info → universal_mcp_applications-0.1.3.dist-info}/WHEEL +0 -0
  103. {universal_mcp_applications-0.1.1.dist-info → universal_mcp_applications-0.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -3,6 +3,7 @@ from collections.abc import Callable
3
3
  from typing import Any, Literal
4
4
 
5
5
  from loguru import logger
6
+
6
7
  from universal_mcp.applications.application import APIApplication
7
8
  from universal_mcp.integrations import Integration
8
9
 
@@ -666,7 +667,10 @@ class UnipileApp(APIApplication):
666
667
  keywords: str | None = None,
667
668
  sort_by: Literal["relevance", "date"] | None = None,
668
669
  date_posted: Literal["past_day", "past_week", "past_month"] | None = None,
669
- content_type: Literal["videos", "images", "live_videos", "collaborative_articles", "documents"] | None = None,
670
+ content_type: Literal[
671
+ "videos", "images", "live_videos", "collaborative_articles", "documents"
672
+ ]
673
+ | None = None,
670
674
  posted_by: dict[str, Any] | None = None,
671
675
  mentioning: dict[str, Any] | None = None,
672
676
  author: dict[str, Any] | None = None,
@@ -1,56 +1,57 @@
1
1
  from typing import Any
2
2
 
3
3
  import requests
4
- from universal_mcp.applications import BaseApplication
5
- from universal_mcp.exceptions import NotAuthorizedError
6
- from universal_mcp.integrations import AgentRIntegration
7
- from universal_mcp_whatsapp.whatsapp import (
4
+ from .whatsapp import (
8
5
  WHATSAPP_API_BASE_URL,
9
6
  )
10
- from universal_mcp_whatsapp.whatsapp import (
7
+ from .whatsapp import (
11
8
  download_media as whatsapp_download_media,
12
9
  )
13
- from universal_mcp_whatsapp.whatsapp import (
10
+ from .whatsapp import (
14
11
  get_chat as whatsapp_get_chat,
15
12
  )
16
- from universal_mcp_whatsapp.whatsapp import (
13
+ from .whatsapp import (
17
14
  get_contact_chats as whatsapp_get_contact_chats,
18
15
  )
19
- from universal_mcp_whatsapp.whatsapp import (
16
+ from .whatsapp import (
20
17
  get_direct_chat_by_contact as whatsapp_get_direct_chat_by_contact,
21
18
  )
22
- from universal_mcp_whatsapp.whatsapp import (
19
+ from .whatsapp import (
23
20
  get_last_interaction as whatsapp_get_last_interaction,
24
21
  )
25
- from universal_mcp_whatsapp.whatsapp import (
22
+ from .whatsapp import (
26
23
  get_message_context as whatsapp_get_message_context,
27
24
  )
28
- from universal_mcp_whatsapp.whatsapp import (
25
+ from .whatsapp import (
29
26
  list_chats as whatsapp_list_chats,
30
27
  )
31
- from universal_mcp_whatsapp.whatsapp import (
28
+ from .whatsapp import (
32
29
  list_messages as whatsapp_list_messages,
33
30
  )
34
- from universal_mcp_whatsapp.whatsapp import (
31
+ from .whatsapp import (
35
32
  search_contacts as whatsapp_search_contacts,
36
33
  )
37
- from universal_mcp_whatsapp.whatsapp import (
34
+ from .whatsapp import (
38
35
  send_audio_message as whatsapp_audio_voice_message,
39
36
  )
40
- from universal_mcp_whatsapp.whatsapp import (
37
+ from .whatsapp import (
41
38
  send_file as whatsapp_send_file,
42
39
  )
43
- from universal_mcp_whatsapp.whatsapp import (
40
+ from .whatsapp import (
44
41
  send_message as whatsapp_send_message,
45
42
  )
46
43
 
44
+ from universal_mcp.applications.application import BaseApplication
45
+ from universal_mcp.exceptions import NotAuthorizedError
46
+ from universal_mcp.agentr.integration import AgentrIntegration
47
+
47
48
 
48
49
  class WhatsappApp(BaseApplication):
49
50
  """
50
51
  Base class for Universal MCP Applications.
51
52
  """
52
53
 
53
- def __init__(self, integration: AgentRIntegration | None = None, **kwargs) -> None:
54
+ def __init__(self, integration: AgentrIntegration | None = None, **kwargs) -> None:
54
55
  super().__init__(name="whatsapp", integration=integration, **kwargs)
55
56
  self.base_url = WHATSAPP_API_BASE_URL
56
57
  self._api_key: str = integration.client.api_key if integration else None
@@ -0,0 +1,110 @@
1
+ import os
2
+ import subprocess
3
+ import tempfile
4
+
5
+ def convert_to_opus_ogg(input_file, output_file=None, bitrate="32k", sample_rate=24000):
6
+ """
7
+ Convert an audio file to Opus format in an Ogg container.
8
+
9
+ Args:
10
+ input_file (str): Path to the input audio file
11
+ output_file (str, optional): Path to save the output file. If None, replaces the
12
+ extension of input_file with .ogg
13
+ bitrate (str, optional): Target bitrate for Opus encoding (default: "32k")
14
+ sample_rate (int, optional): Sample rate for output (default: 24000)
15
+
16
+ Returns:
17
+ str: Path to the converted file
18
+
19
+ Raises:
20
+ FileNotFoundError: If the input file doesn't exist
21
+ RuntimeError: If the ffmpeg conversion fails
22
+ """
23
+ if not os.path.isfile(input_file):
24
+ raise FileNotFoundError(f"Input file not found: {input_file}")
25
+
26
+ # If no output file is specified, replace the extension with .ogg
27
+ if output_file is None:
28
+ output_file = os.path.splitext(input_file)[0] + ".ogg"
29
+
30
+ # Ensure the output directory exists
31
+ output_dir = os.path.dirname(output_file)
32
+ if output_dir and not os.path.exists(output_dir):
33
+ os.makedirs(output_dir)
34
+
35
+ # Build the ffmpeg command
36
+ cmd = [
37
+ "ffmpeg",
38
+ "-i", input_file,
39
+ "-c:a", "libopus",
40
+ "-b:a", bitrate,
41
+ "-ar", str(sample_rate),
42
+ "-application", "voip", # Optimize for voice
43
+ "-vbr", "on", # Variable bitrate
44
+ "-compression_level", "10", # Maximum compression
45
+ "-frame_duration", "60", # 60ms frames (good for voice)
46
+ "-y", # Overwrite output file if it exists
47
+ output_file
48
+ ]
49
+
50
+ try:
51
+ # Run the ffmpeg command and capture output
52
+ process = subprocess.run(
53
+ cmd,
54
+ stdout=subprocess.PIPE,
55
+ stderr=subprocess.PIPE,
56
+ text=True,
57
+ check=True
58
+ )
59
+ return output_file
60
+ except subprocess.CalledProcessError as e:
61
+ raise RuntimeError(f"Failed to convert audio. You likely need to install ffmpeg {e.stderr}")
62
+
63
+
64
+ def convert_to_opus_ogg_temp(input_file, bitrate="32k", sample_rate=24000):
65
+ """
66
+ Convert an audio file to Opus format in an Ogg container and store in a temporary file.
67
+
68
+ Args:
69
+ input_file (str): Path to the input audio file
70
+ bitrate (str, optional): Target bitrate for Opus encoding (default: "32k")
71
+ sample_rate (int, optional): Sample rate for output (default: 24000)
72
+
73
+ Returns:
74
+ str: Path to the temporary file with the converted audio
75
+
76
+ Raises:
77
+ FileNotFoundError: If the input file doesn't exist
78
+ RuntimeError: If the ffmpeg conversion fails
79
+ """
80
+ # Create a temporary file with .ogg extension
81
+ temp_file = tempfile.NamedTemporaryFile(suffix=".ogg", delete=False)
82
+ temp_file.close()
83
+
84
+ try:
85
+ # Convert the audio
86
+ convert_to_opus_ogg(input_file, temp_file.name, bitrate, sample_rate)
87
+ return temp_file.name
88
+ except Exception as e:
89
+ # Clean up the temporary file if conversion fails
90
+ if os.path.exists(temp_file.name):
91
+ os.unlink(temp_file.name)
92
+ raise e
93
+
94
+
95
+ if __name__ == "__main__":
96
+ # Example usage
97
+ import sys
98
+
99
+ if len(sys.argv) < 2:
100
+ print("Usage: python audio.py input_file [output_file]")
101
+ sys.exit(1)
102
+
103
+ input_file = sys.argv[1]
104
+
105
+ try:
106
+ result = convert_to_opus_ogg_temp(input_file)
107
+ print(f"Successfully converted to: {result}")
108
+ except Exception as e:
109
+ print(f"Error: {e}")
110
+ sys.exit(1)
@@ -0,0 +1,398 @@
1
+ import requests
2
+ import json
3
+ from datetime import datetime
4
+ from dataclasses import dataclass
5
+ from typing import Optional, List, Tuple
6
+ import os.path
7
+ from . import audio
8
+ from dotenv import load_dotenv
9
+
10
+ load_dotenv()
11
+
12
+ WHATSAPP_API_BASE_URL = os.getenv('WHATSAPP_API_BASE_URL', "http://134.209.144.43:8080")
13
+
14
+ @dataclass
15
+ class Message:
16
+ timestamp: datetime
17
+ sender: str
18
+ content: str
19
+ is_from_me: bool
20
+ chat_jid: str
21
+ id: str
22
+ chat_name: Optional[str] = None
23
+ media_type: Optional[str] = None
24
+
25
+ @dataclass
26
+ class Chat:
27
+ jid: str
28
+ name: Optional[str]
29
+ last_message_time: Optional[datetime]
30
+ last_message: Optional[str] = None
31
+ last_sender: Optional[str] = None
32
+ last_is_from_me: Optional[bool] = None
33
+
34
+ @property
35
+ def is_group(self) -> bool:
36
+ """Determine if chat is a group based on JID pattern."""
37
+ return self.jid.endswith("@g.us")
38
+
39
+ @dataclass
40
+ class Contact:
41
+ phone_number: str
42
+ name: Optional[str]
43
+ jid: str
44
+
45
+ @dataclass
46
+ class MessageContext:
47
+ message: Message
48
+ before: List[Message]
49
+ after: List[Message]
50
+
51
+ def _make_api_request(endpoint: str, method: str = "GET", data: dict = None, user_id: str = "default_user") -> dict:
52
+ """Make HTTP request to WhatsApp Bridge API."""
53
+ url = f"{WHATSAPP_API_BASE_URL}/api/{endpoint}"
54
+
55
+ # Add user_id to query parameters for GET requests
56
+ if method.upper() == "GET" and data:
57
+ data["user_id"] = user_id
58
+ elif method.upper() == "GET":
59
+ data = {"user_id": user_id}
60
+
61
+ try:
62
+ if method.upper() == "GET":
63
+ response = requests.get(url, params=data, timeout=30)
64
+ elif method.upper() == "POST":
65
+ response = requests.post(url, json=data, timeout=30)
66
+ else:
67
+ raise ValueError(f"Unsupported HTTP method: {method}")
68
+
69
+ if response.status_code == 200:
70
+ return response.json()
71
+ else:
72
+ return {"error": f"HTTP {response.status_code}: {response.text}"}
73
+
74
+ except requests.RequestException as e:
75
+ return {"error": f"Request failed: {str(e)}"}
76
+ except json.JSONDecodeError:
77
+ return {"error": f"Invalid JSON response: {response.text}"}
78
+
79
+ def get_sender_name(sender_jid: str, user_id: str = "default_user") -> str:
80
+ """Get sender name via API call."""
81
+ result = _make_api_request("sender_name", data={"sender_jid": sender_jid}, user_id=user_id)
82
+
83
+ if "error" in result:
84
+ return sender_jid
85
+
86
+ return result.get("name", sender_jid)
87
+
88
+ def format_message(message: Message, show_chat_info: bool = True, user_id: str = "default_user") -> str:
89
+ """Print a single message with consistent formatting."""
90
+ output = ""
91
+
92
+ if show_chat_info and message.chat_name:
93
+ output += f"[{message.timestamp:%Y-%m-%d %H:%M:%S}] Chat: {message.chat_name} "
94
+ else:
95
+ output += f"[{message.timestamp:%Y-%m-%d %H:%M:%S}] "
96
+
97
+ content_prefix = ""
98
+ if hasattr(message, 'media_type') and message.media_type:
99
+ content_prefix = f"[{message.media_type} - Message ID: {message.id} - Chat JID: {message.chat_jid}] "
100
+
101
+ try:
102
+ sender_name = get_sender_name(message.sender, user_id) if not message.is_from_me else "Me"
103
+ output += f"From: {sender_name}: {content_prefix}{message.content}\n"
104
+ except Exception as e:
105
+ print(f"Error formatting message: {e}")
106
+ return output
107
+
108
+ def format_messages_list(messages: List[Message], show_chat_info: bool = True, user_id: str = "default_user") -> str:
109
+ output = ""
110
+ if not messages:
111
+ output += "No messages to display."
112
+ return output
113
+
114
+ for message in messages:
115
+ output += format_message(message, show_chat_info, user_id)
116
+ return output
117
+
118
+ def list_messages(
119
+ after: Optional[str] = None,
120
+ before: Optional[str] = None,
121
+ sender_phone_number: Optional[str] = None,
122
+ chat_jid: Optional[str] = None,
123
+ query: Optional[str] = None,
124
+ limit: int = 20,
125
+ page: int = 0,
126
+ include_context: bool = True,
127
+ context_before: int = 1,
128
+ context_after: int = 1,
129
+ user_id: str = "default_user"
130
+ ) -> str:
131
+ """Get messages matching the specified criteria with optional context via API."""
132
+ params = {
133
+ "after": after,
134
+ "before": before,
135
+ "sender_phone_number": sender_phone_number,
136
+ "chat_jid": chat_jid,
137
+ "query": query,
138
+ "limit": limit,
139
+ "page": page,
140
+ "include_context": include_context,
141
+ "context_before": context_before,
142
+ "context_after": context_after
143
+ }
144
+
145
+ # Remove None values
146
+ params = {k: v for k, v in params.items() if v is not None}
147
+
148
+ result = _make_api_request("messages", data=params, user_id=user_id)
149
+
150
+ if "error" in result:
151
+ return f"Error retrieving messages: {result['error']}"
152
+
153
+ return result.get("messages", "No messages found")
154
+
155
+ def get_message_context(
156
+ message_id: str,
157
+ before: int = 5,
158
+ after: int = 5,
159
+ user_id: str = "default_user"
160
+ ) -> MessageContext:
161
+ """Get context around a specific message via API."""
162
+ params = {
163
+ "message_id": message_id,
164
+ "before": before,
165
+ "after": after
166
+ }
167
+
168
+ result = _make_api_request("message_context", data=params, user_id=user_id)
169
+
170
+ if "error" in result:
171
+ raise ValueError(f"Error getting message context: {result['error']}")
172
+
173
+ # Parse the response into MessageContext object
174
+ # This would need to be implemented based on the API response structure
175
+ # For now, returning a simple error message
176
+ raise NotImplementedError("Message context API not yet implemented in bridge")
177
+
178
+ def list_chats(
179
+ query: Optional[str] = None,
180
+ limit: int = 20,
181
+ page: int = 0,
182
+ include_last_message: bool = True,
183
+ sort_by: str = "last_active",
184
+ user_id: str = "default_user"
185
+ ) -> List[Chat]:
186
+ """Get chats matching the specified criteria via API."""
187
+ params = {
188
+ "query": query,
189
+ "limit": limit,
190
+ "page": page,
191
+ "include_last_message": include_last_message,
192
+ "sort_by": sort_by
193
+ }
194
+
195
+ # Remove None values
196
+ params = {k: v for k, v in params.items() if v is not None}
197
+
198
+ result = _make_api_request("chats", data=params, user_id=user_id)
199
+
200
+ if "error" in result:
201
+ print(f"Error retrieving chats: {result['error']}")
202
+ return []
203
+
204
+ chats_data = result.get("chats", [])
205
+ chats = []
206
+
207
+ for chat_data in chats_data:
208
+ chat = Chat(
209
+ jid=chat_data["jid"],
210
+ name=chat_data.get("name"),
211
+ last_message_time=datetime.fromisoformat(chat_data["last_message_time"]) if chat_data.get("last_message_time") else None,
212
+ last_message=chat_data.get("last_message"),
213
+ last_sender=chat_data.get("last_sender"),
214
+ last_is_from_me=chat_data.get("last_is_from_me")
215
+ )
216
+ chats.append(chat)
217
+
218
+ return chats
219
+
220
+ def search_contacts(query: str, user_id: str = "default_user") -> List[Contact]:
221
+ """Search contacts by name or phone number via API."""
222
+ result = _make_api_request("contacts", data={"query": query}, user_id=user_id)
223
+
224
+ if "error" in result:
225
+ print(f"Error searching contacts: {result['error']}")
226
+ return []
227
+
228
+ contacts_data = result.get("contacts", [])
229
+ contacts = []
230
+
231
+ for contact_data in contacts_data:
232
+ contact = Contact(
233
+ phone_number=contact_data["phone_number"],
234
+ name=contact_data.get("name"),
235
+ jid=contact_data["jid"]
236
+ )
237
+ contacts.append(contact)
238
+
239
+ return contacts
240
+
241
+ def get_contact_chats(jid: str, limit: int = 20, page: int = 0, user_id: str = "default_user") -> List[Chat]:
242
+ """Get all chats involving the contact via API."""
243
+ params = {
244
+ "jid": jid,
245
+ "limit": limit,
246
+ "page": page
247
+ }
248
+
249
+ result = _make_api_request("contact_chats", data=params, user_id=user_id)
250
+
251
+ if "error" in result:
252
+ print(f"Error getting contact chats: {result['error']}")
253
+ return []
254
+
255
+ chats_data = result.get("chats", [])
256
+ chats = []
257
+
258
+ for chat_data in chats_data:
259
+ chat = Chat(
260
+ jid=chat_data["jid"],
261
+ name=chat_data.get("name"),
262
+ last_message_time=datetime.fromisoformat(chat_data["last_message_time"]) if chat_data.get("last_message_time") else None,
263
+ last_message=chat_data.get("last_message"),
264
+ last_sender=chat_data.get("last_sender"),
265
+ last_is_from_me=chat_data.get("last_is_from_me")
266
+ )
267
+ chats.append(chat)
268
+
269
+ return chats
270
+
271
+ def get_last_interaction(jid: str, user_id: str = "default_user") -> str:
272
+ """Get most recent message involving the contact via API."""
273
+ result = _make_api_request("last_interaction", data={"jid": jid}, user_id=user_id)
274
+
275
+ if "error" in result:
276
+ return f"Error getting last interaction: {result['error']}"
277
+
278
+ return result.get("message", "No interaction found")
279
+
280
+ def get_chat(chat_jid: str, include_last_message: bool = True, user_id: str = "default_user") -> Optional[Chat]:
281
+ """Get chat metadata by JID via API."""
282
+ params = {
283
+ "chat_jid": chat_jid,
284
+ "include_last_message": include_last_message
285
+ }
286
+
287
+ result = _make_api_request("chat", data=params, user_id=user_id)
288
+
289
+ if "error" in result:
290
+ print(f"Error getting chat: {result['error']}")
291
+ return None
292
+
293
+ chat_data = result.get("chat")
294
+ if not chat_data:
295
+ return None
296
+
297
+ return Chat(
298
+ jid=chat_data["jid"],
299
+ name=chat_data.get("name"),
300
+ last_message_time=datetime.fromisoformat(chat_data["last_message_time"]) if chat_data.get("last_message_time") else None,
301
+ last_message=chat_data.get("last_message"),
302
+ last_sender=chat_data.get("last_sender"),
303
+ last_is_from_me=chat_data.get("last_is_from_me")
304
+ )
305
+
306
+ def get_direct_chat_by_contact(sender_phone_number: str, user_id: str = "default_user") -> Optional[Chat]:
307
+ """Get chat metadata by sender phone number via API."""
308
+ result = _make_api_request("direct_chat", data={"sender_phone_number": sender_phone_number}, user_id=user_id)
309
+
310
+ if "error" in result:
311
+ print(f"Error getting direct chat: {result['error']}")
312
+ return None
313
+
314
+ chat_data = result.get("chat")
315
+ if not chat_data:
316
+ return None
317
+
318
+ return Chat(
319
+ jid=chat_data["jid"],
320
+ name=chat_data.get("name"),
321
+ last_message_time=datetime.fromisoformat(chat_data["last_message_time"]) if chat_data.get("last_message_time") else None,
322
+ last_message=chat_data.get("last_message"),
323
+ last_sender=chat_data.get("last_sender"),
324
+ last_is_from_me=chat_data.get("last_is_from_me")
325
+ )
326
+
327
+ def send_message(recipient: str, message: str, user_id: str = "default_user") -> Tuple[bool, str]:
328
+ """Send message via API."""
329
+ payload = {
330
+ "user_id": user_id,
331
+ "recipient": recipient,
332
+ "message": message
333
+ }
334
+
335
+ result = _make_api_request("send", method="POST", data=payload, user_id=user_id)
336
+
337
+ if "error" in result:
338
+ return False, result["error"]
339
+
340
+ return result.get("success", False), result.get("message", "Unknown response")
341
+
342
+ def send_file(recipient: str, media_path: str, user_id: str = "default_user") -> Tuple[bool, str]:
343
+ """Send file via API."""
344
+ payload = {
345
+ "user_id": user_id,
346
+ "recipient": recipient,
347
+ "media_path": media_path
348
+ }
349
+
350
+ result = _make_api_request("send", method="POST", data=payload, user_id=user_id)
351
+
352
+ if "error" in result:
353
+ return False, result["error"]
354
+
355
+ return result.get("success", False), result.get("message", "Unknown response")
356
+
357
+ def send_audio_message(recipient: str, media_path: str, user_id: str = "default_user") -> Tuple[bool, str]:
358
+ """Send audio message via API."""
359
+ if not media_path.endswith(".ogg"):
360
+ try:
361
+ media_path = audio.convert_to_opus_ogg_temp(media_path)
362
+ except Exception as e:
363
+ return False, f"Error converting file to opus ogg. You likely need to install ffmpeg: {str(e)}"
364
+
365
+ payload = {
366
+ "user_id": user_id,
367
+ "recipient": recipient,
368
+ "media_path": media_path
369
+ }
370
+
371
+ result = _make_api_request("send", method="POST", data=payload, user_id=user_id)
372
+
373
+ if "error" in result:
374
+ return False, result["error"]
375
+
376
+ return result.get("success", False), result.get("message", "Unknown response")
377
+
378
+ def download_media(message_id: str, chat_jid: str, user_id: str = "default_user") -> Optional[str]:
379
+ """Download media from a message via API."""
380
+ payload = {
381
+ "user_id": user_id,
382
+ "message_id": message_id,
383
+ "chat_jid": chat_jid
384
+ }
385
+
386
+ result = _make_api_request("download", method="POST", data=payload, user_id=user_id)
387
+
388
+ if "error" in result:
389
+ print(f"Download failed: {result['error']}")
390
+ return None
391
+
392
+ if result.get("success", False):
393
+ path = result.get("path")
394
+ print(f"Media downloaded successfully: {path}")
395
+ return path
396
+ else:
397
+ print(f"Download failed: {result.get('message', 'Unknown error')}")
398
+ return None
@@ -6,7 +6,7 @@ from universal_mcp.integrations import Integration
6
6
 
7
7
  class WhatsappBusinessApp(APIApplication):
8
8
  def __init__(self, integration: Integration = None, **kwargs) -> None:
9
- super().__init__(name="whatsapp-business", integration=integration, **kwargs)
9
+ super().__init__(name="whatsapp_business", integration=integration, **kwargs)
10
10
  self.base_url = "https://graph.facebook.com"
11
11
 
12
12
  def get_analytics(