Secured APIs as tools
Let agents call OAuth-secured APIs through a tool - two credentials that never meet.
An AI agent needs data from an enterprise API that requires OAuth - and giving the OAuth client's secret to the agent is exactly what must never happen. The configuration below gives the agent a tool instead: the agent authenticates to an MCP gateway with its own API key, the tool is a service, and the service calls the secured API through an outgoing connection whose OAuth credentials live in the platform. Two credentials, two directions, and neither side ever sees the other's secret.
Prerequisites. An OAuth-secured REST API to call - any API with a client_credentials token endpoint. The example uses a CRM at https://crm.internal.example.com.
The service
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class GetCustomerBalance(Service):
""" Returns the outstanding balance of one customer account from the CRM.
"""
name = 'example.crm.get-customer-balance'
input = 'account_id'
output = 'account_id', 'balance', 'currency'
def handle(self):
account_id = self.request.input.account_id
# The connection injects the Bearer token it obtains and refreshes itself -
# the OAuth client's id and secret never appear in service code.
conn = self.rest['CRM API']
response = conn.get(self.cid, params={'account_id': account_id})
crm_data = response.data
self.response.payload.account_id = account_id
self.response.payload.balance = crm_data['balance']
self.response.payload.currency = crm_data['currency']
The docstring and the declared I/O are what the agent's model sees - the tool schemas page explains the mapping.
The configuration
One enmasse file holds both credential paths:
security:
# What the agent presents to the gateway - an API key, never the OAuth secret
- name: crm.agent.key
type: apikey
username: crm-agent
password: Zato_Enmasse_Env.CRM_Agent_Key
# What the platform presents to the CRM - the OAuth client, never given to any agent.
# The platform obtains and refreshes the Bearer tokens automatically.
- name: crm.oauth.client
type: bearer_token
username: zato-crm-client
password: Zato_Enmasse_Env.CRM_OAuth_Secret
auth_endpoint: https://login.internal.example.com/oauth2/token
grant_type: client_credentials
groups:
- name: crm-agents
members:
- crm.agent.key
outgoing_rest:
- name: CRM API
host: https://crm.internal.example.com
url_path: /api/v1/balance
security: crm.oauth.client
mcp_gateway:
- name: crm
url_path: /mcp/crm
services:
- example.crm.get-customer-balance
security_groups:
- crm-agents
validate_input: true
is_audit_log_active: true
Connect the agent
claude mcp add --transport http crm http://localhost:17010/mcp/crm \
--header "X-API-Key: the-agent-key-value"
Asking the agent about a customer's balance makes it call the example.crm.get-customer-balance tool and the reply carries the CRM's data - the CRM sees the platform's OAuth client and nothing else, and the agent holds its own API key and nothing else.
Failure behavior
Each credential fails separately:
- An agent with a wrong or revoked API key gets HTTP 403 from the gateway and the CRM is never contacted - the rejection is in the server log and, as an
auth-failedevent, in the audit log. - When the OAuth client is the problem - an expired secret, a revoked grant - the tool call fails with
isErrorand the agent sees that the tool failed, not why. The reason, with the CRM's response, is in the server log under the request's CID - to follow it end to end, see tracing one call. - Revoking one agent is removing its key from the
crm-agentsgroup - membership changes are live, with no effect on the OAuth side.
See also
| Feature | What it does |
|---|---|
| MCP gateway security | The credential types and groups agents authenticate with |
| Multi-agent isolation | Several agents on one platform that cannot see each other |
| Tool selection | Docstrings and schemas that steer agents to the right tool |