Packages
fnord
0.9.36
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/mcp/transport.ex
defmodule MCP.Transport do
@moduledoc "Convert MCP server config into Hermes transport tuples and helpers for OAuth header injection"
@typedoc "Hermes transport tuple"
@type t ::
{:stdio, keyword()}
| {:streamable_http, keyword()}
| {:websocket, keyword()}
@doc "Convert server config map into a Hermes transport tuple"
@spec map(String.t(), map()) :: {atom(), keyword()}
def map(_server, %{"transport" => "stdio"} = cfg) do
{:stdio,
[
command: MCP.STDIOWrapper.script_path!(),
args: [cfg["command"] | cfg["args"] || []],
env: cfg["env"] || %{}
]}
end
def map(server, %{"transport" => "http"} = cfg) do
headers = merge_oauth_header(server, cfg, cfg["headers"] || %{})
{base_url, mcp_path} = split_endpoint(cfg)
{:streamable_http,
[
base_url: base_url,
mcp_path: mcp_path,
headers: headers
]}
end
def map(server, %{"transport" => "websocket"} = cfg) do
headers = merge_oauth_header(server, cfg, cfg["headers"] || %{})
{:websocket,
[
base_url: cfg["base_url"],
headers: headers
]}
end
def map(server, %{"transport" => transport} = _cfg) do
UI.error("""
Invalid transport '#{transport}' for MCP server '#{server}'.
Valid transports are: "stdio", "http", "websocket"
This error often occurs with outdated config values (e.g., "streamable_http" from older versions).
To fix:
1. Manually edit ~/.fnord/settings.json and change the transport value to a valid one
2. Or remove the server config and re-add it: fnord config mcp remove #{server} && fnord config mcp add #{server} [options]
""")
raise ArgumentError, "Invalid transport '#{transport}' for MCP server '#{server}'"
end
def map(server, cfg) do
UI.error("""
Missing or invalid transport configuration for MCP server '#{server}'.
Config received: #{inspect(cfg)}
To fix:
1. Manually edit ~/.fnord/settings.json to add a valid "transport" field
2. Or remove the server config and re-add it: fnord config mcp remove #{server} && fnord config mcp add #{server} [options]
""")
raise ArgumentError, "Missing transport configuration for MCP server '#{server}'"
end
# Hermes builds the request URL as URI.append_path(base_url, mcp_path),
# with mcp_path defaulting to "/". A base_url that already carries the
# endpoint path (e.g. https://mcp.linear.app/mcp) would be mangled to
# ".../mcp/" - and servers route the trailing-slash form as a distinct,
# usually nonexistent, path. So: an explicit mcp_path passes through with
# Hermes's append semantics intact (base_url is the origin, mcp_path the
# endpoint); without one, the configured base_url is treated as the
# complete endpoint URL and split into origin + path so the request hits
# exactly the URL the user configured.
defp split_endpoint(cfg) do
base_url = cfg["base_url"]
case Map.get(cfg, "mcp_path") do
path when is_binary(path) ->
{base_url, path}
_ ->
uri = URI.parse(base_url)
case uri.path do
path when is_binary(path) and path != "" ->
{URI.to_string(%{uri | path: nil}), path}
_ ->
{base_url, "/"}
end
end
end
# Inject OAuth Authorization header if server has oauth config and valid credentials
defp merge_oauth_header(server, cfg, base_headers) do
case Map.get(cfg, "oauth") do
nil ->
base_headers
oauth_cfg when is_map(oauth_cfg) ->
case MCP.OAuth2.Bridge.authorization_header(server, cfg) do
{:ok, oauth_headers} ->
# Convert list of tuples to map and merge with base headers
oauth_map = Map.new(oauth_headers)
Map.merge(base_headers, oauth_map)
{:error, reason} when reason in [:no_credentials, :not_found] ->
UI.warn(
"MCP server '#{server}' has OAuth config but no credentials. To fix",
"fnord config mcp login #{server}"
)
base_headers
{:error, :no_refresh_token} ->
UI.warn(
"MCP server '#{server}' has expired credentials with no refresh token. To fix",
"fnord config mcp login #{server}"
)
base_headers
{:error, reason} ->
UI.warn("Failed to get OAuth token for MCP server '#{server}'", reason)
base_headers
end
end
end
end