Easily Switch Transport Protocols in MCP Servers


I would like to expose one more benefit of the Model Context Protocol (MCP) — the ability to easily change the transport protocol. There are three different transport protocols available now, and each has its own benefits and drawbacks.
However, if an MCP server is implemented properly using a good SDK, then switching to another transport protocol is easy.
Quick Recap: What is MCP?
- Model Context Protocol (MCP) is a new standard for integrating external tools with AI chat applications. For example, you can add Google Search as an MCP server to Claude Desktop, allowing the LLM to perform live searches to improve its responses. In this case, Claude Desktop is the MCP Host.
There are three common types of MCP server transports:
STDIO Transport: The MCP server runs locally on the same machine as the MCP Host. Users download a small application (the MCP server), install it, and configure the MCP Host to communicate with it via standard input/output.
SSE Transport: The MCP server runs as a network service, typically on a remote server (but it can also be on
localhost
). It's essentially a special kind of website that the MCP Host connects to via Server-Sent Events (SSE).HTTP Streaming: This is a new protocol approved only recently. It is designed to replace SSE transport. Currently, there are not many examples of this protocol in use because SDK developers have not yet released updates.
What Type of Transport to Use?
There can be a couple of reasons why you might want to change the transport protocol.
In this blog post, I described the differences between STDIO and SSE transport and explained the reasons why you might prefer one over the other.
I think there are a couple of common rules:
If the MCP server can work remotely, use SSE transport.
If the MCP server can run in a Docker container, use SSE transport.
So, STDIO should be used only if there is no other option. For example, if the MCP server has to interact with processes or applications running on the same machine like MS Excel, then STDIO transport is the only option.
When we look at a directory of MCP servers (there are a few directories), we find that most of them use STDIO transport. The reason is simple — popular AI tools like Claude Desktop or Cursor only allow connecting to STDIO transport. So no other options are available.
When Is Changing the Transport Protocol Necessary?
If you want to create an MCP server not only for your own use but also for others, then you should consider using SSE or HTTP Streaming transport. Using STDIO servers can be risky. A user has to download some script or program and run it on their machine. Of course, it's okay if they trust you and if the download is hosted on a trusted website.
The danger is, for example, if your server is copied and published on another website, someone could modify it with malicious code, and your users might unknowingly download and run it. You can’t control this.
Only if some kind of “Play Store” for MCP servers is created, where each MCP server is verified and signed by a trusted authority, would this be safer. But that is not the case yet — we are far from it.
Instead, if you create an SSE or HTTP Streaming MCP server and host it on your own server, your clients can connect directly to it. You can control the code, update it, add security layers, etc., and your users don’t need to run any code on their machines.
In some cases, the MCP server must work with the local filesystem. Even in this case, you can use SSE or HTTP Streaming. Just create a Docker image with your MCP server inside, and users can run it locally with access to specific folders.
What Is the Difference Between SSE and HTTP Streaming at the Code Level?
The difference is really small. Here are two examples in Golang.
This is a simple example of an MCP server that uses STDIO transport:
package main
import (
"context"
"errors"
"fmt"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func main() {
mcpServer := createServer()
// Start the stdio server
if err := server.ServeStdio(mcpServer); err != nil {
fmt.Printf("Server error: %v\n", err)
}
}
func createServer() *server.MCPServer {
// Create MCP server
s := server.NewMCPServer(
"Demo 🚀",
"1.0.0",
)
// Add tool
tool := mcp.NewTool("hello_world",
mcp.WithDescription("Say hello to someone"),
mcp.WithString("name",
mcp.Required(),
mcp.Description("Name of the person to greet"),
),
)
// Add tool handler
s.AddTool(tool, helloHandler)
return s
}
func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, ok := request.Params.Arguments["name"].(string)
if !ok {
return nil, errors.New("name must be a string")
}
return mcp.NewToolResultText(fmt.Sprintf("Hello, %s!", name)), nil
}
To change it to SSE transport, you only need to modify a few lines:
// Start the stdio server
if err := server.ServeStdio(mcpServer); err != nil {
fmt.Printf("Server error: %v\n", err)
}
Change it to:
host_and_port := "localhost:8080"
// Start the SSE server
sObj := server.NewSSEServer(mcpServer)
if err := sObj.Start(host_and_port); err != nil {
fmt.Printf("Server error: %v\n", err)
}
If you use a good SDK for MCP server development, changing the transport protocol is easy.
Any Transport Example
Here is a simple example of an MCP server that can be used with any transport protocol. Depending on your needs, you can use STDIO, SSE, or HTTP Streaming transport.
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var requireAuth = false
func main() {
if len(os.Args) < 2 {
binaryName := filepath.Base(os.Args[0])
fmt.Println("Usage: ./" + binaryName + " <TRANSPORT> [OPTIONS]")
fmt.Println("Usage: ./" + binaryName + " sse HOST:PORT")
fmt.Println("Usage: ./" + binaryName + " stdoi")
os.Exit(1)
}
transport := os.Args[1]
switch transport {
case "stdio":
runAsStdio()
case "sse":
if len(os.Args) < 3 {
fmt.Println("sse transport requires a host and port argument (e.g., 0.0.0.0:8080)")
os.Exit(1)
}
host_and_port := os.Args[2]
runAsSSE(host_and_port)
case "http_streaming":
fmt.Println("This is not implemented yet. Waiting for github.com/mark3labs/mcp-go to add support of http streaming")
os.Exit(2)
default:
fmt.Printf("Unknown transport: %s\n", transport)
os.Exit(3)
}
}
func runAsStdio() {
if err := server.ServeStdio(createServer()); err != nil {
fmt.Printf("😡 Server error: %v\n", err)
}
}
func runAsSSE(host_and_port string) {
// for SSE we require auth
requireAuth = true
// Start the stdio server
sObj := server.NewSSEServer(createServer(),
server.WithSSEContextFunc(server.SSEContextFunc(func(ctx context.Context, r *http.Request) context.Context {
// Extract the Authorization header from the request and get the token from it
header := r.Header.Get("Authorization")
if header == "" {
return ctx
}
// get token after Bearer
token := header[len("Bearer "):]
if token == "" {
return ctx
}
fmt.Printf("😎 Token: %s\n", token)
// add this token to the context. We will check it later
ctx = context.WithValue(ctx, "token", token)
return ctx
})),
)
fmt.Println("🚀 Server started")
if err := sObj.Start(host_and_port); err != nil {
fmt.Printf("😡 Server error: %v\n", err)
}
fmt.Println("👋 Server stopped")
}
func runAsHTTPStreaming(endpoint string) {
// TODO: Implement HTTP streaming server
}
// All the code below is not related to MCP server transport
func createServer() *server.MCPServer {
// Create MCP server
s := server.NewMCPServer(
"Server to manage a Linux instance",
"1.0.0",
)
execTool := mcp.NewTool("exec_cmd",
mcp.WithDescription("Execute a Linux command with optional working directory"),
mcp.WithString("command",
mcp.Required(),
mcp.Description("The full shell command to execute"),
),
mcp.WithString("working_dir",
mcp.Description("Optional working directory where the command should run"),
),
)
s.AddTool(execTool, RequireAuth(execCmdHandler))
return s
}
func RequireAuth(handler server.ToolHandlerFunc) server.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
if !requireAuth {
// If we don't require auth, just call the handler
return handler(ctx, request)
}
token, ok := ctx.Value("token").(string)
if !ok || token != "expected-token" { // or validate it
return mcp.NewToolResultError("unauthorized"), nil
}
return handler(ctx, request)
}
}
func execCmdHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
cmdStr, ok := request.Params.Arguments["command"].(string)
if !ok || cmdStr == "" {
return mcp.NewToolResultError("command is required"), nil
}
// Optional working_dir
var workingDir string
if wd, ok := request.Params.Arguments["working_dir"].(string); ok {
workingDir = wd
}
// Use "sh -c" to allow full shell command with arguments and operators
cmd := exec.Command("sh", "-c", cmdStr)
if workingDir != "" {
cmd.Dir = workingDir
}
output, err := cmd.CombinedOutput()
if err != nil {
// Include both the error and output for context
return mcp.NewToolResultError(fmt.Sprintf("execution failed: %v\n%s", err, output)), nil
}
return mcp.NewToolResultText(string(output)), nil
}
The example is intended to work in three modes but currently works only in STDIO and SSE. HTTP Streaming is not yet implemented because the mcp-go
library does not support it — but it should be added soon.
Details of the Example
STDIO mode does not need authentication.
SSE mode requires authentication. The token should be provided in the
Authorization
header.The server can execute any command on the host machine. It is not safe and should only be used for testing purposes.
How to Try This Example
I used CleverChatty CLI — a simple CLI tool that enables AI chat and acts as the MCP Host. This AI Chat tool can work with different LLMs using Ollama or APIs from providers like OpenAI, Anthropic, etc.
To run the example in STDIO mode, your config would look like this:
{
"model": "ollama:qwen2.5:3b",
"mcpServers": {
"Linux_STDIO_Server": {
"command": "./mcp-all-transports",
"args": [
"stdio"
]
}
}
}
You need to provide the path to the mcp-all-transports
binary. You can build it from the code above (build steps are skipped here). The path can be relative or absolute.
In the same directory as the config file, run:
go run github.com/gelembjuk/cleverchatty-cli@latest --config config.json
Ask the assistant to execute a command, e.g., ls -la
or echo hello world
.
To run it as SSE, change the config to:
{
"model": "ollama:qwen2.5:3b",
"mcpServers": {
"Linux_SSE_Server": {
"url": "http://lubuntu:8080/sse",
"headers": [
"Authorization: Bearer expected-token"
]
}
}
}
Then run your server:
./mcp-all-transports sse 0.0.0.0:8080
This will start the server on port 8080 and listen for incoming requests.
Run CleverChatty CLI with this new config and try to execute some commands.
Final thoughts
The transport protocol can be changed for MCP servers. It is easy to do if you use a good SDK for MCP server development. The example above shows how to create a simple MCP server that can be used with any transport protocol.
The ability to switch transport protocols gives developers great flexibility when deploying MCP servers. Whether you want the simplicity of STDIO for local use, the scalability of SSE for remote use, or the future-ready nature of HTTP Streaming, the choice is yours — as long as your architecture is modular. While we wait for broader support of HTTP Streaming, building your MCP server with portability in mind ensures you're ready for whatever comes next.
Subscribe to my newsletter
Read articles from Roman Gelembjuk directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
