{"slug":"agents-mcp-gateway","title":"MCP Gateway — Bridge MCP Servers to SignalWire SWAIG","tags":["signalwire","agents-sdk","mcp","mcp-gateway","tool-federation"],"agent_summary":"MCPGateway is an HTTP service that exposes MCP-protocol tool servers as SignalWire SWAIG functions. Covers MCPGateway, MCPManager, MCPClient, the config.json shape, session lifecycle, auth/SSL, and the agent-side AgentBase.add_mcp_server pattern.","trigger_phrases":["MCP gateway SignalWire","expose MCP server as SWAIG","model context protocol SignalWire","MCPClient call_tool","AgentBase add_mcp_server","MCP federation voice AI"],"runnable":true,"markdown":"\n# MCP Gateway\n\n`MCPGateway` is an HTTP/HTTPS service that bridges Model Context Protocol (MCP) servers to SignalWire SWAIG functions. It manages sessions, handles auth, and translates between MCP's JSON-RPC tool-call protocol and SignalWire's SWAIG webhook protocol.\n\nUse the gateway when you want to expose tools from one or more MCP servers — Anthropic's reference tooling, an org-wide MCP federation, or custom-built MCP services — as SWAIG functions an AI agent can call mid-conversation.\n\n## Two integration shapes\n\n| Shape | Use when |\n|---|---|\n| **Standalone MCP Gateway service** | You have multiple MCP servers, multiple agents, and want one bridge. |\n| **`AgentBase.add_mcp_server()`** | One agent, one or two MCP servers, no shared infra. |\n\n## Standalone gateway\n\n```python\nfrom signalwire.mcp_gateway import MCPGateway\n\ngateway = MCPGateway(\"config.json\")\ngateway.run()\n```\n\nThe gateway loads config, spawns each configured MCP server as a subprocess, exposes their tools at authenticated HTTP routes, and translates SWAIG webhook calls into MCP JSON-RPC requests.\n\n### `config.json` shape\n\n```json\n{\n  \"services\": {\n    \"todo\": {\n      \"command\": [\"python3\", \"todo_mcp.py\"],\n      \"description\": \"Todo list management\",\n      \"enabled\": true\n    },\n    \"github\": {\n      \"command\": [\"node\", \"github-mcp.js\"],\n      \"description\": \"GitHub repo operations\",\n      \"enabled\": true,\n      \"env\": { \"GITHUB_TOKEN\": \"${GITHUB_TOKEN}\" }\n    }\n  },\n  \"auth\": {\n    \"basic\": { \"username\": \"signalwire\", \"password\": \"${MCP_GATEWAY_PASSWORD}\" }\n  },\n  \"ssl\": {\n    \"enabled\": true,\n    \"cert\": \"/etc/ssl/cert.pem\",\n    \"key\": \"/etc/ssl/key.pem\"\n  }\n}\n```\n\n`${VAR}` placeholders are env-var-substituted at load time.\n\n### Auth options\n\n- `basic` — HTTP Basic auth with username/password.\n- `bearer` — Bearer token (configured under `auth.bearer`).\n- `api_key` — Header-based API key (`auth.api_key`).\n\nAll three can be enabled simultaneously. Each request just needs to satisfy any one.\n\n## Component classes\n\n### MCPManager\n\nSpawns, tracks, and shuts down MCP server subprocesses. The gateway owns one `MCPManager`.\n\n```python\nfrom signalwire.mcp_gateway import MCPManager\n\nconfig = {\n    \"services\": {\n        \"todo\": {\"command\": [\"python3\", \"todo_mcp.py\"],\n                 \"description\": \"Todo list\", \"enabled\": True}\n    }\n}\nmanager = MCPManager(config)\nclient = manager.create_client(\"todo\")\ntools = client.get_tools()\nresult = client.call_tool(\"add_todo\", {\"text\": \"Buy groceries\"})\nclient.stop()\nmanager.shutdown()\n```\n\n### MCPClient\n\nManages a single MCP server subprocess: start, JSON-RPC init, call tools, stop with cleanup. Normally created by `MCPManager.create_client()`, not directly.\n\nKey methods:\n\n| Method | Purpose |\n|---|---|\n| `start()` | Spawn process, init MCP session, fetch tool list. Returns `True` on success. |\n| `stop()` | Graceful JSON-RPC shutdown → SIGTERM → SIGKILL fallback. Cleans up sandbox. |\n| `call_tool(name, args)` | Invoke a tool, return result dict. |\n| `call_method(method, params)` | Generic JSON-RPC call (e.g., `\"tools/list\"`). 30s timeout. |\n| `get_tools()` | Return cached tool definitions. |\n\n### SessionManager\n\nTracks MCP session lifecycle. Sessions auto-expire after a configurable idle timeout. The gateway uses sessions to multiplex multiple concurrent SWAIG callers against the same MCP server subprocess.\n\n## Agent-side: `AgentBase.add_mcp_server()`\n\nIf you only have one agent talking to one MCP server, skip the standalone gateway:\n\n```python\nfrom signalwire import AgentBase\n\nagent = AgentBase(name=\"assistant\")\n\nagent.add_mcp_server(\n    name=\"todo\",\n    command=[\"python3\", \"todo_mcp.py\"],\n    env={\"TODO_DB\": \"/var/data/todos.db\"},\n)\n\n# Tools from the MCP server are now auto-registered as SWAIG functions.\n```\n\nThe agent spawns the MCP subprocess at boot, registers every MCP tool as a SWAIG function under the namespace `mcp_{server_name}_{tool_name}`, and tears down the subprocess when the agent stops.\n\nFor agents deployed serverless, prefer the **standalone gateway** — serverless cold starts can't host long-lived MCP subprocesses.\n\n## Sandbox config\n\nEach MCP service runs in a sandboxed working directory. Defaults are safe (no host filesystem access, ephemeral tmp). Override per-service if a tool needs persistent state:\n\n```json\n{\n  \"services\": {\n    \"filebrowser\": {\n      \"command\": [\"python3\", \"fb_mcp.py\"],\n      \"enabled\": true,\n      \"sandbox_config\": {\n        \"working_directory\": \"/var/data/sandbox\",\n        \"ephemeral\": false\n      }\n    }\n  }\n}\n```\n\n## End-to-end SWAIG call flow\n\n1. SignalWire AI agent decides to invoke a function (e.g., `mcp_todo_add_todo`).\n2. SignalWire POSTs the SWAIG webhook to the gateway URL.\n3. Gateway authenticates the request, finds the session, locates the MCP client.\n4. Gateway translates the SWAIG payload into an MCP `tools/call` JSON-RPC request.\n5. MCP server executes the tool and returns a result.\n6. Gateway formats the result as a SWAIG response and returns it to SignalWire.\n7. AI speaks the result to the caller.\n\n## CLI\n\nThe package ships with a `mcp-gateway` CLI for running the gateway in production:\n\n```bash\nmcp-gateway --config config.json --port 8080\nmcp-gateway --config config.json --ssl-cert cert.pem --ssl-key key.pem\n```\n\n## Anti-patterns\n\n- Running the gateway and the agent in the same process — gateway needs persistent subprocesses; serverless agents can't host that.\n- Using `MCPClient` directly instead of via `MCPManager` — you lose session tracking and shutdown ordering.\n- No auth on a public-facing gateway — anyone on the internet can run your MCP tools.\n- Setting `sandbox_config.ephemeral: false` for stateless tools — leaks files between calls.\n- Embedding secrets directly in `config.json` — use `${ENV_VAR}` substitution.\n\n## See also\n\n- [Python Agents SDK](/topic/signalwire-python-agents-sdk)\n- [Agents skills system](/topic/agents-skills-system)\n- [SWAIG functions](/topic/swaig-functions)\n","html":"<h1>MCP Gateway</h1>\n<p><code>MCPGateway</code> is an HTTP/HTTPS service that bridges Model Context Protocol (MCP) servers to SignalWire SWAIG functions. It manages sessions, handles auth, and translates between MCP's JSON-RPC tool-call protocol and SignalWire's SWAIG webhook protocol.</p>\n<p>Use the gateway when you want to expose tools from one or more MCP servers — Anthropic's reference tooling, an org-wide MCP federation, or custom-built MCP services — as SWAIG functions an AI agent can call mid-conversation.</p>\n<h2>Two integration shapes</h2>\n<p>| Shape | Use when |\n|---|---|\n| <strong>Standalone MCP Gateway service</strong> | You have multiple MCP servers, multiple agents, and want one bridge. |\n| <strong><code>AgentBase.add_mcp_server()</code></strong> | One agent, one or two MCP servers, no shared infra. |</p>\n<h2>Standalone gateway</h2>\n<pre><code class=\"language-python\">from signalwire.mcp_gateway import MCPGateway\n\ngateway = MCPGateway(\"config.json\")\ngateway.run()\n</code></pre>\n<p>The gateway loads config, spawns each configured MCP server as a subprocess, exposes their tools at authenticated HTTP routes, and translates SWAIG webhook calls into MCP JSON-RPC requests.</p>\n<h3><code>config.json</code> shape</h3>\n<pre><code class=\"language-json\">{\n  \"services\": {\n    \"todo\": {\n      \"command\": [\"python3\", \"todo_mcp.py\"],\n      \"description\": \"Todo list management\",\n      \"enabled\": true\n    },\n    \"github\": {\n      \"command\": [\"node\", \"github-mcp.js\"],\n      \"description\": \"GitHub repo operations\",\n      \"enabled\": true,\n      \"env\": { \"GITHUB_TOKEN\": \"${GITHUB_TOKEN}\" }\n    }\n  },\n  \"auth\": {\n    \"basic\": { \"username\": \"signalwire\", \"password\": \"${MCP_GATEWAY_PASSWORD}\" }\n  },\n  \"ssl\": {\n    \"enabled\": true,\n    \"cert\": \"/etc/ssl/cert.pem\",\n    \"key\": \"/etc/ssl/key.pem\"\n  }\n}\n</code></pre>\n<p><code>${VAR}</code> placeholders are env-var-substituted at load time.</p>\n<h3>Auth options</h3>\n<ul>\n<li><code>basic</code> — HTTP Basic auth with username/password.</li>\n<li><code>bearer</code> — Bearer token (configured under <code>auth.bearer</code>).</li>\n<li><code>api_key</code> — Header-based API key (<code>auth.api_key</code>).</li>\n</ul>\n<p>All three can be enabled simultaneously. Each request just needs to satisfy any one.</p>\n<h2>Component classes</h2>\n<h3>MCPManager</h3>\n<p>Spawns, tracks, and shuts down MCP server subprocesses. The gateway owns one <code>MCPManager</code>.</p>\n<pre><code class=\"language-python\">from signalwire.mcp_gateway import MCPManager\n\nconfig = {\n    \"services\": {\n        \"todo\": {\"command\": [\"python3\", \"todo_mcp.py\"],\n                 \"description\": \"Todo list\", \"enabled\": True}\n    }\n}\nmanager = MCPManager(config)\nclient = manager.create_client(\"todo\")\ntools = client.get_tools()\nresult = client.call_tool(\"add_todo\", {\"text\": \"Buy groceries\"})\nclient.stop()\nmanager.shutdown()\n</code></pre>\n<h3>MCPClient</h3>\n<p>Manages a single MCP server subprocess: start, JSON-RPC init, call tools, stop with cleanup. Normally created by <code>MCPManager.create_client()</code>, not directly.</p>\n<p>Key methods:</p>\n<p>| Method | Purpose |\n|---|---|\n| <code>start()</code> | Spawn process, init MCP session, fetch tool list. Returns <code>True</code> on success. |\n| <code>stop()</code> | Graceful JSON-RPC shutdown → SIGTERM → SIGKILL fallback. Cleans up sandbox. |\n| <code>call_tool(name, args)</code> | Invoke a tool, return result dict. |\n| <code>call_method(method, params)</code> | Generic JSON-RPC call (e.g., <code>\"tools/list\"</code>). 30s timeout. |\n| <code>get_tools()</code> | Return cached tool definitions. |</p>\n<h3>SessionManager</h3>\n<p>Tracks MCP session lifecycle. Sessions auto-expire after a configurable idle timeout. The gateway uses sessions to multiplex multiple concurrent SWAIG callers against the same MCP server subprocess.</p>\n<h2>Agent-side: <code>AgentBase.add_mcp_server()</code></h2>\n<p>If you only have one agent talking to one MCP server, skip the standalone gateway:</p>\n<pre><code class=\"language-python\">from signalwire import AgentBase\n\nagent = AgentBase(name=\"assistant\")\n\nagent.add_mcp_server(\n    name=\"todo\",\n    command=[\"python3\", \"todo_mcp.py\"],\n    env={\"TODO_DB\": \"/var/data/todos.db\"},\n)\n\n# Tools from the MCP server are now auto-registered as SWAIG functions.\n</code></pre>\n<p>The agent spawns the MCP subprocess at boot, registers every MCP tool as a SWAIG function under the namespace <code>mcp_{server_name}_{tool_name}</code>, and tears down the subprocess when the agent stops.</p>\n<p>For agents deployed serverless, prefer the <strong>standalone gateway</strong> — serverless cold starts can't host long-lived MCP subprocesses.</p>\n<h2>Sandbox config</h2>\n<p>Each MCP service runs in a sandboxed working directory. Defaults are safe (no host filesystem access, ephemeral tmp). Override per-service if a tool needs persistent state:</p>\n<pre><code class=\"language-json\">{\n  \"services\": {\n    \"filebrowser\": {\n      \"command\": [\"python3\", \"fb_mcp.py\"],\n      \"enabled\": true,\n      \"sandbox_config\": {\n        \"working_directory\": \"/var/data/sandbox\",\n        \"ephemeral\": false\n      }\n    }\n  }\n}\n</code></pre>\n<h2>End-to-end SWAIG call flow</h2>\n<ol>\n<li>SignalWire AI agent decides to invoke a function (e.g., <code>mcp_todo_add_todo</code>).</li>\n<li>SignalWire POSTs the SWAIG webhook to the gateway URL.</li>\n<li>Gateway authenticates the request, finds the session, locates the MCP client.</li>\n<li>Gateway translates the SWAIG payload into an MCP <code>tools/call</code> JSON-RPC request.</li>\n<li>MCP server executes the tool and returns a result.</li>\n<li>Gateway formats the result as a SWAIG response and returns it to SignalWire.</li>\n<li>AI speaks the result to the caller.</li>\n</ol>\n<h2>CLI</h2>\n<p>The package ships with a <code>mcp-gateway</code> CLI for running the gateway in production:</p>\n<pre><code class=\"language-bash\">mcp-gateway --config config.json --port 8080\nmcp-gateway --config config.json --ssl-cert cert.pem --ssl-key key.pem\n</code></pre>\n<h2>Anti-patterns</h2>\n<ul>\n<li>Running the gateway and the agent in the same process — gateway needs persistent subprocesses; serverless agents can't host that.</li>\n<li>Using <code>MCPClient</code> directly instead of via <code>MCPManager</code> — you lose session tracking and shutdown ordering.</li>\n<li>No auth on a public-facing gateway — anyone on the internet can run your MCP tools.</li>\n<li>Setting <code>sandbox_config.ephemeral: false</code> for stateless tools — leaks files between calls.</li>\n<li>Embedding secrets directly in <code>config.json</code> — use <code>${ENV_VAR}</code> substitution.</li>\n</ul>\n<h2>See also</h2>\n<ul>\n<li><a href=\"/topic/signalwire-python-agents-sdk\">Python Agents SDK</a></li>\n<li><a href=\"/topic/agents-skills-system\">Agents skills system</a></li>\n<li><a href=\"/topic/swaig-functions\">SWAIG functions</a></li>\n</ul>\n"}