Skip to main content

Hooks

Overview

Jupyter MCP Server includes a hook system that fires events before and after MCP tool calls and kernel executions. This enables auditing and custom integrations without modifying core server code. For built-in tracing, see Observability.

Hook Events

The hook system fires the following events:

EventWhen FiredKey Data
BEFORE_TOOL_CALLBefore any MCP tool executestool_name, arguments
AFTER_TOOL_CALLAfter any MCP tool completestool_name, arguments, result, error
BEFORE_EXECUTEBefore code execution in a kernelkernel_id, code
AFTER_EXECUTEAfter code execution completeskernel_id, outputs, error
KERNEL_LIFECYCLEOn kernel start, restart, or shutdownevent_type, kernel_id, kernel_name

All built-in tools are instrumented with BEFORE_TOOL_CALL / AFTER_TOOL_CALL hooks. Cell and code execution tools additionally fire BEFORE_EXECUTE / AFTER_EXECUTE hooks around kernel interactions.

Architecture

Each tool function is wrapped with a @with_hooks decorator that fires BEFORE_TOOL_CALL / AFTER_TOOL_CALL events. For execution tools, BEFORE_EXECUTE / AFTER_EXECUTE events are additionally fired around the kernel interaction. All events are dispatched through a singleton HookRegistry to registered handlers.

Handler Error Behavior

Each handler declares a propagate_errors flag:

Critical vs Optional Handlers
  • propagate_errors = True (critical): Exceptions propagate to the caller and may fail the tool call.
  • propagate_errors = False (optional): Exceptions are logged at DEBUG level and suppressed. The built-in OTel handler uses this mode.

Context Correlation

Before/after event pairs share a context dictionary. Handlers can store data in the context during BEFORE_* events and retrieve it during the corresponding AFTER_* event. For example, the OTel handler stores a span reference in context["_otel_span"] to end it on the after event.

Writing a Custom Hook Handler

You can create custom handlers for logging, metrics, auditing, or any other purpose.

Handler Interface

class HookHandler(Protocol):
propagate_errors: bool

async def on_event(self, event: HookEvent, **kwargs) -> None: ...

Example: Simple Logging Handler

from jupyter_mcp_server.hooks import HookEvent, HookRegistry

class LoggingHandler:
propagate_errors = False # Don't break tool calls on logging errors

async def on_event(self, event: HookEvent, **kwargs) -> None:
if event == HookEvent.BEFORE_TOOL_CALL:
print(f"Tool called: {kwargs.get('tool_name')}")
elif event == HookEvent.AFTER_TOOL_CALL:
error = kwargs.get("error")
if error:
print(f"Tool failed: {kwargs.get('tool_name')} - {error}")
else:
print(f"Tool completed: {kwargs.get('tool_name')}")

# Register the handler
handler = LoggingHandler()
HookRegistry.get_instance().register(handler)

Registering Handlers

Handlers are registered with the singleton HookRegistry:

from jupyter_mcp_server.hooks import HookRegistry

registry = HookRegistry.get_instance()

# Register
registry.register(my_handler)

# Unregister when done
registry.unregister(my_handler)