"""
Django Signal Receivers for mcpclient.
.. note::
**Experimental.** The MCPClient was designed and coded by Claude Code (Anthropic's
Claude Opus 5.5), with Lawrence McDaniel as co-author. It is experimental, and will
be documented.
"""
# pylint: disable=W0613,C0115
from django.db import transaction
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from smarter.lib import logging
from smarter.lib.django.waffle import SmarterWaffleSwitches
from .caching import invalidate_cached_catalog
from .models import MCPClient
from .serializers import MCPClientSerializer
from .signals import (
mcpclient_connected,
mcpclient_connection_failed,
mcpclient_resource_read,
mcpclient_tool_called,
mcpclient_tool_failed,
mcpclient_tool_responded,
)
from .tasks import refresh_mcpclient
logger = logging.getSmarterLogger(__name__, any_switches=[SmarterWaffleSwitches.MCPCLIENT_LOGGING])
module_prefix = __name__
[docs]
@receiver(post_save, sender=MCPClient)
def mcpclient_saved(sender, instance: MCPClient, created: bool, **kwargs):
"""
Log the MCPClient, and refresh its catalog once the transaction commits.
Status updates are made with queryset updates, which do not send ``post_save``, so
this runs only when the MCPClient's configuration is saved.
"""
prefix = logging.formatted_text(f"{module_prefix}.mcpclient_saved()")
data = logging.formatted_json(MCPClientSerializer(instance).data)
logger.info("%s - %s %s, %s", prefix, "created" if created else "updated", instance, data)
if instance.is_active and instance.endpoint_url:
mcpclient_id = instance.pk
transaction.on_commit(lambda: refresh_mcpclient.delay(mcpclient_id))
[docs]
@receiver(pre_delete, sender=MCPClient)
def mcpclient_deleted(sender, instance: MCPClient, **kwargs):
"""Log the MCPClient, and invalidate its cached catalog."""
prefix = logging.formatted_text(f"{module_prefix}.mcpclient_deleted()")
logger.info("%s - %s", prefix, instance)
invalidate_cached_catalog(instance)
[docs]
@receiver(mcpclient_connected, dispatch_uid="mcpclient_connected")
def handle_mcpclient_connected(sender, mcpclient: MCPClient, catalog, **kwargs):
"""Log a successful connection to an MCP server."""
logger.info(
"%s - %s connected to %s %s, protocol %s, %s tools",
logging.formatted_text(f"{module_prefix}.mcpclient_connected"),
mcpclient.name,
catalog.server_name,
catalog.server_version,
catalog.protocol_version,
len(catalog.tools),
)
[docs]
@receiver(mcpclient_connection_failed, dispatch_uid="mcpclient_connection_failed")
def handle_mcpclient_connection_failed(sender, mcpclient: MCPClient, error: str, **kwargs):
"""Log a failed connection to an MCP server."""
logger.warning(
"%s - %s: %s",
logging.formatted_text(f"{module_prefix}.mcpclient_connection_failed"),
mcpclient.name,
error,
)
[docs]
@receiver(mcpclient_resource_read, dispatch_uid="mcpclient_resource_read")
def handle_mcpclient_resource_read(sender, mcpclient: MCPClient, uri: str, characters: int, **kwargs):
"""Log a resource read."""
logger.info(
"%s - %s %s characters: %s",
logging.formatted_text(f"{module_prefix}.mcpclient_resource_read"),
mcpclient.name,
uri,
characters,
)