Skip to content
Status

Intro to MCP

What are MCP transports?

Governing MCP across your organization?

Speakeasy’s AI control plane connects, secures, and observes every agent and MCP server from one entry point — with SSO, RBAC, runtime guardrails, and a full audit trail.

Legacy SDK examples

The Python examples below use initialization-based SDK APIs. The HTTP+SSE example is retained for existing deployments; new servers should use Streamable HTTP and the 2026-07-28 request model described below.

So, you’ve built your first MCP server – you’ve defined some tools, exposed a few resources, and maybe even wired up prompts – but how do you actually connect to it? That’s where transports come in.

MCP transports define how messages move between clients and servers. While MCP is a model-agnostic, JSON-RPC-based protocol, the transport layer handles the real-world details of sending and receiving data – whether you’re embedding the server in a local CLI, streaming updates to a browser, or wiring it into a desktop agent.

MCP supports two standard transports: stdio and Streamable HTTP. Custom transports must preserve MCP message patterns and per-request metadata.

Request, response, and notification formats

All communication follows the JSON-RPC 2.0 wire protocol. Every message sent between the client and the server falls into one of these categories:

Request

The client sends the following payload to invoke a method on the server:

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "place_order", "arguments": { ... } }
}

Response

The server returns the following data after handling a request:

{
"jsonrpc": "2.0",
"id": 1,
"result": { "status": "ok" }
}

Notification

A notification is a one-way message that does not expect a response.

{
"jsonrpc": "2.0",
"method": "notifications/prompts/list_changed",
"params": { "updated": true }
}

Built-in MCP transports

MCP servers can be exposed over two built-in transport mechanisms:

stdio

Standard input and output (stdio) is the simplest and most universal transport for MCP. It allows a server to read from stdin and write to stdout, which is ideal for local use, CLIs, and agent plugins.

Other use cases are:

  • CLI tools
  • Local development
  • Agent plugins (like Claude Desktop or LangGraph steps)

Here is a Python implementation of an MCP transport with stdio:

from mcp.server import Server
from mcp.server import stdio_server
app = Server("stdio-mcp-server")
@app.list_tools()
async def tools():
return []
async def main():
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
app.create_initialization_options()
)
if __name__ == "__main__":
import asyncio
asyncio.run(main())

In the example above, we:

  • Register an empty list of tools via @list_tools, which could later be populated with callable server-side functions.
  • Use stdio_server() as the transport, which opens standard input and output streams for communication and is ideal for local processes and CLI integrations.
  • Run the server with app.run(...), passing the opened streams and initialization options to handle incoming JSON-RPC messages and serve responses.

Streamable HTTP

Streamable HTTP sends client messages as HTTP POST requests to a single MCP endpoint. Responses can be JSON or request-scoped server-sent event (SSE) streams. Change notifications use a subscriptions/listen POST-response stream.

The older HTTP+SSE transport is deprecated. The following example uses that legacy transport and should not be used as a template for new deployments:

from mcp.server import Server
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.routing import Route
app = Server("sse-mcp-server")
sse = SseServerTransport("/messages")
async def sse_handler(scope, receive, send):
async with sse.connect_sse(scope, receive, send) as streams:
await app.run(streams[0], streams[1], app.create_initialization_options())
async def post_handler(scope, receive, send):
await sse.handle_post_message(scope, receive, send)
starlette_app = Starlette(routes=[
Route("/sse", sse_handler),
Route("/messages", post_handler, methods=["POST"])
])

In the example above, we:

  • Initialize the SSE transport using SseServerTransport("/messages"), which defines the internal POST message endpoint that the server will listen to for client inputs.
  • Define a /sse endpoint via sse_handler, which establishes a persistent SSE connection. Inside it, we call app.run() with the input and output streams from the connected client.
  • Define a /messages POST route with post_handler, allowing the client to send messages back to the server via HTTP POST, which is how the client communicates with the server.
  • Wrap the handlers in a Starlette app using route definitions, making it ready to be served by any ASGI server (for example, Uvicorn).

Custom transport implementations

MCP does not require servers to use stdio or Streamable HTTP. Custom transports must:

  1. Preserve the JSON-RPC message format and MCP message patterns.
  2. Carry protocol version and client capabilities on each request.

Custom transports should document connection establishment, message framing, and cancellation.

So, you can use:

  • WebSockets
  • gRPC
  • Shared memory channels
  • Named pipes
  • Custom browser bridges

You must ensure that the messages follow the JSON-RPC structure and that your transport yields the appropriate async streams.

Connection lifecycle and control

MCP 2026-07-28 is stateless. The initialize / notifications/initialized handshake and protocol-level sessions have been removed.

Every request carries its protocol version and client capabilities in _meta. Servers must implement server/discover to advertise their versions, capabilities, and identity; clients may call it before other requests.

Clients send requests, and servers return responses. Servers can request additional input through an InputRequiredResult, which the client supplies by retrying the original request. Progress notifications travel on the response stream of the request they describe.

Cancellation depends on the transport: stdio uses notifications/cancelled, while Streamable HTTP clients close the response stream. There is no protocol session to terminate.

Final thoughts

Transports should be secured according to their communication layers:

  • For Streamable HTTP, validate Origin headers, enforce authentication, and bind to localhost during local development to avoid DNS rebinding vulnerabilities.
  • For custom transports, ensure that authentication, rate limiting, and input sanitization are enforced. Always follow the JSON-RPC security best practices and sanitize all payloads.

The transport may be low-level but it defines the perimeter of your server, so treat it as a security boundary.

An MCP server can support multiple transport mechanisms simultaneously, but it must implement at least one transport to be operational. Each client connection uses one specific transport, but the server itself can expose its functionality through different transport interfaces based on various client needs.

Use caseRecommended transport
Local developmentstdio
CLI toolsstdio
Agent plugins (LangGraph, Claude Desktop)stdio
Server UIs / browsersStreamable HTTP
Cross-network communicationStreamable HTTP with CORS/Auth
Custom hardware or IPCCustom
WebSocket or real-time streamingCustom (WebSocket)