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:
| Key | Value |
|---|---|
gateway | The name of the gateway |
method | The JSON-RPC method of the message |
username | The 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 |
request | The JSON-RPC message, as the agent sent it |
What the service does decides what the gateway processes:
- A service that produces no response, or a response without a
requestkey, leaves the message unchanged. - A service that returns an object under the
requestkey replaces the message with that object. - A service that raises an exception refuses the message. The agent receives JSON-RPC error
-32030with the messageRequest rejected, and the reason stays in the server's log.
The service below rewrites the arguments of one tool and refuses another one:
# 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:
| Key | Value |
|---|---|
gateway | The name of the gateway |
method | The 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 |
request | The message the reply answers, as the request hook left it, or null in the same two cases |
response | The 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
- The gateway authenticates the caller and parses the message.
- The request hook service receives the message.
- The gateway checks the session, validates arguments and invokes the tool.
- Response controls shape the tool's result.
- 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
| Feature | What it does |
|---|---|
| Response controls | Data masking, PII removal and size caps applied before the response hook |
| Argument validation | Checks on tool arguments after the request hook |
| Audit log | How refused messages are recorded |