Expose services as MCP

Let AI applications use existing Zato services under the access controls you define.

New MCP gateway

An MCP gateway lets AI applications invoke selected Zato services as tools. The gateway authenticates each request and enforces its configured controls.

This page builds a patient lookup that queries the clinical audit log. The lookup is one example, and the same process applies to other services that can be exposed through an MCP gateway.

Before you begin

You need:

  • A running Zato environment. The Docker installation guide sets one up.
  • An MLLP channel with message auditing enabled. The HL7 v2 tutorial sends the example message for MRN 12345 with control ID MSG00001, which is what we'll use here. The point is, we need some real data for AI to make use of via MCP.
  • An MCP client, such as Claude Code

Search for 12345 under Monitoring > Audit log before continuing. The result should contain the received message and its ACK, as the clinical audit log explains.

Write the service

The service searches the clinical audit log for received messages that mention the requested MRN. The source, event_type and query filters combine, so acknowledgments from the same trace remain outside the service response.

The response contains the control ID, event type and arrival time of each matching message:

# -*- coding: utf-8 -*-

# Zato
from zato.common.audit_log.api import AuditEvent, AuditSource
from zato.server.service import Service

# #####################################################################
# #####################################################################

class PatientMessageStatus(Service):
    """ Returns whether HL7 messages for a given patient MRN arrived,
    with the control ID, event type and arrival time of each.
    """
    name = 'patient.message-status'

    input  = 'patient_mrn'
    output = 'has_arrived', 'messages'

    def handle(self) -> 'None':

        # Read the MRN the caller asked about ..
        patient_mrn = self.request.input.patient_mrn

        # .. search for received MLLP messages that mention it ..
        events = self.audit.search(
            source=AuditSource.MLLP_Channel,
            event_type=AuditEvent.Message_Received,
            query=patient_mrn,
        )

        # .. copy only the fields needed by the caller ..
        messages = []

        for event in events:
            messages.append({
                'control_id': event['msg_id'],
                'event_type': event['event_type'],
                'received_at': event['event_time_iso'],
            })

        # .. determine whether at least one message arrived ..
        has_arrived = len(messages) > 0

        # .. and return the minimized result.
        self.response.payload = {
            'has_arrived': has_arrived,
            'messages': messages,
        }

The docstring becomes the description used during model tool selection. The declared input and output fields become its MCP schema, as tool schemas explains.

Deploy the class as patient.message-status using the Zato IDE or the deployment process used in your environment.

Create the client identity

Every MCP gateway requires authentication. Create the API key used in this example:

  1. Open Security > API keys in the Dashboard.
  2. Create an API key named clinical-agents.
  3. Set its key to abc123.
  4. Click OK.
New API key

The security definition becomes the client identity in MCP sessions, rate-limit counters and audit records. Give each client its own credential when they require separate identity or rate accounting, as MCP gateway security explains.

Configure the gateway

The Dashboard steps and the GitOps YAML below create the same gateway - named clinical at /mcp/clinical, with the patient.message-status service assigned, the clinical-agents API key allowed, tool arguments validated and handled MCP requests recorded.

  1. Open AI > MCP gateways and click Create a new MCP gateway.
  2. Enter clinical as the name and /mcp/clinical as the URL path.
  3. Assign patient.message-status in the Services picker.
  4. Assign clinical-agents in the Security picker.
  5. On What do they receive?, open More options and Gateway options.
  6. Enable Validate input against each tool's schema and Record each request in the audit log.
  7. Click OK.

Gateway options

Define the API key, the gateway security group and the gateway in the same Enmasse YAML:

security:

  - name: clinical-agents
    type: apikey
    password: Zato_Enmasse_Env.ClinicalAgentsApiKey

groups:

  - name: mcp.clinical
    members:
      - clinical-agents

mcp_gateway:

  - name: clinical
    url_path: /mcp/clinical
    services:
      - patient.message-status
    security_groups:
      - mcp.clinical
    validate_input: true
    is_audit_log_active: true

Zato_Enmasse_Env.ClinicalAgentsApiKey reads the API key from the ClinicalAgentsApiKey environment variable. Set that variable to abc123 for this example.

You can import the file from System > Config > Import enmasse. A file mounted at /opt/hot-deploy/enmasse/enmasse.yaml is imported at startup. Matching Enmasse files inside a mounted project are also imported when they change, as automatic imports explains.

Connect the MCP client

The MCP endpoint for your gateway will be http://localhost:11223/mcp/clinical, and here's how you can connect Claude Code:

claude mcp add --transport http --header "X-API-Key: abc123" zato-clinical http://localhost:11223/mcp/clinical

With other MCP clients, just point them to the same URL and X-API-Key header.

Verify the lookup

You can now ask your AI:

Do we have any messages for patient 12345?
Yes. One message arrived for patient 12345 at 12:00 on 15 March 2026. Its control ID is MSG00001.

Behind this exchange, the model selected patient.message-status and called it with:

{
  "patient_mrn": "12345"
}

For the message sent by the HL7 v2 tutorial, the tool returned:

{
  "has_arrived": true,
  "messages": [{
    "control_id": "MSG00001",
    "event_type": "message-received",
    "received_at": "2026-03-15T12:00:00"
  }]
}

And this is the same lookup for an MRN with no messages:

Did a message arrive for patient 99999?
No. There are no messages for patient 99999.

Behind the scenes, the tool returned an empty result, which is how AI knew there were no messages for 99999.

{
  "has_arrived": false,
  "messages": []
}

Add more MCP tools

You can create and expose other useful services in a similar way, and here are some demo examples to get you started:


Schedule a meaningful demo

Book a demo with an expert who will help you build meaningful systems that match your ambitions

"We evaluated 12 integration platforms and Zato was the only one to score 100%."

Philip Zuñiga, Assistant Professor, University of the Philippines