Implementing Authentication in a Remote MCP Server with SSE Transport

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](/blog/post/mcp-could-significantly-transform-how-we-use-the-internet/). For my experiments, I’ve modified the [mcphost](https://github.com/mark3labs/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](https://k33g.hashnode.dev/creating-an-mcp-server-in-go-and-serving-it-with-docker). 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: ```go 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](https://github.com/modelcontextprotocol/python-sdk). ```python 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: ```python @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](https://github.com/modelcontextprotocol/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. ```bash npx @modelcontextprotocol/inspector node build/index.js ``` It lunches the local web server at the endpoint http://127.0.0.1:6274 ![MCP Inspector](/blog/image/implementing-authentication-in-a-remote-mcp-server-with-sse-transport/1.png) 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 ```json { "mcpServers": { "linux_server": { "transport": "sse", "endpoint": "http://192.168.40.173:8000/sse", "bearertoken":"expected-token" } } } ``` And i run the command ```bash ./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` ```plaintext 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. ```

Previous Post:

MCP, LLM
"Tool calling" from LLM. Understanding hot it works
25 March 2025

In this post, I explore how LLMs can understand requests requiring a "tool call" and how they can effectively use external APIs to enhance their responses.

Continue Reading