Skip to content

AI / AI Sidecar mcp_servers details. #347

@briankwest

Description

@briankwest

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/listresources/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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions