From c2bc527b30648edb0c5c980bfa467d15712e828a Mon Sep 17 00:00:00 2001 From: Luxferre Date: Sat, 21 Mar 2026 16:29:28 +0200 Subject: [PATCH] added auth to remote mcps --- README.md | 84 +++++++++++++++++++++++++++++++++++-------------------- config.go | 36 ++++++++++++++++-------- mcp.go | 33 ++++++++++++++++++---- 3 files changed, 107 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 9dbf2a5..303ddbc 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,6 @@ This file defines your models, MCP servers, and agent profiles. tool_response_threshold = 10000 # Bytes after which tool results are buffered to files [models.default] -``` - model_id = "env:DEFAULT_MODEL_ID" endpoint = "env:DEFAULT_MODEL_ENDPOINT" api_key = "env:DEFAULT_MODEL_API_KEY" @@ -67,6 +65,12 @@ transport = "stdio" command = "npx" args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] +[mcp_servers.remote_tools] +transport = "sse" +url = "https://api.example.com/mcp" +auth_token = "env:MCP_API_KEY" # Optional Bearer token +headers = { "X-Custom-Header" = "value" } # Optional custom headers + [agents.coordinator] role = "COORDINATOR" model_id = "default" @@ -105,6 +109,26 @@ user's request and delegate tasks accordingly. ./bin/sidekick-tui ``` +### Running the Sidekick MCP Server + +Sidekick can also act as an MCP server, allowing other LLM-powered applications to leverage its agentic capabilities. + +1. Configure the `mcp_listener` in `config.toml`: + +```toml +[mcp_listener] +transport = "sse" # "stdio" or "sse"/"http" +port = 8080 # for sse/http +``` + +2. Run the MCP server: + +```bash +./bin/sidekick-mcp +``` + +The server provides a `query` tool that routes requests to your coordinator agent, maintaining conversation history if provided. + * **Input:** Type your messages at the bottom prompt. * **Send:** Press `Enter` to send a message. * **Quit:** Press `ctrl+c` to exit. @@ -116,43 +140,43 @@ user's request and delegate tasks accordingly. package main import ( - "context" - "fmt" - "log" + "context" + "fmt" + "log" - "github.com/yourusername/sidekick-ng" + "github.com/yourusername/sidekick-ng" ) func main() { - // 1. Load Configuration - cfg, err := sidekick.LoadConfig("config.toml") - if err != nil { - log.Fatal(err) - } + // 1. Load Configuration + cfg, err := sidekick.LoadConfig("config.toml") + if err != nil { + log.Fatal(err) + } - // 2. Load and Apply Prompts (Optional) - tmpl, _ := sidekick.LoadPrompts("prompts.toml") + // 2. Load and Apply Prompts (Optional) + tmpl, _ := sidekick.LoadPrompts("prompts.toml") - // 3. Initialize Agent - agentCfg := cfg.Agents["coordinator"] - - // Apply template override if exists - if tmpl != nil && tmpl.Lookup("coordinator") != nil { - if rendered, err := sidekick.RenderPrompt(tmpl, "coordinator"); err == nil { - agentCfg.SystemPrompt = rendered - } - } + // 3. Initialize Agent + agentCfg := cfg.Agents["coordinator"] + + // Apply template override if exists + if tmpl != nil && tmpl.Lookup("coordinator") != nil { + if rendered, err := sidekick.RenderPrompt(tmpl, "coordinator"); err == nil { + agentCfg.SystemPrompt = rendered + } + } - agent := sidekick.NewAgent("coordinator", agentCfg, cfg) + agent := sidekick.NewAgent("coordinator", agentCfg, cfg) - // 4. Run the Agent Loop - ctx := context.Background() - result, err := agent.Run(ctx, "What files are in the /tmp directory?") - if err != nil { - log.Fatal(err) - } + // 4. Run the Agent Loop + ctx := context.Background() + result, err := agent.Run(ctx, "What files are in the /tmp directory?") + if err != nil { + log.Fatal(err) + } - fmt.Println(result) + fmt.Println(result) } ``` diff --git a/config.go b/config.go index d589342..d0926dd 100644 --- a/config.go +++ b/config.go @@ -25,22 +25,24 @@ type MCPListenerConfig struct { // ModelConfig holds configuration for the LLM type ModelConfig struct { - ModelID string `toml:"model_id"` - Endpoint string `toml:"endpoint"` - APIKey string `toml:"api_key"` - Temperature float32 `toml:"temperature"` - MaxTokens int `toml:"max_tokens"` - TimeoutSecs int `toml:"timeout_secs"` + ModelID string `toml:"model_id"` + Endpoint string `toml:"endpoint"` + APIKey string `toml:"api_key"` + Temperature float32 `toml:"temperature"` + MaxTokens int `toml:"max_tokens"` + TimeoutSecs int `toml:"timeout_secs"` Timeout time.Duration `toml:"-"` } // MCPServerConfig configures a single MCP server connection type MCPServerConfig struct { - Transport string `toml:"transport"` // "stdio", "sse", "http", "websocket" - Command string `toml:"command"` // for stdio - Args []string `toml:"args"` // for stdio - Env []string `toml:"env"` // for stdio, format "KEY=VALUE" - URL string `toml:"url"` // for HTTP/SSE/WebSocket + Transport string `toml:"transport"` // "stdio", "sse", "http", "websocket" + Command string `toml:"command"` // for stdio + Args []string `toml:"args"` // for stdio + Env []string `toml:"env"` // for stdio, format "KEY=VALUE" + URL string `toml:"url"` // for HTTP/SSE/WebSocket + AuthToken string `toml:"auth_token"` // for HTTP Bearer authentication + Headers map[string]string `toml:"headers"` // for custom headers } // AgentConfig holds configuration for a specific agent instance @@ -66,6 +68,14 @@ func resolveEnv(val string) string { return val } +func (s *MCPServerConfig) Resolve() { + s.URL = resolveEnv(s.URL) + s.AuthToken = resolveEnv(s.AuthToken) + for k, v := range s.Headers { + s.Headers[k] = resolveEnv(v) + } +} + // Resolve applies environment variable resolution to relevant fields func (m *ModelConfig) Resolve() { m.ModelID = resolveEnv(m.ModelID) @@ -92,6 +102,10 @@ func LoadConfig(path string) (*ProjectConfig, error) { m.Resolve() cfg.Models[k] = m } + for k, s := range cfg.MCPServers { + s.Resolve() + cfg.MCPServers[k] = s + } for k, a := range cfg.Agents { a.ID = k a.ToolResponseThreshold = cfg.ToolResponseThreshold diff --git a/mcp.go b/mcp.go index 70d0c8f..434464d 100644 --- a/mcp.go +++ b/mcp.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/client/transport" "github.com/mark3labs/mcp-go/mcp" ) @@ -22,12 +23,12 @@ func (w *mcpGoClientWrapper) CallTool(ctx context.Context, name string, args map req := mcp.CallToolRequest{} req.Params.Name = name req.Params.Arguments = args - + res, err := w.client.CallTool(ctx, req) if err != nil { return "", err } - + if res.IsError { return "", fmt.Errorf("tool returned error") } @@ -71,9 +72,31 @@ var defaultMCPFactory MCPClientFactory = func(toolset string) (MCPClientInterfac case "stdio": c, err = client.NewStdioMCPClient(cfg.Command, cfg.Env, cfg.Args...) case "sse": - c, err = client.NewSSEMCPClient(cfg.URL) + var opts []transport.ClientOption + headers := make(map[string]string) + if cfg.AuthToken != "" { + headers["Authorization"] = "Bearer " + cfg.AuthToken + } + for k, v := range cfg.Headers { + headers[k] = v + } + if len(headers) > 0 { + opts = append(opts, transport.WithHeaders(headers)) + } + c, err = client.NewSSEMCPClient(cfg.URL, opts...) case "http": - c, err = client.NewStreamableHttpClient(cfg.URL) + var opts []transport.StreamableHTTPCOption + headers := make(map[string]string) + if cfg.AuthToken != "" { + headers["Authorization"] = "Bearer " + cfg.AuthToken + } + for k, v := range cfg.Headers { + headers[k] = v + } + if len(headers) > 0 { + opts = append(opts, transport.WithHTTPHeaders(headers)) + } + c, err = client.NewStreamableHttpClient(cfg.URL, opts...) default: return nil, fmt.Errorf("unsupported MCP transport: %s", cfg.Transport) } @@ -89,7 +112,7 @@ var defaultMCPFactory MCPClientFactory = func(toolset string) (MCPClientInterfac Name: "Sidekick", Version: "1.0.0", } - + _, err = c.Initialize(context.Background(), initReq) if err != nil { return nil, fmt.Errorf("failed to initialize MCP client: %w", err)