2025-06-09 14:38:31 +03:00
|
|
|
package config
|
|
|
|
|
|
|
|
|
|
import (
|
2025-06-24 15:56:29 +03:00
|
|
|
"encoding/json"
|
2025-06-09 14:38:31 +03:00
|
|
|
"fmt"
|
|
|
|
|
"os"
|
2025-06-09 23:44:01 +03:00
|
|
|
"path/filepath"
|
2025-06-11 11:45:55 +03:00
|
|
|
"strings"
|
2025-09-02 09:30:20 +02:00
|
|
|
|
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
|
"gopkg.in/yaml.v3"
|
2025-06-09 14:38:31 +03:00
|
|
|
)
|
|
|
|
|
|
2025-11-12 16:48:46 +03:00
|
|
|
// MCPServerConfig represents configuration for an MCP server, supporting both
|
2026-02-26 17:41:02 +03:00
|
|
|
// local (stdio) and remote (StreamableHTTP/SSE) server types.
|
2025-11-12 16:48:46 +03:00
|
|
|
// It maintains backward compatibility with legacy configuration formats.
|
2025-06-09 14:38:31 +03:00
|
|
|
type MCPServerConfig struct {
|
2025-06-24 15:56:29 +03:00
|
|
|
Type string `json:"type"`
|
|
|
|
|
Command []string `json:"command,omitempty"`
|
|
|
|
|
Environment map[string]string `json:"environment,omitempty"`
|
|
|
|
|
URL string `json:"url,omitempty"`
|
2025-06-27 16:30:18 +03:00
|
|
|
AllowedTools []string `json:"allowedTools,omitempty" yaml:"allowedTools,omitempty"`
|
|
|
|
|
ExcludedTools []string `json:"excludedTools,omitempty" yaml:"excludedTools,omitempty"`
|
2025-06-24 16:29:44 +03:00
|
|
|
|
2026-04-11 15:24:47 +03:00
|
|
|
// OAuth configuration for remote servers that don't support dynamic
|
|
|
|
|
// client registration (e.g. GitHub). When OAuthClientID is set, it is
|
|
|
|
|
// passed directly to the transport's OAuthConfig instead of relying on
|
|
|
|
|
// dynamic registration.
|
|
|
|
|
OAuthClientID string `json:"oauthClientId,omitempty" yaml:"oauthClientId,omitempty"`
|
|
|
|
|
OAuthClientSecret string `json:"oauthClientSecret,omitempty" yaml:"oauthClientSecret,omitempty"`
|
|
|
|
|
OAuthScopes []string `json:"oauthScopes,omitempty" yaml:"oauthScopes,omitempty"`
|
|
|
|
|
|
2026-04-21 22:24:10 +03:00
|
|
|
// NoOAuth disables OAuth transport configuration for this server, even
|
|
|
|
|
// when the connection pool has an auth handler. Use this for public MCP
|
|
|
|
|
// servers (e.g. PubMed) that don't require authentication. Without this
|
|
|
|
|
// flag, the pool would attach OAuth transport to every remote server,
|
|
|
|
|
// causing proactive dynamic-client-registration attempts that fail on
|
|
|
|
|
// servers that don't support it.
|
|
|
|
|
NoOAuth bool `json:"noOAuth,omitempty" yaml:"noOAuth,omitempty"`
|
|
|
|
|
|
2026-05-04 16:51:09 +03:00
|
|
|
// TasksMode controls when this server's tools/call requests are augmented
|
|
|
|
|
// with MCP task metadata (turning a synchronous call into an asynchronous,
|
|
|
|
|
// pollable job — see https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks).
|
|
|
|
|
//
|
|
|
|
|
// Valid values:
|
|
|
|
|
// - "" or "auto": (default) augment requests with task metadata only
|
|
|
|
|
// when the server advertises tasks/toolCalls capability during initialize.
|
|
|
|
|
// - "never": never augment — every tool call is synchronous, regardless
|
|
|
|
|
// of server capability.
|
|
|
|
|
// - "always": always augment, even when the server didn't advertise
|
|
|
|
|
// task support. The server may still respond synchronously; this just
|
|
|
|
|
// opts in unconditionally on the client side.
|
|
|
|
|
//
|
|
|
|
|
// In all modes, when the server returns a CreateTaskResult the client polls
|
|
|
|
|
// tasks/get / tasks/result until the task reaches a terminal state.
|
|
|
|
|
TasksMode string `json:"tasksMode,omitempty" yaml:"tasksMode,omitempty"`
|
|
|
|
|
|
2026-04-15 16:29:07 +03:00
|
|
|
// InProcessServer holds a live *server.MCPServer for in-process transport.
|
|
|
|
|
// When set (and Type is "inprocess"), the connection pool creates an
|
|
|
|
|
// in-process client instead of spawning a subprocess or making HTTP calls.
|
|
|
|
|
// This field is never serialized — it is only used programmatically via the SDK.
|
|
|
|
|
InProcessServer any `json:"-" yaml:"-"`
|
|
|
|
|
|
2025-06-24 15:56:29 +03:00
|
|
|
// Legacy fields for backward compatibility
|
2025-06-24 16:29:44 +03:00
|
|
|
Transport string `json:"transport,omitempty"`
|
|
|
|
|
Args []string `json:"args,omitempty"`
|
|
|
|
|
Env map[string]any `json:"env,omitempty"`
|
|
|
|
|
Headers []string `json:"headers,omitempty"`
|
2025-06-24 15:56:29 +03:00
|
|
|
}
|
|
|
|
|
|
2025-11-12 16:48:46 +03:00
|
|
|
// UnmarshalJSON handles both new and legacy config formats for backward compatibility.
|
|
|
|
|
// New format uses "type" field with "local", "remote", or "builtin" values.
|
|
|
|
|
// Legacy format uses "transport", "command", "args", and "env" fields.
|
2025-06-24 15:56:29 +03:00
|
|
|
func (s *MCPServerConfig) UnmarshalJSON(data []byte) error {
|
|
|
|
|
// First try to unmarshal as the new format
|
|
|
|
|
type newFormat struct {
|
2026-04-11 15:24:47 +03:00
|
|
|
Type string `json:"type"`
|
|
|
|
|
Command []string `json:"command,omitempty"`
|
|
|
|
|
Environment map[string]string `json:"environment,omitempty"`
|
|
|
|
|
URL string `json:"url,omitempty"`
|
|
|
|
|
Headers []string `json:"headers,omitempty"`
|
|
|
|
|
AllowedTools []string `json:"allowedTools,omitempty" yaml:"allowedTools,omitempty"`
|
|
|
|
|
ExcludedTools []string `json:"excludedTools,omitempty" yaml:"excludedTools,omitempty"`
|
|
|
|
|
OAuthClientID string `json:"oauthClientId,omitempty" yaml:"oauthClientId,omitempty"`
|
|
|
|
|
OAuthClientSecret string `json:"oauthClientSecret,omitempty" yaml:"oauthClientSecret,omitempty"`
|
|
|
|
|
OAuthScopes []string `json:"oauthScopes,omitempty" yaml:"oauthScopes,omitempty"`
|
2026-04-21 22:24:10 +03:00
|
|
|
NoOAuth bool `json:"noOAuth,omitempty" yaml:"noOAuth,omitempty"`
|
2026-05-04 16:51:09 +03:00
|
|
|
TasksMode string `json:"tasksMode,omitempty" yaml:"tasksMode,omitempty"`
|
2025-06-24 15:56:29 +03:00
|
|
|
}
|
2025-06-24 16:29:44 +03:00
|
|
|
|
2025-06-24 15:56:29 +03:00
|
|
|
// Also try legacy format
|
|
|
|
|
type legacyFormat struct {
|
|
|
|
|
Transport string `json:"transport,omitempty"`
|
|
|
|
|
Command string `json:"command,omitempty"`
|
|
|
|
|
Args []string `json:"args,omitempty"`
|
|
|
|
|
Env map[string]any `json:"env,omitempty"`
|
|
|
|
|
URL string `json:"url,omitempty"`
|
|
|
|
|
Headers []string `json:"headers,omitempty"`
|
2025-06-27 16:30:18 +03:00
|
|
|
AllowedTools []string `json:"allowedTools,omitempty" yaml:"allowedTools,omitempty"`
|
|
|
|
|
ExcludedTools []string `json:"excludedTools,omitempty" yaml:"excludedTools,omitempty"`
|
2026-05-04 16:51:09 +03:00
|
|
|
TasksMode string `json:"tasksMode,omitempty" yaml:"tasksMode,omitempty"`
|
2025-06-24 15:56:29 +03:00
|
|
|
}
|
2025-06-24 16:29:44 +03:00
|
|
|
|
2025-06-24 15:56:29 +03:00
|
|
|
// Try new format first
|
|
|
|
|
var newConfig newFormat
|
|
|
|
|
if err := json.Unmarshal(data, &newConfig); err == nil && newConfig.Type != "" {
|
|
|
|
|
s.Type = newConfig.Type
|
|
|
|
|
s.Command = newConfig.Command
|
|
|
|
|
s.Environment = newConfig.Environment
|
|
|
|
|
s.URL = newConfig.URL
|
2025-06-25 13:15:09 +03:00
|
|
|
s.Headers = newConfig.Headers
|
2025-06-24 15:56:29 +03:00
|
|
|
s.AllowedTools = newConfig.AllowedTools
|
|
|
|
|
s.ExcludedTools = newConfig.ExcludedTools
|
2026-04-11 15:24:47 +03:00
|
|
|
s.OAuthClientID = newConfig.OAuthClientID
|
|
|
|
|
s.OAuthClientSecret = newConfig.OAuthClientSecret
|
|
|
|
|
s.OAuthScopes = newConfig.OAuthScopes
|
2026-04-21 22:24:10 +03:00
|
|
|
s.NoOAuth = newConfig.NoOAuth
|
2026-05-04 16:51:09 +03:00
|
|
|
s.TasksMode = newConfig.TasksMode
|
2025-06-24 15:56:29 +03:00
|
|
|
return nil
|
|
|
|
|
}
|
2025-06-24 16:29:44 +03:00
|
|
|
|
2025-06-24 15:56:29 +03:00
|
|
|
// Fall back to legacy format
|
|
|
|
|
var legacyConfig legacyFormat
|
|
|
|
|
if err := json.Unmarshal(data, &legacyConfig); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2025-06-24 16:29:44 +03:00
|
|
|
|
2025-06-24 15:56:29 +03:00
|
|
|
// Convert legacy format to new format
|
|
|
|
|
s.Transport = legacyConfig.Transport
|
|
|
|
|
if legacyConfig.Command != "" {
|
|
|
|
|
s.Command = append([]string{legacyConfig.Command}, legacyConfig.Args...)
|
|
|
|
|
}
|
|
|
|
|
s.Args = legacyConfig.Args
|
|
|
|
|
s.Env = legacyConfig.Env
|
|
|
|
|
s.URL = legacyConfig.URL
|
|
|
|
|
s.Headers = legacyConfig.Headers
|
|
|
|
|
s.AllowedTools = legacyConfig.AllowedTools
|
|
|
|
|
s.ExcludedTools = legacyConfig.ExcludedTools
|
2026-05-04 16:51:09 +03:00
|
|
|
s.TasksMode = legacyConfig.TasksMode
|
2025-06-24 16:29:44 +03:00
|
|
|
|
2025-06-25 10:58:14 +03:00
|
|
|
// Infer type from legacy format for better compatibility
|
|
|
|
|
// Only set Type when it doesn't change existing transport behavior
|
|
|
|
|
if legacyConfig.Command != "" {
|
|
|
|
|
s.Type = "local" // This maps to "stdio" which matches legacy behavior
|
|
|
|
|
}
|
|
|
|
|
// Don't set Type for URL-only configs to preserve legacy "sse" behavior
|
|
|
|
|
// The URL will be handled by the legacy fallback logic in GetTransportType()
|
|
|
|
|
|
2025-06-24 15:56:29 +03:00
|
|
|
return nil
|
2025-06-09 14:38:31 +03:00
|
|
|
}
|
|
|
|
|
|
2025-11-12 16:48:46 +03:00
|
|
|
// AdaptiveColor represents a color that adapts to light and dark themes.
|
|
|
|
|
// Either light or dark can be specified, or both for theme-aware coloring.
|
2025-09-02 09:30:20 +02:00
|
|
|
type AdaptiveColor struct {
|
|
|
|
|
Light string `json:"light,omitempty" yaml:"light,omitempty"`
|
|
|
|
|
Dark string `json:"dark,omitempty" yaml:"dark,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 18:04:56 +03:00
|
|
|
// MarkdownThemeConfig defines color overrides for markdown rendering and
|
|
|
|
|
// syntax highlighting.
|
|
|
|
|
type MarkdownThemeConfig struct {
|
|
|
|
|
Text AdaptiveColor `json:"text,omitzero" yaml:"text,omitempty"`
|
|
|
|
|
Muted AdaptiveColor `json:"muted,omitzero" yaml:"muted,omitempty"`
|
|
|
|
|
Heading AdaptiveColor `json:"heading,omitzero" yaml:"heading,omitempty"`
|
|
|
|
|
Emph AdaptiveColor `json:"emph,omitzero" yaml:"emph,omitempty"`
|
|
|
|
|
Strong AdaptiveColor `json:"strong,omitzero" yaml:"strong,omitempty"`
|
|
|
|
|
Link AdaptiveColor `json:"link,omitzero" yaml:"link,omitempty"`
|
|
|
|
|
Code AdaptiveColor `json:"code,omitzero" yaml:"code,omitempty"`
|
|
|
|
|
Error AdaptiveColor `json:"error,omitzero" yaml:"error,omitempty"`
|
|
|
|
|
Keyword AdaptiveColor `json:"keyword,omitzero" yaml:"keyword,omitempty"`
|
|
|
|
|
String AdaptiveColor `json:"string,omitzero" yaml:"string,omitempty"`
|
|
|
|
|
Number AdaptiveColor `json:"number,omitzero" yaml:"number,omitempty"`
|
|
|
|
|
Comment AdaptiveColor `json:"comment,omitzero" yaml:"comment,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 16:48:46 +03:00
|
|
|
// Theme defines the color scheme for the application UI with adaptive colors
|
|
|
|
|
// that support both light and dark modes.
|
2025-09-02 09:30:20 +02:00
|
|
|
type Theme struct {
|
2026-03-19 18:04:56 +03:00
|
|
|
Primary AdaptiveColor `json:"primary,omitzero" yaml:"primary,omitempty"`
|
|
|
|
|
Secondary AdaptiveColor `json:"secondary,omitzero" yaml:"secondary,omitempty"`
|
|
|
|
|
Success AdaptiveColor `json:"success,omitzero" yaml:"success,omitempty"`
|
|
|
|
|
Warning AdaptiveColor `json:"warning,omitzero" yaml:"warning,omitempty"`
|
|
|
|
|
Error AdaptiveColor `json:"error,omitzero" yaml:"error,omitempty"`
|
|
|
|
|
Info AdaptiveColor `json:"info,omitzero" yaml:"info,omitempty"`
|
|
|
|
|
Text AdaptiveColor `json:"text,omitzero" yaml:"text,omitempty"`
|
|
|
|
|
Muted AdaptiveColor `json:"muted,omitzero" yaml:"muted,omitempty"`
|
|
|
|
|
VeryMuted AdaptiveColor `json:"very-muted,omitzero" yaml:"very-muted,omitempty"`
|
|
|
|
|
Background AdaptiveColor `json:"background,omitzero" yaml:"background,omitempty"`
|
|
|
|
|
Border AdaptiveColor `json:"border,omitzero" yaml:"border,omitempty"`
|
|
|
|
|
MutedBorder AdaptiveColor `json:"muted-border,omitzero" yaml:"muted-border,omitempty"`
|
|
|
|
|
System AdaptiveColor `json:"system,omitzero" yaml:"system,omitempty"`
|
|
|
|
|
Tool AdaptiveColor `json:"tool,omitzero" yaml:"tool,omitempty"`
|
|
|
|
|
Accent AdaptiveColor `json:"accent,omitzero" yaml:"accent,omitempty"`
|
|
|
|
|
Highlight AdaptiveColor `json:"highlight,omitzero" yaml:"highlight,omitempty"`
|
|
|
|
|
|
|
|
|
|
// Diff block backgrounds
|
|
|
|
|
DiffInsertBg AdaptiveColor `json:"diff-insert-bg,omitzero" yaml:"diff-insert-bg,omitempty"`
|
|
|
|
|
DiffDeleteBg AdaptiveColor `json:"diff-delete-bg,omitzero" yaml:"diff-delete-bg,omitempty"`
|
|
|
|
|
DiffEqualBg AdaptiveColor `json:"diff-equal-bg,omitzero" yaml:"diff-equal-bg,omitempty"`
|
|
|
|
|
DiffMissingBg AdaptiveColor `json:"diff-missing-bg,omitzero" yaml:"diff-missing-bg,omitempty"`
|
|
|
|
|
|
|
|
|
|
// Code/output block backgrounds
|
|
|
|
|
CodeBg AdaptiveColor `json:"code-bg,omitzero" yaml:"code-bg,omitempty"`
|
|
|
|
|
GutterBg AdaptiveColor `json:"gutter-bg,omitzero" yaml:"gutter-bg,omitempty"`
|
|
|
|
|
WriteBg AdaptiveColor `json:"write-bg,omitzero" yaml:"write-bg,omitempty"`
|
|
|
|
|
|
|
|
|
|
// Markdown rendering and syntax highlighting
|
|
|
|
|
Markdown MarkdownThemeConfig `json:"markdown,omitzero" yaml:"markdown,omitempty"`
|
2025-09-02 09:30:20 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-09 12:07:42 +03:00
|
|
|
// GenerationParams defines generation parameter defaults that can be attached
|
|
|
|
|
// to individual models. These act as model-level defaults — CLI flags and
|
|
|
|
|
// global config values take precedence when explicitly set.
|
|
|
|
|
type GenerationParams struct {
|
|
|
|
|
MaxTokens *int `json:"maxTokens,omitempty" yaml:"maxTokens,omitempty"`
|
|
|
|
|
Temperature *float32 `json:"temperature,omitempty" yaml:"temperature,omitempty"`
|
|
|
|
|
TopP *float32 `json:"topP,omitempty" yaml:"topP,omitempty"`
|
|
|
|
|
TopK *int32 `json:"topK,omitempty" yaml:"topK,omitempty"`
|
|
|
|
|
FrequencyPenalty *float32 `json:"frequencyPenalty,omitempty" yaml:"frequencyPenalty,omitempty"`
|
|
|
|
|
PresencePenalty *float32 `json:"presencePenalty,omitempty" yaml:"presencePenalty,omitempty"`
|
|
|
|
|
StopSequences []string `json:"stopSequences,omitempty" yaml:"stopSequences,omitempty"`
|
|
|
|
|
ThinkingLevel string `json:"thinkingLevel,omitempty" yaml:"thinkingLevel,omitempty"`
|
2026-04-09 12:35:00 +03:00
|
|
|
SystemPrompt string `json:"systemPrompt,omitempty" yaml:"systemPrompt,omitempty"`
|
2026-04-09 12:07:42 +03:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 14:19:49 +03:00
|
|
|
// CustomModelConfig defines a custom model that can be used with custom/custom
|
|
|
|
|
// or other custom/ prefixed models. These models are loaded from the config file
|
|
|
|
|
// and merged into the custom provider in the model registry.
|
|
|
|
|
type CustomModelConfig struct {
|
|
|
|
|
Name string `json:"name" yaml:"name"`
|
2026-04-03 12:37:14 +03:00
|
|
|
BaseURL string `json:"baseUrl,omitempty" yaml:"baseUrl,omitempty"`
|
|
|
|
|
APIKey string `json:"apiKey,omitempty" yaml:"apiKey,omitempty"`
|
2026-03-24 14:19:49 +03:00
|
|
|
Family string `json:"family,omitempty" yaml:"family,omitempty"`
|
|
|
|
|
Attachment bool `json:"attachment,omitempty" yaml:"attachment,omitempty"`
|
|
|
|
|
Reasoning bool `json:"reasoning,omitempty" yaml:"reasoning,omitempty"`
|
|
|
|
|
Temperature bool `json:"temperature,omitempty" yaml:"temperature,omitempty"`
|
|
|
|
|
Knowledge string `json:"knowledge,omitempty" yaml:"knowledge,omitempty"`
|
|
|
|
|
Cost CostConfig `json:"cost" yaml:"cost"`
|
|
|
|
|
Limit LimitConfig `json:"limit" yaml:"limit"`
|
2026-04-09 12:07:42 +03:00
|
|
|
|
|
|
|
|
// Generation parameter defaults for this model.
|
|
|
|
|
// These are applied when the user hasn't explicitly set the corresponding
|
|
|
|
|
// CLI flag or global config value.
|
|
|
|
|
Params GenerationParams `json:"params,omitzero" yaml:"params,omitempty"`
|
2026-03-24 14:19:49 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CostConfig defines the pricing for a custom model.
|
|
|
|
|
type CostConfig struct {
|
|
|
|
|
Input float64 `json:"input" yaml:"input"`
|
|
|
|
|
Output float64 `json:"output" yaml:"output"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LimitConfig defines context and output limits for a custom model.
|
|
|
|
|
type LimitConfig struct {
|
|
|
|
|
Context int `json:"context" yaml:"context"`
|
|
|
|
|
Output int `json:"output" yaml:"output"`
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 16:48:46 +03:00
|
|
|
// Config represents the complete application configuration including MCP servers,
|
|
|
|
|
// model settings, UI preferences, and API credentials. It supports both command-line
|
|
|
|
|
// flags and configuration file settings.
|
2025-06-09 14:38:31 +03:00
|
|
|
type Config struct {
|
2025-06-11 13:09:51 +03:00
|
|
|
MCPServers map[string]MCPServerConfig `json:"mcpServers" yaml:"mcpServers"`
|
|
|
|
|
Model string `json:"model,omitempty" yaml:"model,omitempty"`
|
|
|
|
|
MaxSteps int `json:"max-steps,omitempty" yaml:"max-steps,omitempty"`
|
|
|
|
|
Debug bool `json:"debug,omitempty" yaml:"debug,omitempty"`
|
|
|
|
|
SystemPrompt string `json:"system-prompt,omitempty" yaml:"system-prompt,omitempty"`
|
|
|
|
|
ProviderAPIKey string `json:"provider-api-key,omitempty" yaml:"provider-api-key,omitempty"`
|
|
|
|
|
ProviderURL string `json:"provider-url,omitempty" yaml:"provider-url,omitempty"`
|
2025-06-27 16:40:42 +03:00
|
|
|
Stream *bool `json:"stream,omitempty" yaml:"stream,omitempty"`
|
2025-09-02 09:30:20 +02:00
|
|
|
Theme any `json:"theme" yaml:"theme"`
|
2025-06-11 11:45:55 +03:00
|
|
|
// Model generation parameters
|
2026-04-06 10:52:33 +03:00
|
|
|
MaxTokens int `json:"max-tokens,omitempty" yaml:"max-tokens,omitempty"`
|
|
|
|
|
Temperature *float32 `json:"temperature,omitempty" yaml:"temperature,omitempty"`
|
|
|
|
|
TopP *float32 `json:"top-p,omitempty" yaml:"top-p,omitempty"`
|
|
|
|
|
TopK *int32 `json:"top-k,omitempty" yaml:"top-k,omitempty"`
|
|
|
|
|
FrequencyPenalty *float32 `json:"frequency-penalty,omitempty" yaml:"frequency-penalty,omitempty"`
|
|
|
|
|
PresencePenalty *float32 `json:"presence-penalty,omitempty" yaml:"presence-penalty,omitempty"`
|
|
|
|
|
StopSequences []string `json:"stop-sequences,omitempty" yaml:"stop-sequences,omitempty"`
|
2025-08-05 21:00:58 +07:00
|
|
|
|
2026-03-07 21:27:46 +03:00
|
|
|
// Thinking / extended reasoning
|
|
|
|
|
ThinkingLevel string `json:"thinking-level,omitempty" yaml:"thinking-level,omitempty"`
|
|
|
|
|
|
2025-08-05 21:00:58 +07:00
|
|
|
// TLS configuration
|
|
|
|
|
TLSSkipVerify bool `json:"tls-skip-verify,omitempty" yaml:"tls-skip-verify,omitempty"`
|
2026-03-22 19:09:15 +03:00
|
|
|
|
|
|
|
|
// Prompt templates configuration
|
|
|
|
|
Prompts []string `json:"prompts,omitempty" yaml:"prompts,omitempty"`
|
|
|
|
|
NoPromptTemplates bool `json:"no-prompt-templates,omitempty" yaml:"no-prompt-templates,omitempty"`
|
2026-03-24 14:19:49 +03:00
|
|
|
|
|
|
|
|
// Custom model definitions (under custom/ provider)
|
|
|
|
|
CustomModels map[string]CustomModelConfig `json:"customModels,omitempty" yaml:"customModels,omitempty"`
|
2026-04-09 12:07:42 +03:00
|
|
|
|
|
|
|
|
// Per-model generation parameter overrides. Keys are "provider/model" strings
|
|
|
|
|
// (e.g. "anthropic/claude-sonnet-4-5-20250929", "openai/gpt-4o"). These
|
|
|
|
|
// settings act as model-level defaults — CLI flags and global config values
|
|
|
|
|
// take precedence when explicitly set.
|
|
|
|
|
ModelSettings map[string]GenerationParams `json:"modelSettings,omitempty" yaml:"modelSettings,omitempty"`
|
2025-06-09 14:38:31 +03:00
|
|
|
}
|
|
|
|
|
|
2025-11-12 16:48:46 +03:00
|
|
|
// GetTransportType returns the transport type for the server config, mapping
|
|
|
|
|
// simplified type names to actual transport protocols. Supports legacy format
|
|
|
|
|
// detection and automatic type inference from configuration.
|
2025-06-19 16:16:19 +03:00
|
|
|
func (s *MCPServerConfig) GetTransportType() string {
|
2025-06-25 10:58:14 +03:00
|
|
|
// Legacy format support - check explicit transport first
|
|
|
|
|
if s.Transport != "" {
|
|
|
|
|
return s.Transport
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-24 15:56:29 +03:00
|
|
|
// New simplified format
|
|
|
|
|
if s.Type != "" {
|
|
|
|
|
switch s.Type {
|
|
|
|
|
case "local":
|
|
|
|
|
return "stdio"
|
|
|
|
|
case "remote":
|
|
|
|
|
return "streamable"
|
2026-04-15 16:29:07 +03:00
|
|
|
case "inprocess":
|
|
|
|
|
return "inprocess"
|
2025-06-24 15:56:29 +03:00
|
|
|
default:
|
|
|
|
|
return s.Type
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-24 16:29:44 +03:00
|
|
|
|
2026-04-15 16:29:07 +03:00
|
|
|
// Programmatic in-process server detection.
|
|
|
|
|
if s.InProcessServer != nil {
|
|
|
|
|
return "inprocess"
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-19 16:16:19 +03:00
|
|
|
// Backward compatibility: infer transport type
|
2025-06-24 15:56:29 +03:00
|
|
|
if len(s.Command) > 0 {
|
2025-06-19 16:16:19 +03:00
|
|
|
return "stdio"
|
|
|
|
|
}
|
|
|
|
|
if s.URL != "" {
|
|
|
|
|
return "sse"
|
|
|
|
|
}
|
|
|
|
|
return "stdio" // default
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 16:48:46 +03:00
|
|
|
// Validate validates the configuration, ensuring required fields are present
|
|
|
|
|
// for each server type and that tool filters are used correctly. Returns an
|
|
|
|
|
// error describing any validation failures.
|
2025-06-09 18:51:06 +03:00
|
|
|
func (c *Config) Validate() error {
|
|
|
|
|
for serverName, serverConfig := range c.MCPServers {
|
|
|
|
|
if len(serverConfig.AllowedTools) > 0 && len(serverConfig.ExcludedTools) > 0 {
|
|
|
|
|
return fmt.Errorf("server %s: allowedTools and excludedTools are mutually exclusive", serverName)
|
|
|
|
|
}
|
2025-06-19 16:16:19 +03:00
|
|
|
|
2026-05-04 17:06:11 +03:00
|
|
|
// Reject unknown tasksMode values up front so a typo (e.g. "alwasy")
|
|
|
|
|
// fails loud here instead of being silently downgraded to "auto" by
|
|
|
|
|
// the runtime parser. Comparison is case-insensitive to match
|
|
|
|
|
// tools.ParseTaskMode.
|
|
|
|
|
switch strings.ToLower(strings.TrimSpace(serverConfig.TasksMode)) {
|
|
|
|
|
case "", "auto", "never", "always":
|
|
|
|
|
// ok
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("server %s: invalid tasksMode %q (expected one of: auto, never, always)", serverName, serverConfig.TasksMode)
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-19 16:16:19 +03:00
|
|
|
transport := serverConfig.GetTransportType()
|
|
|
|
|
switch transport {
|
|
|
|
|
case "stdio":
|
2025-06-24 15:56:29 +03:00
|
|
|
// Check both new and legacy command formats
|
|
|
|
|
if len(serverConfig.Command) == 0 && serverConfig.Transport == "" {
|
2025-06-19 16:16:19 +03:00
|
|
|
return fmt.Errorf("server %s: command is required for stdio transport", serverName)
|
|
|
|
|
}
|
|
|
|
|
case "sse", "streamable":
|
|
|
|
|
if serverConfig.URL == "" {
|
|
|
|
|
return fmt.Errorf("server %s: url is required for %s transport", serverName, transport)
|
|
|
|
|
}
|
2026-04-15 16:29:07 +03:00
|
|
|
case "inprocess":
|
|
|
|
|
if serverConfig.InProcessServer == nil {
|
|
|
|
|
return fmt.Errorf("server %s: InProcessServer is required for inprocess transport", serverName)
|
|
|
|
|
}
|
2025-06-19 16:16:19 +03:00
|
|
|
default:
|
2026-04-15 16:29:07 +03:00
|
|
|
return fmt.Errorf("server %s: unsupported transport type '%s'. Supported types: stdio, sse, streamable, inprocess", serverName, transport)
|
2025-06-19 16:16:19 +03:00
|
|
|
}
|
2025-06-09 18:51:06 +03:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 16:48:46 +03:00
|
|
|
// LoadSystemPrompt loads system prompt from file or returns the string directly.
|
|
|
|
|
// If input is a path to an existing file, its contents are read and returned.
|
|
|
|
|
// Otherwise, the input string is returned as-is.
|
2025-06-10 15:18:06 +03:00
|
|
|
func LoadSystemPrompt(input string) (string, error) {
|
|
|
|
|
if input == "" {
|
2025-06-09 14:38:31 +03:00
|
|
|
return "", nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-10 15:18:06 +03:00
|
|
|
// Check if input is a file that exists
|
|
|
|
|
if _, err := os.Stat(input); err == nil {
|
2025-06-11 11:45:55 +03:00
|
|
|
// Read the entire file as plain text
|
|
|
|
|
content, err := os.ReadFile(input)
|
|
|
|
|
if err != nil {
|
2025-06-10 15:18:06 +03:00
|
|
|
return "", fmt.Errorf("error reading system prompt file: %v", err)
|
|
|
|
|
}
|
2025-06-11 11:45:55 +03:00
|
|
|
return strings.TrimSpace(string(content)), nil
|
2025-06-09 14:38:31 +03:00
|
|
|
}
|
|
|
|
|
|
2025-06-10 15:18:06 +03:00
|
|
|
// Treat as direct string
|
|
|
|
|
return input, nil
|
2025-06-09 23:44:01 +03:00
|
|
|
}
|
|
|
|
|
|
2025-11-12 16:48:46 +03:00
|
|
|
// EnsureConfigExists checks if a config file exists and creates a default one if not.
|
2026-02-26 16:59:59 +03:00
|
|
|
// It searches for .kit.{yml,yaml,json} files in the user's home directory.
|
|
|
|
|
// If none exist, creates a default .kit.yml with examples.
|
2025-06-17 23:09:29 +03:00
|
|
|
func EnsureConfigExists() error {
|
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error getting home directory: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 16:59:59 +03:00
|
|
|
// Check for existing config files
|
|
|
|
|
configNames := []string{".kit"}
|
2025-06-18 11:40:47 +03:00
|
|
|
configTypes := []string{"yml", "yaml", "json"}
|
2025-06-17 23:09:29 +03:00
|
|
|
|
|
|
|
|
for _, configName := range configNames {
|
|
|
|
|
for _, configType := range configTypes {
|
|
|
|
|
configPath := filepath.Join(homeDir, configName+"."+configType)
|
|
|
|
|
if _, err := os.Stat(configPath); err == nil {
|
|
|
|
|
// Config file exists, no need to create
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No config file found, create default
|
|
|
|
|
return createDefaultConfig(homeDir)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 16:59:59 +03:00
|
|
|
// createDefaultConfig creates a default .kit.yml file in the user's home directory
|
2025-06-09 23:44:01 +03:00
|
|
|
func createDefaultConfig(homeDir string) error {
|
2026-02-26 16:59:59 +03:00
|
|
|
configPath := filepath.Join(homeDir, ".kit.yml")
|
2025-06-10 01:21:17 +03:00
|
|
|
|
2025-06-09 23:44:01 +03:00
|
|
|
// Create the file
|
|
|
|
|
file, err := os.Create(configPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error creating config file: %v", err)
|
|
|
|
|
}
|
2026-02-25 22:51:45 +03:00
|
|
|
defer func() { _ = file.Close() }()
|
2025-06-10 01:21:17 +03:00
|
|
|
|
2025-06-24 17:50:18 +03:00
|
|
|
// Write a comprehensive YAML template with examples
|
2026-02-26 16:59:59 +03:00
|
|
|
content := `# KIT Configuration File
|
2025-06-09 23:44:01 +03:00
|
|
|
# All command-line flags can be configured here
|
|
|
|
|
|
2026-02-26 17:41:02 +03:00
|
|
|
# MCP Servers configuration (for external tool servers)
|
|
|
|
|
# Core tools (bash, read, write, edit, grep, find, ls) are built-in and always available.
|
|
|
|
|
# Add external MCP servers here for additional tools:
|
2025-06-09 23:44:01 +03:00
|
|
|
# mcpServers:
|
2025-06-24 17:50:18 +03:00
|
|
|
# # Local MCP servers - run commands locally via stdio transport
|
2026-02-26 17:41:02 +03:00
|
|
|
# filesystem:
|
2025-06-24 15:56:29 +03:00
|
|
|
# type: "local"
|
2025-06-24 17:50:18 +03:00
|
|
|
# command: ["npx", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
2025-06-24 15:56:29 +03:00
|
|
|
# environment:
|
2025-06-24 17:50:18 +03:00
|
|
|
# DEBUG: "true"
|
|
|
|
|
#
|
|
|
|
|
# # Remote MCP servers - connect via StreamableHTTP transport
|
2025-06-19 16:16:19 +03:00
|
|
|
# websearch:
|
2025-06-24 15:56:29 +03:00
|
|
|
# type: "remote"
|
2025-06-19 16:16:19 +03:00
|
|
|
# url: "https://api.example.com/mcp"
|
2025-06-09 23:44:01 +03:00
|
|
|
|
|
|
|
|
mcpServers:
|
|
|
|
|
|
|
|
|
|
# Application settings (all optional)
|
2026-02-25 18:41:49 +03:00
|
|
|
# model: "anthropic/claude-sonnet-4-5-20250929" # Default model to use
|
2025-06-24 17:50:18 +03:00
|
|
|
# max-steps: 10 # Maximum agent steps (0 for unlimited)
|
2025-06-09 23:44:01 +03:00
|
|
|
# debug: false # Enable debug logging
|
2025-06-11 11:45:55 +03:00
|
|
|
# system-prompt: "/path/to/system-prompt.txt" # System prompt text file
|
|
|
|
|
|
2026-04-09 12:07:42 +03:00
|
|
|
# Model generation parameters (all optional, apply globally to all models)
|
2025-06-11 11:45:55 +03:00
|
|
|
# max-tokens: 4096 # Maximum tokens in response
|
|
|
|
|
# temperature: 0.7 # Randomness (0.0-1.0)
|
|
|
|
|
# top-p: 0.95 # Nucleus sampling (0.0-1.0)
|
|
|
|
|
# top-k: 40 # Top K sampling
|
2026-04-06 10:52:33 +03:00
|
|
|
# frequency-penalty: 0.0 # Penalize frequent tokens (0.0-2.0)
|
|
|
|
|
# presence-penalty: 0.0 # Penalize present tokens (0.0-2.0)
|
2025-06-11 11:45:55 +03:00
|
|
|
# stop-sequences: ["Human:", "Assistant:"] # Custom stop sequences
|
2025-06-09 23:44:01 +03:00
|
|
|
|
2026-04-09 12:07:42 +03:00
|
|
|
# Per-model generation parameter overrides (apply to specific models)
|
|
|
|
|
# These act as model-level defaults — CLI flags and global settings above take precedence.
|
|
|
|
|
# Keys are "provider/model" strings matching the model you use.
|
|
|
|
|
# modelSettings:
|
|
|
|
|
# anthropic/claude-sonnet-4-5-20250929:
|
|
|
|
|
# temperature: 0.3
|
|
|
|
|
# maxTokens: 8192
|
|
|
|
|
# openai/gpt-4o:
|
|
|
|
|
# temperature: 0.7
|
|
|
|
|
# topP: 0.95
|
|
|
|
|
# topK: 40
|
|
|
|
|
# frequencyPenalty: 0.1
|
|
|
|
|
# presencePenalty: 0.1
|
|
|
|
|
# anthropic/claude-opus-4-6:
|
|
|
|
|
# thinkingLevel: "high"
|
|
|
|
|
# maxTokens: 16384
|
2026-04-09 12:35:00 +03:00
|
|
|
# systemPrompt: "You are a deep reasoning assistant." # or a file path
|
2026-04-09 12:07:42 +03:00
|
|
|
|
2026-06-12 18:53:17 +05:30
|
|
|
# Skills configuration (all optional)
|
|
|
|
|
# no-skills: false # Set to true to disable all skill loading
|
|
|
|
|
# skill: # Explicit skill files/dirs (disables auto-discovery)
|
|
|
|
|
# - "/path/to/skill.md"
|
|
|
|
|
# skills-dir: "/path/to/skills" # Override project-local directory for auto-discovery
|
|
|
|
|
|
2025-06-09 23:44:01 +03:00
|
|
|
# API Configuration (can also use environment variables)
|
2025-06-11 11:45:55 +03:00
|
|
|
# provider-api-key: "your-api-key" # API key for OpenAI, Anthropic, or Google
|
|
|
|
|
# provider-url: "https://api.openai.com/v1" # Base URL for OpenAI, Anthropic, or Ollama
|
2026-04-09 12:07:42 +03:00
|
|
|
|
|
|
|
|
# Custom model definitions (under custom/ provider)
|
|
|
|
|
# customModels:
|
|
|
|
|
# my-local-llama:
|
|
|
|
|
# name: "Local Llama 3"
|
|
|
|
|
# baseUrl: "http://localhost:8080/v1"
|
|
|
|
|
# family: "llama"
|
|
|
|
|
# temperature: true
|
|
|
|
|
# cost:
|
|
|
|
|
# input: 0.0
|
|
|
|
|
# output: 0.0
|
|
|
|
|
# limit:
|
|
|
|
|
# context: 131072
|
|
|
|
|
# output: 8192
|
|
|
|
|
# params: # Generation parameter defaults for this model
|
|
|
|
|
# temperature: 0.8
|
|
|
|
|
# topP: 0.95
|
|
|
|
|
# topK: 40
|
2026-04-09 12:35:00 +03:00
|
|
|
# systemPrompt: "You are a helpful local assistant."
|
2025-06-09 23:44:01 +03:00
|
|
|
`
|
2025-06-10 01:21:17 +03:00
|
|
|
|
2025-06-09 23:44:01 +03:00
|
|
|
_, err = file.WriteString(content)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error writing config content: %v", err)
|
|
|
|
|
}
|
2025-06-10 01:21:17 +03:00
|
|
|
|
2025-06-09 23:44:01 +03:00
|
|
|
return nil
|
2025-06-10 01:21:17 +03:00
|
|
|
}
|
2025-09-02 09:30:20 +02:00
|
|
|
|
2025-11-12 16:48:46 +03:00
|
|
|
// FilepathOr reads a configuration value that can be either a direct value or a
|
|
|
|
|
// filepath to a JSON/YAML file containing the value. If the value is a string
|
|
|
|
|
// starting with "~/" or a relative path, it's expanded to an absolute path.
|
|
|
|
|
// The contents of the file are then unmarshaled into the provided value pointer.
|
2025-09-02 09:30:20 +02:00
|
|
|
func FilepathOr[T any](key string, value *T) error {
|
|
|
|
|
var field any
|
|
|
|
|
err := viper.UnmarshalKey(key, &field)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
switch f := field.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
{
|
|
|
|
|
absPath := f
|
|
|
|
|
if strings.HasPrefix(absPath, "~/") {
|
|
|
|
|
home, err := os.UserHomeDir()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-03-28 23:58:14 +03:00
|
|
|
absPath = filepath.Join(home, absPath[2:])
|
2025-09-02 09:30:20 +02:00
|
|
|
}
|
|
|
|
|
if !filepath.IsAbs(absPath) {
|
|
|
|
|
base := configPath
|
|
|
|
|
if base == "" {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "unable to build relative path to config.")
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
absPath = filepath.Join(filepath.Dir(base), absPath)
|
|
|
|
|
}
|
|
|
|
|
b, err := os.ReadFile(absPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "%q", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
2026-03-20 12:54:16 +03:00
|
|
|
switch filepath.Ext(absPath) {
|
|
|
|
|
case ".json":
|
2025-09-02 09:30:20 +02:00
|
|
|
return json.Unmarshal(b, value)
|
2026-03-20 12:54:16 +03:00
|
|
|
case ".yaml", ".yml":
|
2025-09-02 09:30:20 +02:00
|
|
|
return yaml.Unmarshal(b, value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case map[string]any:
|
|
|
|
|
return viper.UnmarshalKey(key, value)
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("invalid type for field %q", key)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var configPath string
|
|
|
|
|
|
2025-11-12 16:48:46 +03:00
|
|
|
// SetConfigPath sets the configuration file path for resolving relative paths
|
|
|
|
|
// in configuration values. This should be called when the configuration file
|
|
|
|
|
// location is known.
|
2025-09-02 09:30:20 +02:00
|
|
|
func SetConfigPath(path string) {
|
|
|
|
|
configPath = path
|
|
|
|
|
}
|