MCP hook services

Inspect, rewrite or refuse every MCP message in a Python service.

A service is a Python class deployed to Zato, and an MCP gateway normally exposes services as tools. Hook services are invoked by the gateway itself, independently of the tools, to apply rules of your own to the protocol traffic:

  • The request hook service receives every JSON-RPC message an agent sends, before the gateway processes it, and can leave it unchanged, replace it or refuse it.
  • The response hook service receives every reply the gateway is about to send, and can leave it unchanged or replace it.

Both are configured in the Hook services card under More options on the wizard's step 02, What do they receive? - each line opens a list of the deployed services to pick one from. In enmasse, they are the request_hook_service and response_hook_service keys.

The request hook

The request hook service receives one message at a time, for every method - initialize, tools/list, tools/call, prompts/get and the notifications alike:

KeyValue
gatewayThe name of the gateway
methodThe JSON-RPC method of the message
usernameThe identity of the caller - the person a bearer token names when its definition has an identity claim, otherwise the name of the security definition the caller authenticated with
requestThe JSON-RPC message, as the agent sent it

What the service does decides what the gateway processes:

  1. A service that produces no response, or a response without a request key, leaves the message unchanged.
  2. A service that returns an object under the request key replaces the message with that object.
  3. A service that raises an exception refuses the message. The agent receives JSON-RPC error -32030 with the message Request rejected, and the reason stays in the server's log.

The service below rewrites the arguments of one tool and refuses another one:

crm/hooks.py
# Zato
from zato.server.service import Service

class OrderRequestHook(Service):
    name = 'crm.mcp-request-hook'

    def handle(self):
        payload = self.request.payload
        message = payload['request']

        # Only tool calls carry arguments to check
        if payload['method'] != 'tools/call':
            return

        tool_name = message['params']['name']

        # Cancellations are performed by people, never by agents ..
        if tool_name == 'crm.order.cancel':
            raise Exception('Order cancellations are not permitted through this gateway')

        # .. and status lookups always run against the current region.
        if tool_name == 'crm.order.status':
            message['params']['arguments']['region'] = 'eu-central'
            self.response.payload = {'request': message}

The response hook

The response hook service receives every reply body the gateway sends, the errors the gateway produces itself included:

KeyValue
gatewayThe name of the gateway
methodThe JSON-RPC method of the message the reply answers, or null when the request body is not a JSON object or the request hook refused the message
requestThe message the reply answers, as the request hook left it, or null in the same two cases
responseThe JSON-RPC reply body

A service that returns an object under the response key replaces the reply with that object, and any other response leaves the reply unchanged. A service that raises an exception, or returns anything other than an object under the key, refuses the reply - the agent receives JSON-RPC error -32031 with the message Response rejected in its place.

A notification is answered with HTTP 202 and no body, so no response hook runs for it.

Order of processing

  1. The gateway authenticates the caller and parses the message.
  2. The request hook service receives the message.
  3. The gateway checks the session, validates arguments and invokes the tool.
  4. Response controls shape the tool's result.
  5. The response hook service receives the reply.

The response hook service therefore sees a tool's result after data masking, PII removal and the size caps, which is also what the agent receives.

Hook services run in the request's own thread, so the time they take adds to every call. With the audit log on, a refused message or reply is recorded with an error outcome and the error message.

See also

FeatureWhat it does
Response controlsData masking, PII removal and size caps applied before the response hook
Argument validationChecks on tool arguments after the request hook
Audit logHow refused messages are recorded

Learn more