# Expose services as MCP

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

![New MCP gateway](https://zatosource-production.b-cdn.net/docs/gfx/ai/mcp-gateway-create.webp?v=1790934276)

Dashboard menu: AI > MCP gateways

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 {#before-you-begin}

You need:

- A running Zato environment. The [Docker installation guide](https://zato.io/docs/admin/guide/install/docker.html) sets one up.
- An MLLP channel with message auditing enabled. The [HL7 v2 tutorial](https://zato.io/tutorials/healthcare/hl7v2.html) sends the example message for MRN `12345` with control ID `MSG00001`, and this guide uses that message so that the AI agent has real data to query 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](https://zato.io/docs/dev/healthcare/audit-log.html#search-by-patient-identifier) explains.

## Write the service {#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:

```python
# -*- 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](https://zato.io/docs/ai/mcp/tool-schemas.html) explains.

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

## Create the client identity {#create-the-client-identity}

Every MCP gateway requires authentication. Create the [API key](https://zato.io/docs/security/api-keys.html) 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](https://zatosource-production.b-cdn.net/docs/gfx/security/apikey-create.webp?v=1790440731)

Dashboard menu: Security > API keys

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](https://zato.io/docs/ai/mcp/security.html) explains.

## Configure the gateway {#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.

**Dashboard**

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](https://zatosource-production.b-cdn.net/docs/gfx/ai/mcp-gateway-options.webp?v=1790934278)

Dashboard menu: AI > MCP gateways

**GitOps**

Define the API key, the gateway [security group](https://zato.io/docs/security/groups.html) and the gateway in the same [Enmasse YAML](https://zato.io/docs/ai/enmasse.html):

```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](https://zato.io/docs/admin/enmasse.html#automatic-imports) explains.

## Connect the MCP client {#connect-the-mcp-client}

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

```text
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 {#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:

```json
{
  "patient_mrn": "12345"
}
```

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

```json
{
  "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.

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

## Add more MCP tools {#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:

- [Interface health for AI agents](https://zato.io/docs/dev/healthcare/ai/interface-health.html)
- [Feed silence for AI agents](https://zato.io/docs/dev/healthcare/ai/feed-silence.html)
