**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](https://github.com/Gelembjuk/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
```json
{
"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.
```golang
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 + " [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:
```bash
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`
```json
{
"log_file_path": "",
"model": "ollama:qwen2.5:3b",
"mcpServers": {
"linux_server": {
"url": "http://localhost:8080/mcp",
"headers": []
}
}
}
```
### 2. Start the MCP server
```bash
go run main.go http_streaming 0.0.0.0:8080
```
### 3. Run CleverChatty
```bash
go run github.com/gelembjuk/cleverchatty-cli@latest --config config.json
```
Or install and run:
```bash
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.