feat: add --tls-skip-verify flag for self-signed certificates (#115)

* feat: add --tls-skip-verify flag for self-signed certificates

Adds support for skipping TLS certificate verification when connecting to
providers with self-signed certificates. This is particularly useful for
local Ollama instances secured with HTTPS.

- Add --tls-skip-verify command-line flag with security warnings
- Update ProviderConfig to include TLSSkipVerify field
- Modify HTTP client creation for all providers (Ollama, OpenAI, Anthropic, Google, Azure)
- Create helper functions for TLS-aware HTTP client creation
- Add comprehensive unit tests for TLS skip verify functionality
- Update documentation with usage examples and security warnings

Fixes #113

🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>

* feat: add TLS skip verify support to script mode

- Add TLSSkipVerify field to Config struct for script frontmatter
- Update script parsing to handle tls-skip-verify in YAML frontmatter
- Pass TLS configuration to model creation in script mode
- Add example script demonstrating TLS skip verify usage
- Update script examples documentation

This allows scripts to specify tls-skip-verify: true in their frontmatter
to connect to providers with self-signed certificates.

🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>

---------

Co-authored-by: opencode <noreply@opencode.ai>
This commit is contained in:
Ed Zynda
2025-08-05 21:00:58 +07:00
committed by GitHub
parent d30f15269e
commit fe4db1998d
8 changed files with 248 additions and 8 deletions
+9
View File
@@ -113,6 +113,13 @@ export GOOGLE_API_KEY='your-api-key'
- Get your API server base URL, API key and model name
- Use `--provider-url` and `--provider-api-key` flags or set environment variables
5. Self-Signed Certificates (TLS):
If your provider uses self-signed certificates (e.g., local Ollama with HTTPS), you can skip certificate verification:
```bash
mcphost --provider-url https://192.168.1.100:443 --tls-skip-verify
```
⚠️ **WARNING**: Only use `--tls-skip-verify` for development or when connecting to trusted servers with self-signed certificates. This disables TLS certificate verification and is insecure for production use.
## Installation 📦
```bash
@@ -741,6 +748,7 @@ mcphost -p "Generate a random UUID" --quiet | tr '[:lower:]' '[:upper:]'
### Flags
- `--provider-url string`: Base URL for the provider API (applies to OpenAI, Anthropic, Ollama, and Google)
- `--provider-api-key string`: API key for the provider (applies to OpenAI, Anthropic, and Google)
- `--tls-skip-verify`: Skip TLS certificate verification (WARNING: insecure, use only for self-signed certificates)
- `--config string`: Config file location (default is $HOME/.mcphost.yml)
- `--system-prompt string`: system-prompt file location
- `--debug`: Enable debug logging
@@ -808,6 +816,7 @@ stream: false # Disable streaming (default: true)
# API Configuration
provider-api-key: "your-api-key" # For OpenAI, Anthropic, or Google
provider-url: "https://api.openai.com/v1" # Custom base URL
tls-skip-verify: false # Skip TLS certificate verification (default: false)
```
**Note**: Command-line flags take precedence over config file values.
+11
View File
@@ -55,6 +55,9 @@ var (
// Hooks control
noHooks bool
// TLS configuration
tlsSkipVerify bool
)
// agentUIAdapter adapts agent.Agent to ui.AgentInterface
@@ -260,6 +263,7 @@ func init() {
flags := rootCmd.PersistentFlags()
flags.StringVar(&providerURL, "provider-url", "", "base URL for the provider API (applies to OpenAI, Anthropic, Ollama, and Google)")
flags.StringVar(&providerAPIKey, "provider-api-key", "", "API key for the provider (applies to OpenAI, Anthropic, and Google)")
flags.BoolVar(&tlsSkipVerify, "tls-skip-verify", false, "skip TLS certificate verification (WARNING: insecure, use only for self-signed certificates)")
// Model generation parameters
flags.IntVar(&maxTokens, "max-tokens", 4096, "maximum number of tokens in the response")
@@ -291,6 +295,7 @@ func init() {
viper.BindPFlag("stop-sequences", rootCmd.PersistentFlags().Lookup("stop-sequences"))
viper.BindPFlag("num-gpu-layers", rootCmd.PersistentFlags().Lookup("num-gpu-layers"))
viper.BindPFlag("main-gpu", rootCmd.PersistentFlags().Lookup("main-gpu"))
viper.BindPFlag("tls-skip-verify", rootCmd.PersistentFlags().Lookup("tls-skip-verify"))
// Defaults are already set in flag definitions, no need to duplicate in viper
@@ -364,6 +369,7 @@ func runNormalMode(ctx context.Context) error {
StopSequences: viper.GetStringSlice("stop-sequences"),
NumGPU: &numGPU,
MainGPU: &mainGPU,
TLSSkipVerify: viper.GetBool("tls-skip-verify"),
}
// Create spinner function for agent creation
@@ -448,6 +454,11 @@ func runNormalMode(ctx context.Context) error {
"system-prompt": viper.GetString("system-prompt"),
}
// Add TLS skip verify if enabled
if viper.GetBool("tls-skip-verify") {
debugConfig["tls-skip-verify"] = true
}
// Add Ollama-specific parameters if using Ollama
if strings.HasPrefix(viper.GetString("model"), "ollama:") {
debugConfig["num-gpu-layers"] = viper.GetInt("num-gpu-layers")
+7
View File
@@ -134,6 +134,9 @@ func overrideConfigWithFrontmatter(scriptFile string, variables map[string]strin
if scriptConfig.Stream != nil && !flagChanged("stream") {
viper.Set("stream", *scriptConfig.Stream)
}
if scriptConfig.TLSSkipVerify && !flagChanged("tls-skip-verify") {
viper.Set("tls-skip-verify", scriptConfig.TLSSkipVerify)
}
}
// parseCustomVariables extracts custom variables from command line arguments
@@ -392,6 +395,9 @@ func parseScriptContent(content string, variables map[string]string) (*config.Co
if noExit := frontmatterViper.GetBool("no-exit"); noExit {
scriptConfig.NoExit = noExit
}
if tlsSkipVerify := frontmatterViper.GetBool("tls-skip-verify"); tlsSkipVerify {
scriptConfig.TLSSkipVerify = tlsSkipVerify
}
}
// Set prompt from content after frontmatter
@@ -586,6 +592,7 @@ func runScriptMode(ctx context.Context, mcpConfig *config.Config, prompt string,
TopP: &finalTopP,
TopK: &finalTopK,
StopSequences: finalStopSequences,
TLSSkipVerify: viper.GetBool("tls-skip-verify"),
}
// Create the agent using the factory (scripts don't need spinners)
+22
View File
@@ -28,6 +28,28 @@ mcphost script default-values-demo.sh \
--args:format "json"
```
### `tls-test-script.sh`
Demonstrates TLS skip verify for connecting to providers with self-signed certificates.
**Features showcased:**
- `tls-skip-verify` configuration in script frontmatter
- Connecting to HTTPS endpoints with self-signed certificates
- Security considerations for development environments
**Usage:**
```bash
# Run with TLS skip verify enabled (configured in script)
mcphost script tls-test-script.sh
# Override the provider URL
mcphost script tls-test-script.sh --provider-url https://192.168.1.100:443
# Disable TLS skip verify via command line (overrides script config)
mcphost script tls-test-script.sh --tls-skip-verify=false
```
⚠️ **WARNING**: Only use `tls-skip-verify` for development or when connecting to trusted servers with self-signed certificates.
## Variable Syntax Reference
MCPHost scripts support two types of variables:
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env -S mcphost script
---
# Example script demonstrating TLS skip verify for self-signed certificates
model: "ollama:llama3.2"
provider-url: "https://localhost:8443"
tls-skip-verify: true
max-tokens: 1000
---
Hello! Can you tell me about TLS certificates and why someone might need to skip certificate verification in development environments?
+3
View File
@@ -117,6 +117,9 @@ type Config struct {
TopP *float32 `json:"top-p,omitempty" yaml:"top-p,omitempty"`
TopK *int32 `json:"top-k,omitempty" yaml:"top-k,omitempty"`
StopSequences []string `json:"stop-sequences,omitempty" yaml:"stop-sequences,omitempty"`
// TLS configuration
TLSSkipVerify bool `json:"tls-skip-verify,omitempty" yaml:"tls-skip-verify,omitempty"`
}
// GetTransportType returns the transport type for the server config
+64 -8
View File
@@ -3,6 +3,7 @@ package models
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
@@ -78,6 +79,9 @@ type ProviderConfig struct {
// Ollama-specific parameters
NumGPU *int32
MainGPU *int32
// TLS configuration
TLSSkipVerify bool // Skip TLS certificate verification (insecure)
}
// ProviderResult contains the result of provider creation
@@ -216,6 +220,11 @@ func createAzureOpenAIProvider(ctx context.Context, config *ProviderConfig, mode
azureConfig.Stop = config.StopSequences
}
// Set HTTP client with TLS config if needed
if config.TLSSkipVerify {
azureConfig.HTTPClient = createHTTPClientWithTLSConfig(true)
}
return openai.NewCustomChatModel(ctx, azureConfig)
}
@@ -246,12 +255,16 @@ func createAnthropicProvider(ctx context.Context, config *ProviderConfig, modelN
if strings.HasPrefix(source, "stored OAuth") {
// For OAuth tokens, we need to use Authorization: Bearer header
// Create a custom HTTP client that adds the proper headers
claudeConfig.HTTPClient = createOAuthHTTPClient(apiKey)
claudeConfig.HTTPClient = createOAuthHTTPClient(apiKey, config.TLSSkipVerify)
// Set a dummy API key to prevent the library from failing validation
claudeConfig.APIKey = "oauth-placeholder"
} else {
// For API keys, use the standard x-api-key header
claudeConfig.APIKey = apiKey
// Set HTTP client with TLS config if needed
if config.TLSSkipVerify {
claudeConfig.HTTPClient = createHTTPClientWithTLSConfig(true)
}
}
if config.ProviderURL != "" {
@@ -295,6 +308,11 @@ func createOpenAIProvider(ctx context.Context, config *ProviderConfig, modelName
openaiConfig.BaseURL = config.ProviderURL
}
// Set HTTP client with TLS config if needed
if config.TLSSkipVerify {
openaiConfig.HTTPClient = createHTTPClientWithTLSConfig(true)
}
// Check if this is a reasoning model to handle beta limitations (skip validation if using custom URL)
registry := GetGlobalRegistry()
isReasoningModel := false
@@ -350,10 +368,17 @@ func createGoogleProvider(ctx context.Context, config *ProviderConfig, modelName
return nil, fmt.Errorf("Google API key not provided. Use --provider-api-key flag or GOOGLE_API_KEY/GEMINI_API_KEY/GOOGLE_GENERATIVE_AI_API_KEY environment variable")
}
client, err := genai.NewClient(ctx, &genai.ClientConfig{
clientConfig := &genai.ClientConfig{
APIKey: apiKey,
Backend: genai.BackendGeminiAPI,
})
}
// Set HTTP client with TLS config if needed
if config.TLSSkipVerify {
clientConfig.HTTPClient = createHTTPClientWithTLSConfig(true)
}
client, err := genai.NewClient(ctx, clientConfig)
if err != nil {
return nil, fmt.Errorf("failed to create Google client: %v", err)
}
@@ -389,8 +414,8 @@ type OllamaLoadingResult struct {
}
// loadOllamaModelWithFallback loads an Ollama model with GPU settings and automatic CPU fallback
func loadOllamaModelWithFallback(ctx context.Context, baseURL, modelName string, options *api.Options) (*OllamaLoadingResult, error) {
client := &http.Client{}
func loadOllamaModelWithFallback(ctx context.Context, baseURL, modelName string, options *api.Options, tlsSkipVerify bool) (*OllamaLoadingResult, error) {
client := createHTTPClientWithTLSConfig(tlsSkipVerify)
// Phase 1: Check if model exists locally
if err := checkOllamaModelExists(client, baseURL, modelName); err != nil {
@@ -601,7 +626,7 @@ func createOllamaProviderWithResult(ctx context.Context, config *ProviderConfig,
// Try to pre-load the model with GPU settings and automatic CPU fallback
// If this fails, fall back to the original behavior
loadingResult, err := loadOllamaModelWithFallback(ctx, baseURL, modelName, options)
loadingResult, err := loadOllamaModelWithFallback(ctx, baseURL, modelName, options, config.TLSSkipVerify)
var loadingMessage string
if err != nil {
@@ -620,6 +645,11 @@ func createOllamaProviderWithResult(ctx context.Context, config *ProviderConfig,
Options: finalOptions,
}
// Set HTTP client with TLS config if needed
if config.TLSSkipVerify {
ollamaConfig.HTTPClient = createHTTPClientWithTLSConfig(true)
}
chatModel, err := ollama.NewChatModel(ctx, ollamaConfig)
if err != nil {
return nil, err
@@ -631,12 +661,38 @@ func createOllamaProviderWithResult(ctx context.Context, config *ProviderConfig,
}, nil
}
// createHTTPClientWithTLSConfig creates an HTTP client with optional TLS skip verify
func createHTTPClientWithTLSConfig(skipVerify bool) *http.Client {
if !skipVerify {
return &http.Client{}
}
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
return &http.Client{
Transport: transport,
}
}
// createOAuthHTTPClient creates an HTTP client that adds OAuth headers for Anthropic API
func createOAuthHTTPClient(accessToken string) *http.Client {
func createOAuthHTTPClient(accessToken string, skipVerify bool) *http.Client {
var base http.RoundTripper = http.DefaultTransport
if skipVerify {
base = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
return &http.Client{
Transport: &oauthTransport{
accessToken: accessToken,
base: http.DefaultTransport,
base: base,
},
}
}
+123
View File
@@ -0,0 +1,123 @@
package models
import (
"net/http"
"testing"
)
func TestCreateHTTPClientWithTLSConfig(t *testing.T) {
tests := []struct {
name string
skipVerify bool
wantInsecure bool
}{
{
name: "skip verify disabled",
skipVerify: false,
wantInsecure: false,
},
{
name: "skip verify enabled",
skipVerify: true,
wantInsecure: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := createHTTPClientWithTLSConfig(tt.skipVerify)
if client == nil {
t.Fatal("expected non-nil client")
}
// Check if the client has a custom transport when skipVerify is true
if tt.skipVerify {
transport, ok := client.Transport.(*http.Transport)
if !ok {
t.Fatal("expected *http.Transport when skipVerify is true")
}
if transport.TLSClientConfig == nil {
t.Fatal("expected non-nil TLSClientConfig when skipVerify is true")
}
if transport.TLSClientConfig.InsecureSkipVerify != tt.wantInsecure {
t.Errorf("InsecureSkipVerify = %v, want %v",
transport.TLSClientConfig.InsecureSkipVerify, tt.wantInsecure)
}
}
})
}
}
func TestCreateOAuthHTTPClient(t *testing.T) {
tests := []struct {
name string
accessToken string
skipVerify bool
wantInsecure bool
}{
{
name: "oauth with skip verify disabled",
accessToken: "test-token",
skipVerify: false,
wantInsecure: false,
},
{
name: "oauth with skip verify enabled",
accessToken: "test-token",
skipVerify: true,
wantInsecure: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := createOAuthHTTPClient(tt.accessToken, tt.skipVerify)
if client == nil {
t.Fatal("expected non-nil client")
}
// Check that the transport is an oauthTransport
oauthTransport, ok := client.Transport.(*oauthTransport)
if !ok {
t.Fatal("expected *oauthTransport")
}
if oauthTransport.accessToken != tt.accessToken {
t.Errorf("accessToken = %v, want %v", oauthTransport.accessToken, tt.accessToken)
}
// Check the base transport when skipVerify is true
if tt.skipVerify {
baseTransport, ok := oauthTransport.base.(*http.Transport)
if !ok {
t.Fatal("expected base transport to be *http.Transport when skipVerify is true")
}
if baseTransport.TLSClientConfig == nil {
t.Fatal("expected non-nil TLSClientConfig when skipVerify is true")
}
if baseTransport.TLSClientConfig.InsecureSkipVerify != tt.wantInsecure {
t.Errorf("InsecureSkipVerify = %v, want %v",
baseTransport.TLSClientConfig.InsecureSkipVerify, tt.wantInsecure)
}
}
})
}
}
func TestProviderConfigTLSSkipVerify(t *testing.T) {
// Test that ProviderConfig properly stores TLSSkipVerify
config := &ProviderConfig{
ModelString: "test:model",
TLSSkipVerify: true,
}
if !config.TLSSkipVerify {
t.Error("expected TLSSkipVerify to be true")
}
}