CleverChatty, a lightweight AI Chat tool supporting multiple LLM providers, now includes support for Streamable HTTP with MCP servers. This update enables more flexible and efficient communication with AI models, making integration with custom tools and services even smoother.
🌐 What is CleverChatty?
CleverChatty is a minimalist AI chat interface that works with various large language model (LLM) providers — including OpenAI, Anthropic, Google, and local models like Ollama. It’s designed for users and developers who want a simple, extensible tool that supports MCP-based tool usage.
Until now, CleverChatty only supported STDIO and SSE (Server-Sent Events) as transport protocols for connecting with MCP servers. With the latest update, it now supports Streamable HTTP, expanding compatibility and flexibility.
🚀 How to Use Streamable HTTP in CleverChatty
Streamable HTTP is now the default transport method when the transport
field is omitted but a url
is present in the config file.
To use SSE, explicitly set "transport": "sse"
.
✅ Example Configuration
{
"log_file_path": "",
"model": "ollama:qwen2.5:3b",
"mcpServers": {
"http_streaming_weather_server": {
"url": "http://weather-service:8000/mcp",
"headers": []
},
"sse_weather_server": {
"url": "http://weather-service:8001/sse",
"transport": "sse",
"headers": []
},
"stdio_weather_server": {
"command": "uv",
"args": ["weather-service.py"]
}
}
}
🧪 Example MCP Server: Supports STDIO, SSE, and Streamable HTTP
The example below demonstrates an MCP server that supports all three transports. It also shows how to write a universal server capable of handling multiple protocols, making it perfect for testing CleverChatty.
✅ This server also includes a basic authentication layer and a command-execution tool for Linux/Mac.
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 + " http_streaming HOST:PORT")
fmt.Println("Usage: ./" + binaryName + " stdio")
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":
if len(os.Args) < 3 {
fmt.Println("http streaming transport requires a host and port argument (e.g., 0.0.0.0:8080)")
os.Exit(1)
}
host_and_port := os.Args[2]
runAsHTTPStreaming(host_and_port)
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(httpContextFunction)))
fmt.Println("🚀 SSE Server started")
if err := sObj.Start(host_and_port); err != nil {
fmt.Printf("😡 Server error: %v\n", err)
}
fmt.Println("👋 Server stopped")
}
func runAsHTTPStreaming(host_and_port string) {
// for SSE we require auth
requireAuth = true
// Start the stdio server
sObj := server.NewStreamableHTTPServer(createServer(),
server.WithHTTPContextFunc(server.HTTPContextFunc(httpContextFunction)))
fmt.Println("🚀 Streamable HTTP Server started")
if err := sObj.Start(host_and_port); err != nil {
fmt.Printf("😡 Server error: %v\n", err)
}
fmt.Println("👋 Server stopped")
}
// 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
}
// This function is used to extract the token from the HTTP request context.
// It is used in the SSE and HTTP streaming transports to authenticate requests.
func httpContextFunction(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
}
// RequireAuth is a middleware that checks if the request is authenticated.
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) {
args, ok := request.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("invalid arguments"), nil
}
cmdStr, ok := args["command"].(string)
if !ok || cmdStr == "" {
return mcp.NewToolResultError("command is required"), nil
}
// Optional working_dir
var workingDir string
if wd, ok := args["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
}
You can run this server in different modes using:
go run main.go http_streaming 0.0.0.0:8080
# or
go run main.go sse 0.0.0.0:8080
# or compile and run for stdio
go build -o myserver main.go
./myserver stdio
🛠️ Setting up CleverChatty with Streamable HTTP
To test CleverChatty with your new Streamable HTTP MCP server:
1. Create a config.json
{
"log_file_path": "",
"model": "ollama:qwen2.5:3b",
"mcpServers": {
"linux_server": {
"url": "http://localhost:8080/mcp",
"headers": []
}
}
}
2. Start the MCP server
go run main.go http_streaming 0.0.0.0:8080
3. Run CleverChatty
go run github.com/gelembjuk/cleverchatty-cli@latest --config config.json
Or install and run:
go install github.com/gelembjuk/cleverchatty-cli@latest
cleverchatty --config config.json
⚠️ Platform Note
This MCP server uses sh -c
for executing shell commands, so it works only on Linux or macOS. To run it on Windows, you’ll need to modify the execCmdHandler
function to use a Windows-compatible command execution method.
With Streamable HTTP support now built in, CleverChatty becomes even more versatile for developers building intelligent, tool-using AI agents. Whether you prefer SSE, STDIO, or now Streamable HTTP — you’ve got the freedom to choose what fits your deployment best.