MCPEx.Transport.Http (MCPEx v0.1.0)
Implementation of the MCP transport over HTTP with Server-Sent Events (SSE).
This transport follows the Model Context Protocol (MCP) HTTP transport specification:
- SSE Connection: The client connects to an SSE endpoint (e.g.,
https://example.com/sse
) to receive server events - Message Endpoint Notification: The server sends a special
endpoint
event via SSE with the following format:event: endpoint data: /messages?sessionId=abc123
- Message Endpoint Construction: The client constructs the full message endpoint URL by using the same origin as the SSE connection and appending the received path and query string (e.g.,
https://example.com/messages?sessionId=abc123
) - Message Exchange: The client sends all JSON-RPC messages as HTTP POST requests to this constructed message endpoint
This approach allows the server to dynamically configure where messages should be sent, supporting load balancing and session routing while maintaining the same origin. The transport manages this by:
- Establishing and maintaining the SSE connection
- Processing the endpoint event to configure the message endpoint
- Sending all client messages to the configured endpoint
- Handling server messages received via the SSE stream
This transport follows Req's API patterns, making it easy to test with custom HTTP clients.
Testing
There are two main approaches for testing code that uses this transport:
Using a custom HTTP client (recommended):
# Define a mock HTTP client for testing defmodule MockHttpClient do def post(_url, json_data, _headers, _timeout) do # Verify request data assert json_data.method == "test" # Return a mock response {:ok, %{status: 200, body: %{"result" => "success"}}} end end # Start the transport with the mock client {:ok, transport} = Http.start_link( url: "https://example.com/sse", http_client: MockHttpClient ) # Simulate the SSE message endpoint notification # Note: only the path is sent, the client constructs the full URL send(transport, {:sse_endpoint, "/messages?sessionId=abc123"}) # Test sending a message {:ok, response} = Http.send_message(transport, %{method: "test"}) assert response.status == 200 assert response.body["result"] == "success"
Using Req.Test (for testing without process boundaries):
# Set up Req.Test Req.Test.stub(:test_stub, fn conn -> assert conn.body["method"] == "test" %{conn | status: 200, body: %{"result" => "success"}} end) # Create a request with the test plug req = Http.new( url: "https://example.com/sse", plug: {Req.Test, :test_stub} ) # This approach works for direct Req calls but has limitations # with the GenServer process boundary in the transport
See product/docs/010.01.req-usage.md for more detailed examples and patterns.
Summary
Functions
Returns a specification to start this module under a supervisor.
Closes the transport connection.
Creates a new HTTP transport request configuration.
Sends a message to the server via HTTP POST.
Starts a new HTTP transport as a linked process.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor
.
@spec close(pid()) :: :ok
Closes the transport connection.
Parameters
pid
- The transport process
Returns
:ok
- The connection was closed successfully
@spec new(keyword()) :: Req.Request.t()
Creates a new HTTP transport request configuration.
Options
:url
- The URL of the MCP server SSE endpoint (required):headers
- Additional HTTP headers to include in requests:timeout
- Request timeout in milliseconds (default: 30000):http_client
- Optional custom HTTP client module for testing (see example in tests)- Plus any other options from Req.new/1
Returns
%Req.Request{}
- A request configuration for the HTTP transport
Examples
iex> req = MCPEx.Transport.Http.new(url: "https://example.com/sse")
%Req.Request{}
Sends a message to the server via HTTP POST.
Parameters
pid
- The transport processmessage
- The message to send (string or map)
Returns
{:ok, response}
- The message was sent successfully with the full response{:error, reason}
- Failed to send the message
Examples
iex> Http.send_message(pid, ~s({"jsonrpc":"2.0","method":"test"}))
{:ok, %{status: 200, body: %{...}}}
iex> Http.send_message(pid, %{jsonrpc: "2.0", method: "test"})
{:ok, %{status: 200, body: %{...}}}
Starts a new HTTP transport as a linked process.
Options
:request
- A Req.Request struct created with Http.new/1:http_client
- Optional custom HTTP client module for testing- Or any option accepted by Http.new/1
Returns
{:ok, pid}
- The transport was started successfully{:error, reason}
- Failed to start the transport
Examples
# Using Http.new
iex> req = MCPEx.Transport.Http.new(url: "https://example.com/sse")
iex> {:ok, pid} = MCPEx.Transport.Http.start_link(request: req)
# Or directly with options
iex> {:ok, pid} = MCPEx.Transport.Http.start_link(url: "https://example.com/sse")