📍 Navigation: Documentation Home | Server Guide | Getting started | Architecture | Installation | Configuration | Security | Customization | Client Guide
Server hooks let you inject custom logic around every tool invocation — without modifying the server source or adding a wrapper layer. You provide a plain Python module; the server loads it at startup and calls your functions at the right moments.
Three hook points are called in sequence for every tool call:
| Hook | When it fires | Signature |
|---|---|---|
on_tool_call |
Before the handler runs | (ctx: ToolCallContext) -> None |
on_tool_result |
After a successful result | (ctx: ToolCallContext, result: object) -> None |
on_tool_error |
When the handler raises | (ctx: ToolCallContext, error: Exception) -> None |
All hooks are synchronous and run inside the same worker thread as the tool. A hook that raises an exception is caught and logged — it never interrupts the tool call itself or the server.
Write a plain Python file that imports ServerHooks and ToolCallContext, defines your hook functions, and exposes a get_hooks() factory.
# my_hooks.py
from teradata_mcp_server.hooks import ServerHooks, ToolCallContext
def _on_tool_call(ctx: ToolCallContext) -> None:
print(f"[CALL] {ctx.tool_name} args={ctx.kwargs}")
def _on_tool_result(ctx: ToolCallContext, result: object) -> None:
print(f"[RESULT] {ctx.tool_name}")
def _on_tool_error(ctx: ToolCallContext, error: Exception) -> None:
print(f"[ERROR] {ctx.tool_name}: {error}")
def get_hooks() -> ServerHooks:
return ServerHooks(
on_tool_call=_on_tool_call,
on_tool_result=_on_tool_result,
on_tool_error=_on_tool_error,
)You only need to supply the hooks you use. Omit any field (or pass None) to skip that hook point:
def get_hooks() -> ServerHooks:
# Only intercept errors — ignore call and result events
return ServerHooks(on_tool_error=_on_tool_error)Every hook receives a ToolCallContext snapshot of the in-flight request:
| Field | Type | Description |
|---|---|---|
tool_name |
str |
MCP tool name, e.g. base_tableList |
kwargs |
dict |
Keyword arguments passed to the tool (internal params conn, tool_name already removed) |
request_context |
object |
RequestContext from the middleware; carries request_id, headers, session info |
engine |
object |
SQLAlchemy Engine for this request |
profile_name |
str | None |
Active profile, e.g. dba |
db_user |
str | None |
Teradata database user for this session |
Access request_context.request_id to correlate on_tool_call and on_tool_result / on_tool_error events in concurrent scenarios:
_in_flight: dict[str, float] = {}
def _on_tool_call(ctx: ToolCallContext) -> None:
key = getattr(ctx.request_context, "request_id", None) or ctx.tool_name
_in_flight[key] = time.perf_counter()
def _on_tool_result(ctx: ToolCallContext, result: object) -> None:
key = getattr(ctx.request_context, "request_id", None) or ctx.tool_name
elapsed = time.perf_counter() - _in_flight.pop(key, time.perf_counter())
print(f"{ctx.tool_name} completed in {elapsed:.3f}s")Pass the path to your file via the --hooks_module CLI argument or the HOOKS_MODULE environment variable.
CLI:
uv run teradata-mcp-server \
--database_uri "teradata://user:pass@host/db" \
--hooks_module /path/to/my_hooks.pyEnvironment variable:
export HOOKS_MODULE=/path/to/my_hooks.py
uv run teradata-mcp-server --database_uri "teradata://user:pass@host/db"Claude Desktop claude_desktop_config.json:
{
"mcpServers": {
"teradata": {
"command": "uvx",
"args": ["teradata-mcp-server", "--hooks_module", "/path/to/my_hooks.py"],
"env": {
"DATABASE_URI": "teradata://user:pass@host/db"
}
}
}
}The value can be either:
- A file path ending in
.py— loaded directly from disk - A dotted Python import name (e.g.
my_package.hooks) — resolved via the normal import machinery
If the module cannot be loaded, a warning is logged and the server starts normally with all hooks disabled. A misconfigured hooks module never prevents the server from starting.
- Calls
get_hooks()once at startup and caches the result. - Fires
on_tool_callbefore the handler executes. - Fires
on_tool_resultwith the raw handler result, before it is formatted for MCP. - Fires
on_tool_errorwhen the handler raises, before the error is formatted for MCP. - Catches any exception your hook raises, logs it as a warning, and continues execution.
- Passes the same
ToolCallContextinstance to all three hooks for the same request, so you can safely attach state to it.
The repository ships a complete reference implementation — a performance monitor that times every tool call and writes one log line per invocation:
2026-05-04 10:23:14 | base_tableList | 0.234s | OK
2026-05-04 10:23:15 | base_readQuery | 1.842s | OK
2026-05-04 10:23:16 | base_columnDescription | 0.031s | ERROR: connection timeout
See examples/server-customisation/server-hooks/perf_monitor_hooks.py.
Run it with:
uv run teradata-mcp-server \
--database_uri "teradata://user:pass@host/db" \
--hooks_module examples/server-customisation/server-hooks/perf_monitor_hooks.py| Use case | Hooks used |
|---|---|
| Performance monitoring | on_tool_call, on_tool_result, on_tool_error |
| Audit logging (who called what) | on_tool_call |
| Request rate limiting or quota enforcement | on_tool_call |
| Alerting on specific tool errors | on_tool_error |
| Caching / memoisation of expensive tools | on_tool_call, on_tool_result |
| Telemetry / tracing (OpenTelemetry spans) | on_tool_call, on_tool_result, on_tool_error |
| Component | Purpose |
|---|---|
ServerHooks dataclass |
Declares the three optional hook callables |
ToolCallContext dataclass |
Per-request snapshot passed to every hook |
get_hooks() factory |
Entry point the server calls once at startup |
--hooks_module / HOOKS_MODULE |
How to tell the server which module to load |
| Error isolation | Hook exceptions are caught and logged; they never break tool execution |