High risk. Don't ship without significant remediation.
Scanned 5/12/2026, 7:16:17 PMยทCached resultยทDeep Scanยท91 rulesยทHow we decide โ
AIVSS Score
High
Severity Breakdown
0
critical
13
high
12
medium
0
low
MCP Server Information
Findings
This package has a poor security grade (D) and a low safety score (67/100), indicating significant risks. It contains 13 high-severity and 12 medium-severity issues, including prompt injection vulnerabilities (10), tool poisoning risks (3), and potential resource exhaustion (4). While no critical flaws were found, the volume of high-risk findings suggests it may expose your system to manipulation or instability.
AIPer-finding remediation generated by bedrock-claude-haiku-4-5 โ 25 of 25 findings. Click any finding to read.
No known CVEs found for this package or its dependencies.
Scan Details
Done
Sign in to save scan history and re-scan automatically on new commits.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasnโt shipped yet.
25 of 25 findings
25 findings
Tool list_chats accepts optional query parameter and returns chats without verifying caller ownership or authorization to access those chats.
Evidence
| 67 | context_before=context_before, |
| 68 | context_after=context_after |
| 69 | ) |
| 70 | return messages |
| 71 | |
| 72 | @mcp.tool() |
| 73 | def list_chats( |
| 74 | query: Optional[str] = None, |
| 75 | limit: int = 20, |
| 76 | page: int = 0, |
| 77 | include_last_message: bool = True, |
| 78 | sort_by: str = "last_active" |
| 79 | ) -> List[Dict[str, Any]]: |
| 80 | """Get WhatsApp chats matching specified criteria. |
| 81 | |
| 82 | Args: |
| 83 | query: Optional search term to filter chats by name or JID |
| 84 | limit: Maximum number of chats to return (default 20) |
| 85 | |
RemediationAI
The list_chats tool returns all chats matching a query without verifying the caller has authorization to access those chats. Add an authorization check function that validates the caller's identity against a whitelist or permission store before querying the database; call this function at the start of list_chats() before invoking whatsapp_list_chats(). This ensures only authorized callers can retrieve chat data. Verify the fix by testing with an unauthorized caller and confirming the tool returns an error instead of chat data.
LLM consensus
Tool list_messages accepts chat_jid and sender_phone_number identifiers and returns messages filtered only by these identifiers without verifying caller ownership or authorization of the chat/sender data.
Evidence
| 44 | """Get WhatsApp messages matching specified criteria with optional context. |
| 45 | |
| 46 | Args: |
| 47 | after: Optional ISO-8601 formatted string to only return messages after this date |
| 48 | before: Optional ISO-8601 formatted string to only return messages before this date |
| 49 | sender_phone_number: Optional phone number to filter messages by sender |
| 50 | chat_jid: Optional chat JID to filter messages by chat |
| 51 | query: Optional search term to filter messages by content |
| 52 | limit: Max |
RemediationAI
The list_messages tool filters messages by chat_jid and sender_phone_number but does not verify the caller owns or has permission to access those chats or sender data. Add a per-request authorization check that validates the caller's identity and confirms they have access to the specified chat_jid before querying; implement this as a helper function like _authorize_chat_access(caller_id, chat_jid) called at the start of list_messages(). This prevents unauthorized disclosure of private messages. Test by attempting to access another user's chat_jid and confirm access is denied.
Tool 'list_messages' shadows reserved database/generic tool name without server-specific prefix.
Evidence
| 31 | @mcp.tool() |
| 32 | def list_messages( |
| 33 | after: Optional[str] = None, |
| 34 | before: Optional[str] = None, |
| 35 | sender_phone_number: Optional[str] = None, |
| 36 | chat_jid: Optional[str] = None, |
| 37 | query: Optional[str] = None, |
RemediationAI
The tool name 'list_messages' collides with reserved generic database tool names, risking namespace conflicts and confusion. Rename the tool to 'whatsapp_list_messages' by changing the function name and updating the @mcp.tool() decorator to use a name parameter: @mcp.tool(name='whatsapp_list_messages'). This server-specific prefix prevents shadowing and clarifies the tool's origin. Verify by checking the MCP tool registry or client introspection to confirm the new name appears without conflicts.
LLM consensus
Tool 'list_chats' shadows reserved generic/database tool name without server-specific prefix.
Evidence
| 64 | limit=limit, |
| 65 | page=page, |
| 66 | include_context=include_context, |
| 67 | context_before=context_before, |
| 68 | context_after=context_after |
| 69 | ) |
| 70 | return messages |
RemediationAI
The tool name 'list_chats' collides with reserved generic database tool names, risking namespace conflicts and confusion. Rename the tool to 'whatsapp_list_chats' by changing the function name and updating the @mcp.tool() decorator to use a name parameter: @mcp.tool(name='whatsapp_list_chats'). This server-specific prefix prevents shadowing and clarifies the tool's origin. Verify by checking the MCP tool registry or client introspection to confirm the new name appears without conflicts.
Tool 'search_contacts' shadows reserved search tool name without server-specific prefix.
Evidence
| 24 | Args: |
| 25 | query: Search term to match against contact names or phone numbers |
| 26 | """ |
| 27 | contacts = whatsapp_search_contacts(query) |
| 28 | return contacts |
RemediationAI
The tool name 'search_contacts' collides with reserved search tool names, risking namespace conflicts and confusion. Rename the tool to 'whatsapp_search_contacts' by changing the function name and updating the @mcp.tool() decorator to use a name parameter: @mcp.tool(name='whatsapp_search_contacts'). This server-specific prefix prevents shadowing and clarifies the tool's origin. Verify by checking the MCP tool registry or client introspection to confirm the new name appears without conflicts.
Tool 'list_chats' calls whatsapp_list_chats which accesses SQLite database at module-level global MESSAGES_DB_PATH without consulting caller identity or per-request credentials.
Evidence
| 55 | context_before: Number of messages to include before each match (default 1) |
| 56 | context_after: Number of messages to include after each match (default 1) |
| 57 | """ |
| 58 | messages = whatsapp_list_messages( |
| 59 | after=after, |
| 60 | before=before, |
| 61 | sender_phone_number=sender_phone_number, |
| 62 | chat_jid=chat_jid, |
| 63 | query=query, |
| 64 | limit=limit, |
| 65 | page=page, |
| 66 | include_context=include_context, |
| 67 | context_before=context_before, |
| 68 | context_after=context_afte |
RemediationAI
The list_chats tool accesses a module-level global MESSAGES_DB_PATH without consulting caller identity or per-request credentials, allowing any caller to read any chat. Modify whatsapp_list_chats() to accept a caller_id parameter and filter results by caller ownership; alternatively, pass caller credentials from the MCP request context into the function. This ensures only the chat owner can retrieve their chats. Test by verifying that two different callers see only their own chats.
LLM consensus
Tool 'search_contacts' calls whatsapp_search_contacts which accesses SQLite database at module-level global MESSAGES_DB_PATH without consulting caller identity or per-request credentials.
Evidence
| 21 | @mcp.tool() |
| 22 | def search_contacts(query: str) -> List[Dict[str, Any]]: |
| 23 | """Search WhatsApp contacts by name or phone number. |
| 24 | |
| 25 | Args: |
| 26 | query: Search term to match against contact names or phone numbers |
| 27 | """ |
| 28 | contacts = whatsapp_search_contacts(query) |
| 29 | return contacts |
| 30 | |
| 31 | @mcp.tool() |
RemediationAI
The search_contacts tool accesses a module-level global MESSAGES_DB_PATH without consulting caller identity or per-request credentials, allowing any caller to search all contacts. Modify whatsapp_search_contacts() to accept a caller_id parameter and filter results to only contacts belonging to that caller; pass caller credentials from the MCP request context. This prevents unauthorized contact enumeration. Test by confirming that two different callers see only their own contacts.
LLM consensus
Tool 'list_messages' calls whatsapp_list_messages which accesses SQLite database at module-level global MESSAGES_DB_PATH without consulting caller identity or per-request credentials.
Evidence
| 27 | """ |
| 28 | contacts = whatsapp_search_contacts(query) |
| 29 | return contacts |
| 30 | |
| 31 | @mcp.tool() |
| 32 | def list_messages( |
| 33 | after: Optional[str] = None, |
| 34 | before: Optional[str] = None, |
| 35 | sender_phone_number: Optional[str] = None, |
| 36 | chat_jid: Optional[str] = None, |
| 37 | query: Optional[str] = None, |
| 38 | limit: int = 20, |
| 39 | page: int = 0, |
| 40 | include_context: bool = True, |
| 41 | context_before: int = 1, |
| 42 | context_after: int = 1 |
| 43 | ) -> List[Dict[str, Any]]: |
| 44 | """Get WhatsApp messages matching specified criteria |
RemediationAI
The list_messages tool accesses a module-level global MESSAGES_DB_PATH without consulting caller identity or per-request credentials, allowing any caller to read any message. Modify whatsapp_list_messages() to accept a caller_id parameter and filter results to only messages from chats owned by that caller; extract caller identity from the MCP request context and pass it to the function. This ensures only authorized users can retrieve their messages. Test by verifying that two different callers see only their own messages.
LLM consensus
Tool 'list_chats' description does not disclose that it may perform NETWORK calls to WhatsApp API via whatsapp_list_chats handler
Evidence
| 56 | context_after: Number of messages to include after each match (default 1) |
| 57 | """ |
| 58 | messages = whatsapp_list_messages( |
| 59 | after=after, |
| 60 | before=before, |
| 61 | sender_phone_number=sender_phone_number, |
| 62 | chat_jid=chat_jid, |
| 63 | query=query, |
| 64 | limit=limit, |
| 65 | page=page, |
| 66 | include_context=include_context, |
| 67 | context_before=context_before, |
| 68 | context_after=context_after |
| 69 | ) |
| 70 | return messages |
| 71 | |
| 72 | @mcp.tool() |
| 73 | def list_chats( |
| 74 | query: Optional[str] = |
RemediationAI
The list_chats tool description does not disclose that it may perform network calls to the WhatsApp API, violating transparency requirements. Update the docstring to include: 'Note: This tool may perform network calls to the WhatsApp API and could block or fail if the API is unavailable.' This informs clients of potential latency and failure modes. Verify by reading the tool description via MCP introspection and confirming the network warning is present.
LLM consensus
Tool 'search_contacts' description does not disclose that it may perform NETWORK calls to WhatsApp API via whatsapp_search_contacts handler
Evidence
| 24 | Args: |
| 25 | query: Search term to match against contact names or phone numbers |
| 26 | """ |
| 27 | contacts = whatsapp_search_contacts(query) |
| 28 | return contacts |
| 29 | |
| 30 | @mcp.tool() |
| 31 | def list_messages( |
| 32 | after: Optional[str] = None, |
| 33 | before: Optional[str] = None, |
RemediationAI
The search_contacts tool description does not disclose that it may perform network calls to the WhatsApp API, violating transparency requirements. Update the docstring to include: 'Note: This tool may perform network calls to the WhatsApp API and could block or fail if the API is unavailable.' This informs clients of potential latency and failure modes. Verify by reading the tool description via MCP introspection and confirming the network warning is present.
LLM consensus
Tool 'list_messages' description does not disclose that it may perform NETWORK calls to WhatsApp API via whatsapp_list_messages handler
Evidence
| 31 | @mcp.tool() |
| 32 | def list_messages( |
| 33 | after: Optional[str] = None, |
| 34 | before: Optional[str] = None, |
| 35 | sender_phone_number: Optional[str] = None, |
| 36 | chat_jid: Optional[str] = None, |
| 37 | query: Optional[str] = None, |
| 38 | limit: int = 20, |
| 39 | page: int = 0, |
| 40 | include_context: bool = True, |
| 41 | context_before: int = 1, |
| 42 | context_after: int = 1 |
| 43 | ) -> List[Dict[str, Any]]: |
| 44 | """Get WhatsApp messages matching specified criteria with optional context. |
| 45 | |
| 46 | Args: |
| 47 | after: Optional ISO-8601 f |
RemediationAI
The list_messages tool description does not disclose that it may perform network calls to the WhatsApp API, violating transparency requirements. Update the docstring to include: 'Note: This tool may perform network calls to the WhatsApp API and could block or fail if the API is unavailable.' This informs clients of potential latency and failure modes. Verify by reading the tool description via MCP introspection and confirming the network warning is present.
LLM consensus
Tool 'list_chats' returns untrusted WhatsApp chat metadata and last_message content from SQLite database without provenance delimiters, enabling indirect prompt injection.
Evidence
| 57 | """ |
| 58 | messages = whatsapp_list_messages( |
| 59 | after=after, |
| 60 | before=before, |
| 61 | sender_phone_number=sender_phone_number, |
| 62 | chat_jid=chat_jid, |
| 63 | query=query, |
| 64 | limit=limit, |
| 65 | page=page, |
| 66 | include_context=include_context, |
| 67 | context_before=context_before, |
| 68 | context_after=context_after |
| 69 | ) |
| 70 | return messages |
RemediationAI
The list_chats tool returns untrusted WhatsApp chat metadata and last_message content from the database without provenance delimiters, enabling indirect prompt injection attacks. Wrap all returned chat and message fields in a provenance marker such as <whatsapp_data source="database">...content...</whatsapp_data> or prefix each field with [WHATSAPP_DATA]. This signals to the LLM that the content is untrusted third-party data. Test by injecting prompt-injection payloads into chat names and verifying they are properly delimited and not executed.
Tool 'list_messages' returns untrusted WhatsApp message content from SQLite database (authored by third parties) without provenance delimiters, enabling indirect prompt injection.
Evidence
| 24 | Args: |
| 25 | query: Search term to match against contact names or phone numbers |
| 26 | """ |
| 27 | contacts = whatsapp_search_contacts(query) |
| 28 | return contacts |
| 29 | |
| 30 | @mcp.tool() |
| 31 | def list_messages( |
| 32 | after: Optional[str] = None, |
| 33 | before: Optional[str] = None, |
RemediationAI
The list_messages tool returns untrusted WhatsApp message content authored by third parties without provenance delimiters, enabling indirect prompt injection attacks. Wrap all returned message content in a provenance marker such as <whatsapp_message source="third_party">...content...</whatsapp_message> or prefix each message with [WHATSAPP_MESSAGE]. This signals to the LLM that the content is untrusted. Test by injecting prompt-injection payloads into messages and verifying they are properly delimited and not executed.
LLM consensus
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 722 | except json.JSONDecodeError: |
| 723 | return False, f"Error parsing response: {response.text}" |
| 724 | except Exception as e: |
| 725 | return False, f"Unexpected error: {str(e)}" |
| 726 | |
| 727 | def download_media(message_id: str, chat_jid: str) -> Optional[str]: |
| 728 | """Download media from a message and return the local file path. |
RemediationAI
Exception handlers in whatsapp.py return full exception details via f-strings like f"Unexpected error: {str(e)}", leaking internal paths and library versions useful for reconnaissance. Replace all generic exception handlers with: except Exception as e: return False, "An unexpected error occurred. Please contact support." and log the full exception internally using logging.exception(). This hides implementation details from callers. Verify by triggering an exception and confirming the response contains no traceback or library version info.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 644 | return False, f"Error: HTTP {response.status_code} - {response.text}" |
| 645 | |
| 646 | except requests.RequestException as e: |
| 647 | return False, f"Request error: {str(e)}" |
| 648 | except json.JSONDecodeError: |
| 649 | return False, f"Error parsing response: {response.text}" |
| 650 | except Exception as e: |
RemediationAI
Exception handlers return full exception details via f-strings like f"Request error: {str(e)}", leaking internal paths and library versions. Replace all requests.RequestException handlers with: except requests.RequestException as e: return False, "Request failed. Please try again." and log the full exception internally using logging.exception(). This hides implementation details. Verify by triggering a network error and confirming the response contains no traceback.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 678 | return False, f"Error: HTTP {response.status_code} - {response.text}" |
| 679 | |
| 680 | except requests.RequestException as e: |
| 681 | return False, f"Request error: {str(e)}" |
| 682 | except json.JSONDecodeError: |
| 683 | return False, f"Error parsing response: {response.text}" |
| 684 | except Exception as e: |
RemediationAI
Exception handlers return full exception details via f-strings like f"Error parsing response: {response.text}", leaking internal paths and library versions. Replace all json.JSONDecodeError handlers with: except json.JSONDecodeError as e: return False, "Invalid response format. Please try again." and log the full exception internally using logging.exception(). This hides implementation details. Verify by triggering a JSON parse error and confirming the response contains no traceback.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 718 | return False, f"Error: HTTP {response.status_code} - {response.text}" |
| 719 | |
| 720 | except requests.RequestException as e: |
| 721 | return False, f"Request error: {str(e)}" |
| 722 | except json.JSONDecodeError: |
| 723 | return False, f"Error parsing response: {response.text}" |
| 724 | except Exception as e: |
RemediationAI
Exception handlers return full HTTP response text via f-strings like f"Error: HTTP {response.status_code} - {response.text}", leaking internal paths and library versions. Replace with: except Exception as e: return False, f"Error: HTTP {response.status_code}" and log the full response internally using logging.exception(). This hides implementation details while preserving the status code. Verify by triggering an HTTP error and confirming the response contains no response body.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 682 | except json.JSONDecodeError: |
| 683 | return False, f"Error parsing response: {response.text}" |
| 684 | except Exception as e: |
| 685 | return False, f"Unexpected error: {str(e)}" |
| 686 | |
| 687 | def send_audio_message(recipient: str, media_path: str) -> Tuple[bool, str]: |
| 688 | try: |
RemediationAI
Exception handlers return full exception details via f-strings like f"Unexpected error: {str(e)}", leaking internal paths and library versions. Replace all generic exception handlers with: except Exception as e: return False, "An unexpected error occurred. Please contact support." and log the full exception internally using logging.exception(). This hides implementation details. Verify by triggering an exception and confirming the response contains no traceback.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 648 | except json.JSONDecodeError: |
| 649 | return False, f"Error parsing response: {response.text}" |
| 650 | except Exception as e: |
| 651 | return False, f"Unexpected error: {str(e)}" |
| 652 | |
| 653 | def send_file(recipient: str, media_path: str) -> Tuple[bool, str]: |
| 654 | try: |
RemediationAI
Exception handlers return full exception details via f-strings like f"Unexpected error: {str(e)}", leaking internal paths and library versions. Replace all generic exception handlers with: except Exception as e: return False, "An unexpected error occurred. Please contact support." and log the full exception internally using logging.exception(). This hides implementation details. Verify by triggering an exception and confirming the response contains no traceback.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 668 | "media_path": media_path |
| 669 | } |
| 670 | |
| 671 | response = requests.post(url, json=payload) |
| 672 | |
| 673 | # Check if the request was successful |
| 674 | if response.status_code == 200: |
RemediationAI
The requests.post() call lacks an explicit timeout parameter, allowing a hung WhatsApp API to block indefinitely and exhaust connection pools. Add a timeout parameter: response = requests.post(url, json=payload, timeout=10) with a reasonable timeout value (e.g., 10 seconds). This ensures the request fails fast if the upstream is unresponsive. Verify by simulating a hung upstream and confirming the request times out within the specified duration.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 708 | "media_path": media_path |
| 709 | } |
| 710 | |
| 711 | response = requests.post(url, json=payload) |
| 712 | |
| 713 | # Check if the request was successful |
| 714 | if response.status_code == 200: |
RemediationAI
The requests.post() call lacks an explicit timeout parameter, allowing a hung WhatsApp API to block indefinitely and exhaust connection pools. Add a timeout parameter: response = requests.post(url, json=payload, timeout=10) with a reasonable timeout value (e.g., 10 seconds). This ensures the request fails fast if the upstream is unresponsive. Verify by simulating a hung upstream and confirming the request times out within the specified duration.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 741 | "chat_jid": chat_jid |
| 742 | } |
| 743 | |
| 744 | response = requests.post(url, json=payload) |
| 745 | |
| 746 | if response.status_code == 200: |
| 747 | result = response.json() |
RemediationAI
The requests.post() call lacks an explicit timeout parameter, allowing a hung WhatsApp API to block indefinitely and exhaust connection pools. Add a timeout parameter: response = requests.post(url, json=payload, timeout=10) with a reasonable timeout value (e.g., 10 seconds). This ensures the request fails fast if the upstream is unresponsive. Verify by simulating a hung upstream and confirming the request times out within the specified duration.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 634 | "message": message, |
| 635 | } |
| 636 | |
| 637 | response = requests.post(url, json=payload) |
| 638 | |
| 639 | # Check if the request was successful |
| 640 | if response.status_code == 200: |
RemediationAI
The requests.post() call lacks an explicit timeout parameter, allowing a hung WhatsApp API to block indefinitely and exhaust connection pools. Add a timeout parameter: response = requests.post(url, json=payload, timeout=10) with a reasonable timeout value (e.g., 10 seconds). This ensures the request fails fast if the upstream is unresponsive. Verify by simulating a hung upstream and confirming the request times out within the specified duration.
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # WhatsApp MCP Server |
| 2 | |
| 3 | This is a Model Context Protocol (MCP) server for WhatsApp. |
| 4 | |
| 5 | With this you can search and read your personal Whatsapp messages (including images, videos, documents, and audio messages), search your contacts and send messages to either individuals or groups. You can also send media files including images, videos, documents, and audio messages. |
| 6 | |
| 7 | It connects to your **personal WhatsApp account** directly via the Whatsapp web multidevice API (using the [whatsmeow](https://gith |
RemediationAI
The MCP manifest in README.md declares tools but does not specify an authentication mechanism (no auth, authorization, bearer, oauth, mtls, apiKey, basic, or token fields). Add an explicit authentication field to the manifest such as "auth": "bearer_token" or "auth": "mtls" and document the mechanism in the README. This clarifies to reviewers whether authentication is enforced at the network layer, host level, or via MCP-level credentials. Verify by confirming the authentication field is present and matches the actual implementation.
FastMCP server 'whatsapp' declares dynamic tools via @mcp.tool() decorators but does not emit notifications/tools/list_changed capability or per-tool version/etag/digest fields, preventing clients from detecting tool list mutations at runtime.
Evidence
| 1 | from typing import List, Dict, Any, Optional |
| 2 | from mcp.server.fastmcp import FastMCP |
| 3 | from whatsapp import ( |
| 4 | search_contacts as whatsapp_search_contacts, |
RemediationAI
The FastMCP server declares dynamic tools via @mcp.tool() decorators but does not emit tools/list_changed notifications or per-tool version/etag fields, preventing clients from detecting tool mutations at runtime. Add support for the tools/list_changed capability by implementing a notification handler and assigning a version or etag to each tool; use @mcp.tool(name='...', version='1.0') or emit a tools/list_changed notification whenever tools are added/removed. This allows clients to refresh their tool cache. Verify by adding a tool dynamically and confirming the client receives a tools/list_changed notification.
list_chats
list_messages
+2 more โ click to filter
search_contacts