Packages

A spec-compliant Open Responses server for Elixir and Phoenix, with pluggable provider adapters and first-class streaming.

Current section

Files

Jump to
open_responses lib open_responses mcp connection.ex
Raw

lib/open_responses/mcp/connection.ex

defmodule OpenResponses.MCP.Connection do
@moduledoc """
Connection to a Model Context Protocol (MCP) server.
One `Connection` process is started per MCP server per request. It fetches
the server's tool list on startup and proxies tool calls on behalf of the
agentic loop.
## Usage
Per-request MCP servers are specified in the API request body:
```json
{
"model": "gpt-4o",
"tools": [{"type": "mcp", "server_url": "https://my-mcp.example.com"}],
"input": [{"role": "user", "content": "Search for recent news"}]
}
```
Pre-configured servers available to all requests can be set in config:
```elixir
config :open_responses, :mcp_servers, [
%{
name: "docs",
url: "https://docs-mcp.example.com",
headers: [{"authorization", "Bearer \#{System.get_env("MCP_TOKEN")}"}]
}
]
```
See the [MCP Integration guide](mcp_integration.html) for the full protocol
and authentication details.
"""
use GenServer
defstruct [:endpoint, :headers, :tools]
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) do
GenServer.start_link(__MODULE__, opts)
end
@spec list_tools(pid()) :: {:ok, list()} | {:error, term()}
def list_tools(pid) do
GenServer.call(pid, :list_tools, 10_000)
end
@spec call_tool(pid(), binary(), map()) :: {:ok, String.t()} | {:error, term()}
def call_tool(pid, name, arguments) do
GenServer.call(pid, {:call_tool, name, arguments}, 30_000)
end
@impl GenServer
def init(opts) do
endpoint = Keyword.fetch!(opts, :endpoint)
headers = Keyword.get(opts, :headers, [])
state = %__MODULE__{
endpoint: endpoint,
headers: headers,
tools: []
}
{:ok, state, {:continue, :initialize}}
end
@impl GenServer
def handle_continue(:initialize, state) do
case fetch_tools(state) do
{:ok, tools} -> {:noreply, %{state | tools: tools}}
_ -> {:noreply, state}
end
end
@impl GenServer
def handle_call(:list_tools, _from, state) do
{:reply, {:ok, state.tools}, state}
end
@impl GenServer
def handle_call({:call_tool, name, arguments}, _from, state) do
result = invoke_tool(state, name, arguments)
{:reply, result, state}
end
defp fetch_tools(state) do
case Req.post(
"#{state.endpoint}/tools/list",
headers: state.headers,
json: %{},
receive_timeout: 10_000
) do
{:ok, %{status: 200, body: %{"tools" => tools}}} -> {:ok, tools}
{:ok, %{status: 200, body: body}} when is_list(body) -> {:ok, body}
_ -> {:ok, []}
end
end
defp invoke_tool(state, name, arguments) do
case Req.post(
"#{state.endpoint}/tools/call",
headers: state.headers,
json: %{name: name, arguments: arguments},
receive_timeout: 30_000
) do
{:ok, %{status: 200, body: %{"content" => content}}} ->
{:ok, extract_content(content)}
{:ok, %{status: status, body: body}} ->
{:error, %{status: status, body: body}}
{:error, reason} ->
{:error, reason}
end
end
defp extract_content(content) when is_list(content) do
content
|> Enum.filter(&(&1["type"] == "text"))
|> Enum.map_join("\n", & &1["text"])
end
defp extract_content(content) when is_binary(content), do: content
defp extract_content(_), do: ""
end