MCP Tool Servers
mod_openai includes a built-in Model Context Protocol (MCP) client. Point an AI agent (or an AI sidecar) at one or more remote MCP servers and the platform discovers their tools at call setup, exposes each tool to the LLM as a SWAIG function, and routes the model's tool calls to the server over JSON-RPC — no webhook of your own required.
It can also pull an MCP server's resources into the agent's global_data, making remote context available to your prompt and SWML variable expansion.
The same client serves both surfaces:
- the AI agent — the SWML
<ai> verb (mod_openai.c / app_config.c)
- the AI sidecar — the
ai_sidecar verb (see sidecar.md)
Both read mcp_servers from the SWAIG block, and both run discovery once at session start.
Overview
| Property |
Behavior |
| Transport |
Streamable HTTP — JSON-RPC 2.0 over HTTP POST (Content-Type/Accept: application/json) |
| Discovery timing |
Once, at session start (agent: before the greeting; sidecar: at attach). Tools and resources are not refreshed mid-call. |
| Tools |
Each discovered tool is registered as a SWAIG function and is always active |
| Resources |
Optional, opt-in per server; merged into global_data |
| Auth |
Whatever you put in headers (e.g. a bearer token). No SignalWire request signature is added. |
| Retries |
None — single attempt per request (10s connect, 30s total timeout) |
A server that advertises neither tools nor resources is skipped. If one server fails to initialize, it is skipped with a warning and the remaining servers are still processed.
Configuration — mcp_servers
mcp_servers is an array inside the SWAIG block, alongside functions. Each entry describes one server.
{
"ai": {
"prompt": { "text": "You are a helpful support agent." },
"SWAIG": {
"defaults": {
"web_hook_url": "https://api.example.com/swaig"
},
"functions": [
{ "function": "...", "description": "...", "parameters": { } }
],
"mcp_servers": [
{
"url": "https://crm.example.com/mcp",
"headers": { "Authorization": "Bearer ${global_data.crm_token}" },
"resources": true,
"resource_vars": { "customer_id": "${global_data.customer_id}" }
}
]
}
}
}
You can mix MCP servers and ordinary webhook functions in the same SWAIG block. defaults.web_hook_url is not required for MCP tools — they bypass the webhook path entirely (see Tools as SWAIG functions).
The sidecar uses the identical structure in its YAML config; see the SWAIG block in sidecar.md.
Field reference
| Field |
Type |
Required |
Default |
Description |
url |
string |
yes |
— |
MCP server endpoint (Streamable HTTP). The entry is skipped if empty. |
headers |
object |
no |
— |
Extra HTTP headers sent on every request to this server, as { "Header-Name": "value" }. Use for auth. Values may reference SWML variables (e.g. ${global_data.crm_token}), expanded when the document is processed. |
resources |
bool |
no |
false |
Fetch this server's MCP resources into global_data. Requires the server to advertise the resources capability. |
resource_vars |
object |
no |
— |
Values substituted into {placeholder} slots in resource URI templates (see Resources → global_data). |
Protocol flow
All traffic is JSON-RPC 2.0 over HTTP POST. The client identifies itself as signalwire-ai-agent (version = the module's SWAIG version) and negotiates protocol version 2025-06-18.
At session start, per server:
| Phase |
JSON-RPC method |
When |
| Handshake |
initialize |
Once. Server returns its capabilities (tools, resources). |
| Handshake |
notifications/initialized |
Once, after initialize (fire-and-forget). |
| Discovery |
resources/list → resources/read |
If the server supports resources and the entry sets resources: true. |
| Discovery |
tools/list |
If the server supports tools. Each tool is registered as a SWAIG function. |
During the conversation, per invocation:
| Phase |
JSON-RPC method |
When |
| Invocation |
tools/call |
Each time the LLM calls a tool that resolved to this server. |
If initialize fails or the server advertises no usable capability, that server is skipped. There is no retry or backoff on any request; a slow or unreachable server adds latency at startup (discovery) or on the call itself (invocation).
Debugging: enable the agent's debug parameter to log the raw MCP POST request and MCP RESP response bodies for every JSON-RPC call.
Tools as SWAIG functions
Every tool returned by tools/list becomes a SWAIG function:
- Name — the MCP tool name.
- Description — the tool's
description (falls back to the name), used by the LLM to decide when to call it.
- Parameters — the tool's
inputSchema (JSON Schema) becomes the SWAIG json_argument verbatim.
- State — always active; no
web_hook_url is needed.
When the LLM calls one of these functions, the platform bypasses the webhook path and issues a tools/call to the MCP server directly. The response is handled as follows:
- The
text parts of the response content[] are concatenated (newline-joined) and injected back to the LLM as the function result ({"response": "<text>"}).
- A response with
isError: true is logged as a warning; its text is still returned to the model.
- A JSON-RPC
error with no content surfaces the error message as the response.
- No usable result yields
{"response":"MCP tool returned no result."}.
Every MCP tool call is recorded in the function-call log with mcp_url, mcp_tool, and either mcp_response or mcp_error, so it shows up in the post-conversation payload like any other SWAIG call.
Overriding a discovered tool
If you declare a SWAIG function with the same name as an MCP tool, your definition wins: the platform keeps your description, parameters, fillers, and other settings, and only attaches the MCP routing (URL + tool name + headers) to it. This lets you customize how a tool is presented to the model — or add filler speech while it runs — while still executing it against the MCP server. (Routing fields are only attached if you haven't already set your own.)
Resources → global_data
Some MCP servers expose resources — named blobs of context (a customer record, a product catalog, a knowledge snippet). Set resources: true on a server entry to pull them in. The server must also advertise the resources capability, or the flag is ignored.
At session start the client calls resources/list, then resources/read for each resource, and merges the text into the agent's global_data:
- Key — the resource
name; if absent, the last path component of the resource URI.
- Value — if the resource text parses as JSON it is stored as a JSON object; otherwise it is stored as a string.
- Existing keys with the same name are replaced.
Once merged, resource data is available anywhere global_data is — including prompt and SWML variable expansion via ${global_data.<key>}.
URI templates
A resource advertised with a uriTemplate containing {placeholder} slots is resolved using resource_vars before the read. For example, with:
{ "resources": true, "resource_vars": { "customer_id": "8675309" } }
a template crm://customers/{customer_id} is read as crm://customers/8675309. Placeholders with no matching key in resource_vars are left intact.
Agent vs. sidecar
Both surfaces share the same MCP client and the same mcp_servers schema. The differences are where tools execute and where resources land:
|
AI agent (<ai>) |
AI sidecar (ai_sidecar) |
| Config location |
SWAIG.mcp_servers |
SWAIG.mcp_servers (same) |
| Discovery runs |
At session start, before the greeting |
At sidecar attach |
| Tool execution path |
The SWAIG action path, in the live conversation loop |
The sidecar tick dispatch |
Resources → global_data |
Agent's global_data |
Sidecar's global_data |
| Coexists with |
Your webhook functions, inline actions |
Your webhook functions and the reserved sidecar_skip built-in |
See sidecar.md for sidecar-specific behavior (event stream, ticks, the sidecar_skip tool, and the strict SWAIG parameter schema the sidecar enforces).
Limitations and constraints
- Transport is Streamable HTTP only. The client POSTs JSON-RPC and reads a JSON response body. Servers that require stdio, raw SSE streams, or WebSocket transports are not supported.
- Discovery is one-shot. Tools and resources are read at session start and not refreshed mid-call;
notifications/tools/list_changed is not handled. Restart the session to pick up changes.
- No retry/backoff. Each JSON-RPC request is a single attempt with a 10s connect / 30s total timeout. Webhook SWAIG functions retry; MCP tool calls do not.
- No platform-added auth. Unlike webhook SWAIG (which can carry SignalWire signatures), MCP requests carry only the
headers you supply. Use HTTPS endpoints and put credentials in headers.
- Sidecar schema strictness. When exposing MCP tools through the sidecar, the same strict SWAIG parameter validation applies as for hand-written functions — see sidecar.md.
Troubleshooting
| Symptom |
Likely cause / fix |
Log: MCP: failed to initialize <url>, skipping |
Server unreachable, returned non-200, or didn't advertise tools/resources. Verify the URL, headers, and that the endpoint speaks Streamable HTTP JSON-RPC. |
Tools discovered (registered tool … in logs) but the model never calls them |
The LLM decides from the tool description — make it specific. Also check the name didn't collide with an existing function that routes elsewhere. |
Resource data not in global_data |
The server must advertise the resources capability and the entry must set resources: true. Confirm the resource has text content. |
| Want to see the wire traffic |
Enable the debug parameter to log MCP POST / MCP RESP bodies. |
MCP Tool Servers
mod_openai includes a built-in Model Context Protocol (MCP) client. Point an AI agent (or an AI sidecar) at one or more remote MCP servers and the platform discovers their tools at call setup, exposes each tool to the LLM as a SWAIG function, and routes the model's tool calls to the server over JSON-RPC — no webhook of your own required.
It can also pull an MCP server's resources into the agent's
global_data, making remote context available to your prompt and SWML variable expansion.The same client serves both surfaces:
<ai>verb (mod_openai.c/app_config.c)ai_sidecarverb (see sidecar.md)Both read
mcp_serversfrom theSWAIGblock, and both run discovery once at session start.Overview
POST(Content-Type/Accept: application/json)global_dataheaders(e.g. a bearer token). No SignalWire request signature is added.A server that advertises neither tools nor resources is skipped. If one server fails to initialize, it is skipped with a warning and the remaining servers are still processed.
Configuration —
mcp_serversmcp_serversis an array inside theSWAIGblock, alongsidefunctions. Each entry describes one server.{ "ai": { "prompt": { "text": "You are a helpful support agent." }, "SWAIG": { "defaults": { "web_hook_url": "https://api.example.com/swaig" }, "functions": [ { "function": "...", "description": "...", "parameters": { } } ], "mcp_servers": [ { "url": "https://crm.example.com/mcp", "headers": { "Authorization": "Bearer ${global_data.crm_token}" }, "resources": true, "resource_vars": { "customer_id": "${global_data.customer_id}" } } ] } } }You can mix MCP servers and ordinary webhook
functionsin the sameSWAIGblock.defaults.web_hook_urlis not required for MCP tools — they bypass the webhook path entirely (see Tools as SWAIG functions).The sidecar uses the identical structure in its YAML config; see the
SWAIGblock in sidecar.md.Field reference
urlheaders{ "Header-Name": "value" }. Use for auth. Values may reference SWML variables (e.g.${global_data.crm_token}), expanded when the document is processed.resourcesfalseglobal_data. Requires the server to advertise theresourcescapability.resource_vars{placeholder}slots in resource URI templates (see Resources → global_data).Protocol flow
All traffic is JSON-RPC 2.0 over HTTP
POST. The client identifies itself assignalwire-ai-agent(version = the module's SWAIG version) and negotiates protocol version2025-06-18.At session start, per server:
initializetools,resources).notifications/initializedinitialize(fire-and-forget).resources/list→resources/readresources: true.tools/listDuring the conversation, per invocation:
tools/callIf
initializefails or the server advertises no usable capability, that server is skipped. There is no retry or backoff on any request; a slow or unreachable server adds latency at startup (discovery) or on the call itself (invocation).Tools as SWAIG functions
Every tool returned by
tools/listbecomes a SWAIG function:description(falls back to the name), used by the LLM to decide when to call it.inputSchema(JSON Schema) becomes the SWAIGjson_argumentverbatim.web_hook_urlis needed.When the LLM calls one of these functions, the platform bypasses the webhook path and issues a
tools/callto the MCP server directly. The response is handled as follows:textparts of the responsecontent[]are concatenated (newline-joined) and injected back to the LLM as the function result ({"response": "<text>"}).isError: trueis logged as a warning; its text is still returned to the model.errorwith no content surfaces the errormessageas the response.{"response":"MCP tool returned no result."}.Every MCP tool call is recorded in the function-call log with
mcp_url,mcp_tool, and eithermcp_responseormcp_error, so it shows up in the post-conversation payload like any other SWAIG call.Overriding a discovered tool
If you declare a SWAIG
functionwith the same name as an MCP tool, your definition wins: the platform keeps your description, parameters, fillers, and other settings, and only attaches the MCP routing (URL + tool name + headers) to it. This lets you customize how a tool is presented to the model — or add filler speech while it runs — while still executing it against the MCP server. (Routing fields are only attached if you haven't already set your own.)Resources →
global_dataSome MCP servers expose resources — named blobs of context (a customer record, a product catalog, a knowledge snippet). Set
resources: trueon a server entry to pull them in. The server must also advertise theresourcescapability, or the flag is ignored.At session start the client calls
resources/list, thenresources/readfor each resource, and merges the text into the agent'sglobal_data:name; if absent, the last path component of the resource URI.Once merged, resource data is available anywhere
global_datais — including prompt and SWML variable expansion via${global_data.<key>}.URI templates
A resource advertised with a
uriTemplatecontaining{placeholder}slots is resolved usingresource_varsbefore the read. For example, with:{ "resources": true, "resource_vars": { "customer_id": "8675309" } }a template
crm://customers/{customer_id}is read ascrm://customers/8675309. Placeholders with no matching key inresource_varsare left intact.Agent vs. sidecar
Both surfaces share the same MCP client and the same
mcp_serversschema. The differences are where tools execute and where resources land:<ai>)ai_sidecar)SWAIG.mcp_serversSWAIG.mcp_servers(same)global_dataglobal_dataglobal_datafunctions, inline actionsfunctionsand the reservedsidecar_skipbuilt-inSee sidecar.md for sidecar-specific behavior (event stream, ticks, the
sidecar_skiptool, and the strict SWAIG parameter schema the sidecar enforces).Limitations and constraints
notifications/tools/list_changedis not handled. Restart the session to pick up changes.headersyou supply. Use HTTPS endpoints and put credentials inheaders.Troubleshooting
MCP: failed to initialize <url>, skippingtools/resources. Verify the URL,headers, and that the endpoint speaks Streamable HTTP JSON-RPC.registered tool …in logs) but the model never calls themdescription— make it specific. Also check the name didn't collide with an existing function that routes elsewhere.global_dataresourcescapability and the entry must setresources: true. Confirm the resource has text content.debugparameter to logMCP POST/MCP RESPbodies.