Implementing Authentication in a Remote MCP Server with SSE Transport


Today, I want to show how Model Context Protocol (MCP) servers using SSE transport can be made secure by adding authentication.
I'll use the Authorization
HTTP header to read a Bearer token. Generating the token itself is out of scope for this post, it is same as usual practices for web applications.
To verify how this works, you’ll need an MCP host tool that supports SSE endpoints along with custom headers. Unfortunately, I couldn’t find any AI chat tools that currently support this. For example, Claude Desktop doesn’t, and I haven’t come across any others that do.
However, I’m hopeful that most AI chat tools will start supporting it soon — there’s really no reason not to. By the way, I shared my thoughts on how MCP could transform the web in this post.
For my experiments, I’ve modified the mcphost tool. I’ve submitted a pull request with my changes and hope it gets accepted. For now, I’m using a local modified version. I won’t go into the details here, since the focus is on MCP servers, not clients.
Golang implementation of MCP SSE Server with Authorization
I followed the MCP server example from this blog post. In my version, I replaced the "curl fetch" logic with a tool that executes CLI commands on a Linux server. With this lightweight MCP server, I can now control my Linux system using an LLM model.
Here’s the full code:
package main
import (
"context"
"fmt"
"net/http"
"os/exec"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func main() {
// Create MCP server
s := server.NewMCPServer(
"Server to manage a Linux instance",
"1.0.0",
)
// describe the tool
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"),
),
)
// add the tool
s.AddTool(execTool, RequireAuth(execCmdHandler))
fmt.Println("🚀 Server started")
// Start the stdio server
sObj := server.NewSSEServer(s,
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
})),
)
if err := sObj.Start("0.0.0.0:8001"); err != nil {
fmt.Printf("😡 Server error: %v\n", err)
}
fmt.Println("👋 Server stopped")
}
// we will use this wrapper for each tool handler. We need to verify a user token before any tool call
func RequireAuth(handler server.ToolHandlerFunc) server.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
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
}
How it works
When the server starts, we attach a custom context hook using the server.WithSSEContextFunc
wrapper. This hook is triggered for each new connection and gives us access to the http.Request
object. From there, we read the Authorization
header and extract the token. The token is then stored in the request context.
The next step is to run the tool. In our case, the tool is wrapped with a RequireAuth
middleware. Before the actual tool handler is executed, this wrapper runs first and checks the token. In a production environment, this would typically involve a proper token verification service — but for simplicity, we’re just comparing the token to a hardcoded "correct" value.
If the token is invalid, we return an error message and skip running the actual tool handler entirely.
As a result, our MCP server is protected against unauthorized access.
Python implementation of MCP SSE Server with Authorization
There is same server written with Python. I have used the MCP Python SDK.
from mcp.server.fastmcp import FastMCP
from fastapi import FastAPI, Request
import subprocess
import shlex
# Global variable to keep a token a for a request
auth_token = ""
app = FastAPI()
mcp = FastMCP("Server to manage a Linux instance")
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
auth_header = request.headers.get("Authorization")
if auth_header:
# extract token from the header and keep it in the global variable
global auth_token
auth_token = auth_header.split(" ")[1]
response = await call_next(request)
return response
def require_auth():
"""
Check access and raise an error if the token is not valid.
"""
if auth_token != "expected-token":
raise ValueError("Invalid token")
return None
def run_cli(command: str, cwd: str = None) -> str:
"""
Execute a CLI command using subprocess."""
if cwd == "":
cwd = None
command_list = shlex.split(command)
run_result = subprocess.run(
command_list,
cwd=cwd,
capture_output=True,
text=True,
check=False,
)
success = run_result.returncode == 0
return f"STDOUT: {run_result.stdout}\n\nSTDERR: {run_result.stderr}\nRETURNCODE: {run_result.returncode}\nSUCCESS: {success}"
@mcp.tool()
def cli_command(command: str, work_dir: str | None = "") -> str:
"""
Execute command line cli command on the Linux server.
Arguments:
command - command to execute.
work_dir - workdir will be changed to this path before executing the command.
"""
require_auth() # we have to add this inside each tool method
return run_cli(command, work_dir)
app.mount("/", mcp.sse_app())
It works — but I’m not completely satisfied with the current approach. There’s no clean or standardized way to add middleware for authentication checks. Right now, we’re forced to rely on a global variable, which isn’t ideal.
I was hoping for support similar to FastAPI’s dependency injection system. For example, it would be great if we could write something like:
@mcp.tool()
def cli_command(command: str, work_dir: str | None = "", session: AuthSessionDepend) -> str:
And have the framework automatically create the session
object based on the type annotation.
Alternatively, it would be helpful if we could access and pass context through the request: Request
object — but unfortunately, this object isn’t available in the tool endpoint either.
Testing your MCP servers
To test both servers i use the MCP Inspector tool.
Here's a clearer and more polished version of that paragraph:
I’ve started my servers on a remote Linux machine: the Go-based MCP server is running on port 8001, and the Python server is running on port 8000.
Meanwhile, on my main desktop, I’m running the inspector tool to interact with and monitor both servers.
npx @modelcontextprotocol/inspector node build/index.js
It lunches the local web server at the endpoint http://127.0.0.1:6274
IMG1
I can confirm both my servers are working fine with this tool.
Verify MCP servers with the mcphost application
I have the temporary version of the mcphost
. The final version could be different. This config JSON file is here just for a reference.
So, i create the config file mcp.json
{
"mcpServers": {
"linux_server": {
"transport": "sse",
"endpoint": "http://192.168.40.173:8000/sse",
"bearertoken":"expected-token"
}
}
}
And i run the command
./mcphost --config ./mcp.json --model ollama:qwen2.5:3b
This is my communication with the qwen2.5:3b model and it has the access to one tool - linux server
INFO Model loaded provider=ollama model=qwen2.5:3b
INFO Initializing server... name=linux_server
INFO Server connected name=linux_server
INFO Tools loaded server=linux_server count=1
You:
Execute some cli command on a linux to confirm you have access to the linux server
INFO 🔧 Using tool name=linux_server__cli_command
INFO 🔧 Using tool name=linux_server__cli_command
Assistant:
The commands executed successfully on the Linux server. Here are the results:
1. whoami returned "gelembjuk". This command typically returns your username.
2. The second command, echo 'Test', simply echoed back 'Test'.
These basic tests confirm that I have access to this Linux server and can run commands
successfully. Please note that these are standard commands and further testing or specific
tasks will be required for a comprehensive verification.
You:
What linux distro is that server?
INFO 🔧 Using tool name=linux_server__cli_command
Assistant:
The Linux server is running Ubuntu 24.04.2 LTS (codename 'noble'). This information was
obtained from the /etc/os-release file, which typically contains details about the
operating system's name and version on a Unix-like system.
If you need further assistance or specific commands related to this environment, feel free
to ask!
You:
is that server busy?
INFO 🔧 Using tool name=linux_server__cli_command
Assistant:
The server has been running for about a day with an average CPU load of around 0.05 over
the past 5 minutes. It currently has 2 users logged in and appears to be lightly loaded.
Subscribe to my newsletter
Read articles from Roman Gelembjuk directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
