Transport Protocols
ModelContextProtocol.jl supports two transport protocols for communication between MCP servers and clients.
stdio Transport
The stdio transport uses standard input and output streams for communication. This is the simplest transport method and works well for command-line applications and process-to-process communication.
Basic Usage
using ModelContextProtocol
# Create a server with stdio transport (default)
server = mcp_server(
name = "my-server",
version = "1.0.0",
tools = [my_tool]
)
# Start the server (uses stdio by default)
start!(server)The server will read JSON-RPC messages from stdin and write responses to stdout.
Streamable HTTP Transport
The Streamable HTTP transport implements the MCP protocol over HTTP with Server-Sent Events (SSE) support. This enables web-based clients and provides real-time streaming capabilities.
Basic HTTP Server
using ModelContextProtocol
# Create HTTP transport (the legacy protocol version is negotiated per client;
# the server speaks 2025-11-25 down to 2024-11-05, and serves modern-era
# 2026-07-28 requests on the same endpoint)
transport = HttpTransport(
host = "127.0.0.1",
port = 3000
)
# Create server
server = mcp_server(
name = "http-server",
version = "1.0.0",
tools = [my_tool]
)
# Set transport and start
server.transport = transport
ModelContextProtocol.connect(transport)
start!(server)Configuration Options
The HttpTransport constructor accepts the following keyword arguments, shown with their real defaults:
transport = HttpTransport(
host = "127.0.0.1", # Bind address (loopback by default)
port = 8080, # Port number
endpoint = "/", # Base endpoint path
allowed_origins = String[], # Extra Origins accepted by the DNS-rebinding guard
allowed_hosts = String[], # Extra Host hostnames accepted by that guard
protocol_version = LATEST_PROTOCOL_VERSION, # Advertised legacy version ("2025-11-25")
session_required = false, # Require a session on non-initialize legacy requests
auth = nothing, # Optional AuthMiddleware (see Authentication below)
resource_metadata = nothing, # Optional RFC 9728 Protected Resource Metadata
sse_keepalive_secs = 15.0 # Idle interval between SSE keepalive comments
)sse_keepalive_secs must be finite and positive — the periodic write is the only way to notice a silently-dead SSE peer, so Inf (which would disable detection) and non-positive values are rejected at construction.
protocol_version is the advertised legacy version: 2025-11-25 is the newest version reachable through initialize, negotiated per client down to 2024-11-05, and response headers echo the negotiated value. It is not the newest protocol the server speaks — modern-era clients select 2026-07-28 per request through the params _meta key io.modelcontextprotocol/protocolVersion, with no handshake and no session; see The Modern Era (2026-07-28).
SSE is always available via GET with Accept: text/event-stream — there is no switch.
Session Management
HTTP transport uses session-based communication for security and state tracking:
- Initialization: Client sends initialization request
- Session Creation: Server responds with
Mcp-Session-Idheader - Subsequent Requests: Client includes session ID in
Mcp-Session-Idheader
# Initialize and get session ID
curl -X POST http://localhost:3000/ \
-H 'Content-Type: application/json' \
-H 'MCP-Protocol-Version: 2025-11-25' \
-d '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}' \
-i
# Use session ID in subsequent requests
curl -X POST http://localhost:3000/ \
-H 'Content-Type: application/json' \
-H 'MCP-Protocol-Version: 2025-11-25' \
-H 'Mcp-Session-Id: <session-id-from-response>' \
-d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":2}'Server-Sent Events (SSE)
Notifications related to an in-flight request — notifications/progress from a ctx-aware tool, notifications/message log events emitted during handling — are delivered on that request's own response: when they occur, the POST response is a text/event-stream carrying the notifications followed by the final JSON-RPC response. A request that emits nothing gets a plain application/json response. Clients must accept both content types (send Accept: application/json, text/event-stream); if a client accepts only JSON, request-scoped notifications are dropped and it receives the plain response.
Out-of-band notifications — background MCP Tasks status updates (notifications/tasks/status) and anything emitted outside request handling — flow on a standalone stream that clients open with a GET request:
curl -N -H 'Accept: text/event-stream' http://127.0.0.1:3000/That GET stream is legacy-only. The modern era removed it along with resources/subscribe: a client instead issues subscriptions/listen, a long-lived POST whose response stream carries the notification types it opted into (list-changed events, resource URIs, task ids). A GET that declares a modern MCP-Protocol-Version gets 405 Method Not Allowed. See The Modern Era (2026-07-28).
Modern-era requests (2026-07-28)
Modern-era clients need no initialize and no session: each request carries io.modelcontextprotocol/protocolVersion in its params _meta and is answered as a self-contained exchange on the same endpoint. Over HTTP, SEP-2243 additionally requires standard headers that mirror the body:
MCP-Protocol-VersionandMcp-Methodon every modern request, matching the body's_metaversion and itsmethod.Mcp-Nameon the methods that name a primary target —tools/callandprompts/get(mirroringparams.name),resources/read(params.uri), andtasks/get,tasks/update,tasks/cancel(params.taskId). A value that is not header-safe may be wrapped in the=?base64?…?=sentinel, which the server decodes before comparing.
Header names are matched case-insensitively; a duplicated header, one carrying non-visible-ASCII bytes, or any value that disagrees with the body is a -32020 HeaderMismatch returned with HTTP 400. An unknown method is -32601 with HTTP 404, and an unsupported protocol version is -32022 with HTTP 400. Mcp-Session-Id is ignored on modern requests and sessions are never minted for them.
curl -X POST http://127.0.0.1:3000/ \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/list' \
-d '{"jsonrpc":"2.0","method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}},"id":1}'Security Features
Authentication (OAuth Resource Server)
The HTTP transport can require a bearer token on every request. Validators include GitHub tokens (validated against the GitHub API with optional allowlist/organization checks), JWTs verified against a JWKS endpoint, JWT claims, and RFC 7662 token introspection:
using ModelContextProtocol
auth = create_github_auth(
allowed_users = ["alice", "bob"], # empty list = any authenticated GitHub user
required_org = "MyLab", # optional organization gate
)
meta = create_github_resource_metadata("https://mcp.example.org")
transport = HttpTransport(host = "0.0.0.0", port = 3000,
auth = auth, resource_metadata = meta)For JWTs issued by an external authorization server (Keycloak, Auth0, etc.), use JWKSValidator — it verifies token signatures against the server's published JSON Web Key Set (RFC 7517) and then applies the standard claim checks (issuer, audience, expiry, scopes), all fail-closed:
auth = create_auth_middleware(
OAuthConfig(
issuer = "https://auth.example.org/realms/lab",
audience = "https://mcp.example.org",
),
validator = JWKSValidator("https://auth.example.org/realms/lab/protocol/openid-connect/certs"),
)Keys are fetched lazily and re-fetched on unknown key ids (rotation), rate-limited to one fetch per refresh_interval_seconds (default 300) so attacker-supplied kid values cannot hammer the JWKS endpoint. The alg allowlist defaults to the RSA family (RS256/RS384/RS512) and rejects alg=none outright.
Clients send Authorization: Bearer <token>; unauthorized requests get 401/403 with an RFC 6750 WWW-Authenticate header, and discovery metadata is served at /.well-known/oauth-protected-resource (RFC 9728). Tool handlers can read the verified identity by accepting the request context: handler = (args, ctx) -> ... and using ctx.authenticated_user. Note: JWTValidator checks claims only (no signature verification); prefer JWKSValidator for tokens from external issuers, or the GitHub / introspection validators when tokens must be verified against an authority.
DNS-Rebinding Protection (Host/Origin Validation)
A loopback-bound server without bearer auth automatically rejects requests whose Host or Origin header is neither local (localhost, 127.0.0.1, [::1]) nor allowlisted, with 403 Forbidden. This blocks DNS-rebinding attacks, where a malicious website resolves its own domain to 127.0.0.1 and drives your local server from the victim's browser. Legitimate local clients are unaffected — they send a loopback Host — and enabling auth disables the guard (a browser cannot attach the bearer token, and authenticated deployments commonly sit behind a reverse proxy that forwards a public Host).
Open specific holes with the allowlists:
transport = HttpTransport(
allowed_hosts = ["mcp.example.org"], # extra Host hostnames (reverse proxy)
allowed_origins = [
"http://localhost:3000",
"https://my-app.com" # extra Origins (browser clients)
]
)Session Validation
Sessions provide security and state isolation:
# Require valid sessions for all non-initialization requests
transport = HttpTransport(session_required = true)
# Disable session requirement (less secure)
transport = HttpTransport(session_required = false)Error Handling
The HTTP transport returns appropriate HTTP status codes:
200 OK- Successful requests with JSON response202 Accepted- Notification requests (no response body)400 Bad Request- Missing required session ID, malformed requests, and modern-era protocol violations (SEP-2243 header mismatch-32020, missing client capability-32021, unsupported version-32022)401 Unauthorized- Missing, malformed, or invalid bearer token (auth enabled), or a supplied session ID that does not match the session403 Forbidden- Authenticated but not permitted (missing scope or allowlist), or a request rejected by the DNS-rebinding Host/Origin guard404 Not Found- Unknown endpoints, and unknown methods on modern-era requests500 Internal Server Error- Server-side errors
Performance Considerations
For production deployments:
- Binding: Use
host = "0.0.0.0"to accept external connections - Port Selection: Avoid common ports; use application-specific ports
- Session Management: Monitor session count and implement cleanup
- SSE Connections: Limit concurrent SSE streams per client
- Origin Validation: Always configure allowed origins in production
Troubleshooting
Connection Issues
# Check if server is listening
using Sockets
@assert isopen(connect(transport.host, transport.port))Session Problems
- Ensure
Mcp-Session-Idheader is included after initialization - Check that session ID contains only visible ASCII characters (0x21-0x7E)
- Verify server hasn't restarted (sessions are lost on restart)
Protocol Version Mismatches
- The server accepts any supported version in the
MCP-Protocol-Versionheader: the legacy versions (2025-11-25,2025-06-18,2025-03-26,2024-11-05), which are negotiated duringinitialize, and the modern2026-07-28, which arrives per-request in the params_metaand must also be asserted in the header under SEP-2243 - Response headers echo the negotiated version after initialization
- A modern request whose header and body versions disagree is
-32020/400, not a negotiation failure — check both before suspecting the server - Check server logs for protocol version negotiation messages
Migration from stdio
To migrate from stdio to HTTP transport:
# Before (stdio)
server = mcp_server(name = "my-server", tools = [my_tool])
start!(server)
# After (HTTP)
transport = HttpTransport(port = 3000)
server = mcp_server(name = "my-server", tools = [my_tool])
server.transport = transport
ModelContextProtocol.connect(transport)
start!(server)Key changes:
- Create and configure
HttpTransport - Set
server.transportbefore starting - Call
ModelContextProtocol.connect(transport)to start HTTP server - Update client code to use HTTP requests with session management
Examples
See the examples/ directory for complete working examples:
examples/simple_http_server.jl- Simple HTTP server setupexamples/reg_dir_http.jl- HTTP server with directory auto-registration