Initial minimal mcp server for KorAP
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..361ae2b
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,158 @@
+package config
+
+import (
+	"fmt"
+	"os"
+)
+
+// Config represents the complete configuration for KorAP MCP server
+type Config struct {
+	// Server configuration
+	Server ServerConfig `yaml:"server" embed:""`
+
+	// OAuth2 authentication configuration
+	OAuth OAuthConfig `yaml:"oauth"`
+
+	// KorAP API configuration
+	KorAP KorAPConfig `yaml:"korap"`
+
+	// Logging configuration
+	Logging LoggingConfig `yaml:"logging"`
+}
+
+// ServerConfig represents server-specific configuration
+type ServerConfig struct {
+	// Name is the server name (constant, not configurable)
+	Name string `yaml:"-"`
+
+	// Version is the server version (constant, not configurable)
+	Version string `yaml:"-"`
+
+	// ConfigFile is the path to the configuration file (handled by CLI layer)
+	ConfigFile string `yaml:"-"`
+}
+
+// KorAPConfig represents KorAP API configuration
+type KorAPConfig struct {
+	// BaseURL is the KorAP server base URL
+	BaseURL string `yaml:"base_url" default:"https://korap.ids-mannheim.de" help:"KorAP server base URL"`
+
+	// APIVersion is the API version to use
+	APIVersion string `yaml:"api_version" default:"v1.0" help:"KorAP API version"`
+
+	// Timeout is the HTTP request timeout in seconds
+	Timeout int `yaml:"timeout" default:"30" help:"HTTP request timeout in seconds"`
+
+	// MaxRetries is the maximum number of retry attempts
+	MaxRetries int `yaml:"max_retries" default:"3" help:"Maximum number of retry attempts"`
+}
+
+// LoggingConfig represents logging configuration
+type LoggingConfig struct {
+	// Level is the logging level (trace, debug, info, warn, error)
+	Level string `yaml:"level" default:"info" enum:"trace,debug,info,warn,error" help:"Logging level"`
+
+	// Format is the log format (json, text)
+	Format string `yaml:"format" default:"text" enum:"json,text" help:"Log output format"`
+
+	// File is the log file path (empty for stdout)
+	File string `yaml:"file" help:"Log file path (empty for stdout)"`
+}
+
+// DefaultConfig returns a default configuration
+func DefaultConfig() *Config {
+	return &Config{
+		Server: ServerConfig{
+			// Name and Version are set by the CLI layer as constants
+		},
+		OAuth: *DefaultOAuthConfig(),
+		KorAP: KorAPConfig{
+			BaseURL:    "https://korap.ids-mannheim.de",
+			APIVersion: "v1.0",
+			Timeout:    30,
+			MaxRetries: 3,
+		},
+		Logging: LoggingConfig{
+			Level:  "info",
+			Format: "text",
+		},
+	}
+}
+
+// Validate validates the complete configuration
+func (c *Config) Validate() error {
+	if err := c.OAuth.Validate(); err != nil {
+		return fmt.Errorf("oauth config validation failed: %w", err)
+	}
+
+	if err := c.KorAP.Validate(); err != nil {
+		return fmt.Errorf("korap config validation failed: %w", err)
+	}
+
+	if err := c.Logging.Validate(); err != nil {
+		return fmt.Errorf("logging config validation failed: %w", err)
+	}
+
+	return nil
+}
+
+// Validate validates KorAP configuration
+func (k *KorAPConfig) Validate() error {
+	if k.BaseURL == "" {
+		return fmt.Errorf("base_url cannot be empty")
+	}
+
+	if k.APIVersion == "" {
+		return fmt.Errorf("api_version cannot be empty")
+	}
+
+	if k.Timeout <= 0 {
+		return fmt.Errorf("timeout must be positive")
+	}
+
+	if k.MaxRetries < 0 {
+		return fmt.Errorf("max_retries cannot be negative")
+	}
+
+	return nil
+}
+
+// Validate validates logging configuration
+func (l *LoggingConfig) Validate() error {
+	validLevels := map[string]bool{
+		"trace": true,
+		"debug": true,
+		"info":  true,
+		"warn":  true,
+		"error": true,
+	}
+
+	if !validLevels[l.Level] {
+		return fmt.Errorf("invalid log level: %s", l.Level)
+	}
+
+	validFormats := map[string]bool{
+		"json": true,
+		"text": true,
+	}
+
+	if !validFormats[l.Format] {
+		return fmt.Errorf("invalid log format: %s", l.Format)
+	}
+
+	// Check if log file is writable if specified
+	if l.File != "" {
+		file, err := os.OpenFile(l.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
+		if err != nil {
+			return fmt.Errorf("cannot write to log file %s: %w", l.File, err)
+		}
+		file.Close()
+	}
+
+	return nil
+}
+
+// GetKorAPEndpoint returns the full KorAP API endpoint URL
+func (k *KorAPConfig) GetKorAPEndpoint() string {
+	return fmt.Sprintf("%s/api/%s", k.BaseURL, k.APIVersion)
+}
diff --git a/config/config_test.go b/config/config_test.go
new file mode 100644
index 0000000..5482d0c
--- /dev/null
+++ b/config/config_test.go
@@ -0,0 +1,273 @@
+package config
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDefaultConfig(t *testing.T) {
+	cfg := DefaultConfig()
+
+	assert.NotNil(t, cfg)
+
+	// Note: Server name and version are now set by the CLI layer as constants
+	// so they will be empty in the default config
+
+	// Test KorAP defaults
+	assert.Equal(t, "https://korap.ids-mannheim.de", cfg.KorAP.BaseURL)
+	assert.Equal(t, "v1.0", cfg.KorAP.APIVersion)
+	assert.Equal(t, 30, cfg.KorAP.Timeout)
+	assert.Equal(t, 3, cfg.KorAP.MaxRetries)
+
+	// Test OAuth defaults
+	assert.False(t, cfg.OAuth.Enabled, "Expected OAuth to be disabled by default")
+
+	// Test logging defaults
+	assert.Equal(t, "info", cfg.Logging.Level)
+	assert.Equal(t, "text", cfg.Logging.Format)
+}
+
+func TestConfigValidate(t *testing.T) {
+	// Test valid configuration
+	cfg := DefaultConfig()
+	err := cfg.Validate()
+	assert.NoError(t, err)
+
+	// Test invalid OAuth configuration
+	cfg.OAuth.Enabled = true
+	cfg.OAuth.ClientID = ""
+	err = cfg.Validate()
+	assert.Error(t, err, "Expected validation error for empty OAuth client ID")
+
+	// Test invalid KorAP configuration
+	cfg = DefaultConfig()
+	cfg.KorAP.BaseURL = ""
+	err = cfg.Validate()
+	assert.Error(t, err, "Expected validation error for empty KorAP base URL")
+
+	// Test invalid logging configuration
+	cfg = DefaultConfig()
+	cfg.Logging.Level = "invalid"
+	err = cfg.Validate()
+	assert.Error(t, err, "Expected validation error for invalid log level")
+}
+
+func TestKorAPConfigValidate(t *testing.T) {
+	tests := []struct {
+		name      string
+		config    KorAPConfig
+		expectErr bool
+	}{
+		{
+			name: "valid config",
+			config: KorAPConfig{
+				BaseURL:    "https://korap.ids-mannheim.de",
+				APIVersion: "v1.0",
+				Timeout:    30,
+				MaxRetries: 3,
+			},
+			expectErr: false,
+		},
+		{
+			name: "empty base URL",
+			config: KorAPConfig{
+				BaseURL:    "",
+				APIVersion: "v1.0",
+				Timeout:    30,
+				MaxRetries: 3,
+			},
+			expectErr: true,
+		},
+		{
+			name: "empty API version",
+			config: KorAPConfig{
+				BaseURL:    "https://korap.ids-mannheim.de",
+				APIVersion: "",
+				Timeout:    30,
+				MaxRetries: 3,
+			},
+			expectErr: true,
+		},
+		{
+			name: "zero timeout",
+			config: KorAPConfig{
+				BaseURL:    "https://korap.ids-mannheim.de",
+				APIVersion: "v1.0",
+				Timeout:    0,
+				MaxRetries: 3,
+			},
+			expectErr: true,
+		},
+		{
+			name: "negative timeout",
+			config: KorAPConfig{
+				BaseURL:    "https://korap.ids-mannheim.de",
+				APIVersion: "v1.0",
+				Timeout:    -1,
+				MaxRetries: 3,
+			},
+			expectErr: true,
+		},
+		{
+			name: "negative max retries",
+			config: KorAPConfig{
+				BaseURL:    "https://korap.ids-mannheim.de",
+				APIVersion: "v1.0",
+				Timeout:    30,
+				MaxRetries: -1,
+			},
+			expectErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.config.Validate()
+			if tt.expectErr {
+				assert.Error(t, err, "Expected validation error but got none")
+			} else {
+				assert.NoError(t, err, "Unexpected validation error")
+			}
+		})
+	}
+}
+
+func TestKorAPConfigGetEndpoint(t *testing.T) {
+	config := KorAPConfig{
+		BaseURL:    "https://korap.ids-mannheim.de",
+		APIVersion: "v1.0",
+	}
+
+	expected := "https://korap.ids-mannheim.de/api/v1.0"
+	actual := config.GetKorAPEndpoint()
+
+	assert.Equal(t, expected, actual)
+}
+
+func TestLoggingConfigValidate(t *testing.T) {
+	tests := []struct {
+		name      string
+		config    LoggingConfig
+		expectErr bool
+	}{
+		{
+			name: "valid text format",
+			config: LoggingConfig{
+				Level:  "info",
+				Format: "text",
+			},
+			expectErr: false,
+		},
+		{
+			name: "valid json format",
+			config: LoggingConfig{
+				Level:  "debug",
+				Format: "json",
+			},
+			expectErr: false,
+		},
+		{
+			name: "invalid level",
+			config: LoggingConfig{
+				Level:  "invalid",
+				Format: "text",
+			},
+			expectErr: true,
+		},
+		{
+			name: "invalid format",
+			config: LoggingConfig{
+				Level:  "info",
+				Format: "invalid",
+			},
+			expectErr: true,
+		},
+		{
+			name: "all log levels",
+			config: LoggingConfig{
+				Level:  "trace",
+				Format: "json",
+			},
+			expectErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.config.Validate()
+			if tt.expectErr {
+				assert.Error(t, err, "Expected validation error but got none")
+			} else {
+				assert.NoError(t, err, "Unexpected validation error")
+			}
+		})
+	}
+}
+
+func TestLoggingConfigValidateWithFile(t *testing.T) {
+	// Create a temporary directory for testing
+	tempDir, err := os.MkdirTemp("", "korap-mcp-test")
+	assert.NoError(t, err, "Failed to create temp dir")
+	defer os.RemoveAll(tempDir)
+
+	// Create a writable log file
+	logFile := filepath.Join(tempDir, "test.log")
+	file, err := os.Create(logFile)
+	assert.NoError(t, err)
+	file.Close()
+
+	config := LoggingConfig{
+		Level:  "info",
+		Format: "json",
+		File:   logFile,
+	}
+
+	err = config.Validate()
+	assert.NoError(t, err, "Validation failed for writable log file")
+
+	// Test with invalid log file path
+	config.File = "/root/invalid.log"
+	err = config.Validate()
+	assert.Error(t, err, "Expected validation error for invalid log file path")
+}
+
+func TestServerConfigDefaults(t *testing.T) {
+	cfg := DefaultConfig()
+
+	assert.Empty(t, cfg.Server.ConfigFile, "Expected empty config file by default")
+
+	// Name and version are now constants set by CLI layer, so they will be empty here
+	assert.Empty(t, cfg.Server.Name, "Server name should be empty in default config (set by CLI constants)")
+	assert.Empty(t, cfg.Server.Version, "Server version should be empty in default config (set by CLI constants)")
+}
+
+// Benchmark tests
+func BenchmarkDefaultConfig(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		_ = DefaultConfig()
+	}
+}
+
+func BenchmarkConfigValidate(b *testing.B) {
+	cfg := DefaultConfig()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		_ = cfg.Validate()
+	}
+}
+
+func BenchmarkKorAPGetEndpoint(b *testing.B) {
+	config := KorAPConfig{
+		BaseURL:    "https://korap.ids-mannheim.de",
+		APIVersion: "v1.0",
+	}
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		_ = config.GetKorAPEndpoint()
+	}
+}
diff --git a/config/oauth.go b/config/oauth.go
new file mode 100644
index 0000000..b015f16
--- /dev/null
+++ b/config/oauth.go
@@ -0,0 +1,83 @@
+package config
+
+import (
+	"fmt"
+
+	"golang.org/x/oauth2"
+)
+
+// OAuthConfig represents OAuth2 configuration for KorAP authentication
+type OAuthConfig struct {
+	// ClientID is the OAuth2 client identifier
+	ClientID string `yaml:"client_id"`
+
+	// ClientSecret is the OAuth2 client secret
+	ClientSecret string `yaml:"client_secret"`
+
+	// AuthURL is the authorization endpoint URL
+	AuthURL string `yaml:"auth_url"`
+
+	// TokenURL is the token endpoint URL
+	TokenURL string `yaml:"token_url"`
+
+	// RedirectURL is the callback URL for authorization code flow
+	RedirectURL string `yaml:"redirect_url"`
+
+	// Scopes are the requested OAuth2 scopes
+	Scopes []string `yaml:"scopes"`
+
+	// Enabled indicates whether OAuth2 authentication is enabled
+	Enabled bool `yaml:"enabled"`
+}
+
+// DefaultOAuthConfig returns a default OAuth2 configuration
+func DefaultOAuthConfig() *OAuthConfig {
+	return &OAuthConfig{
+		AuthURL:     "https://korap.ids-mannheim.de/api/v1.0/oauth2/authorize",
+		TokenURL:    "https://korap.ids-mannheim.de/api/v1.0/oauth2/token",
+		RedirectURL: "urn:ietf:wg:oauth:2.0:oob",
+		Scopes:      []string{"read"},
+		Enabled:     false,
+	}
+}
+
+// ToOAuth2Config converts the config to golang.org/x/oauth2.Config
+func (c *OAuthConfig) ToOAuth2Config() *oauth2.Config {
+	if !c.Enabled {
+		return nil
+	}
+
+	return &oauth2.Config{
+		ClientID:     c.ClientID,
+		ClientSecret: c.ClientSecret,
+		Endpoint: oauth2.Endpoint{
+			AuthURL:  c.AuthURL,
+			TokenURL: c.TokenURL,
+		},
+		RedirectURL: c.RedirectURL,
+		Scopes:      c.Scopes,
+	}
+}
+
+// Validate checks if the OAuth2 configuration is valid
+func (c *OAuthConfig) Validate() error {
+	if !c.Enabled {
+		return nil
+	}
+
+	if c.ClientID == "" {
+		return fmt.Errorf("oauth2 client_id is required when authentication is enabled")
+	}
+
+	if c.ClientSecret == "" {
+		return fmt.Errorf("oauth2 client_secret is required when authentication is enabled")
+	}
+
+	if c.TokenURL == "" {
+		return fmt.Errorf("oauth2 token_url is required when authentication is enabled")
+	}
+
+	// AuthURL is only required for authorization code flow, not client credentials
+
+	return nil
+}
diff --git a/config/oauth_test.go b/config/oauth_test.go
new file mode 100644
index 0000000..e658910
--- /dev/null
+++ b/config/oauth_test.go
@@ -0,0 +1,259 @@
+package config
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDefaultOAuthConfig(t *testing.T) {
+	config := DefaultOAuthConfig()
+
+	assert.NotNil(t, config)
+
+	// Test default values
+	assert.Equal(t, "https://korap.ids-mannheim.de/api/v1.0/oauth2/authorize", config.AuthURL)
+	assert.Equal(t, "https://korap.ids-mannheim.de/api/v1.0/oauth2/token", config.TokenURL)
+	assert.Equal(t, "urn:ietf:wg:oauth:2.0:oob", config.RedirectURL)
+	assert.Equal(t, []string{"read"}, config.Scopes)
+	assert.False(t, config.Enabled)
+
+	// Test that required fields are empty by default
+	assert.Empty(t, config.ClientID)
+	assert.Empty(t, config.ClientSecret)
+
+	// Test ToOAuth2Config with disabled config
+	oauth2Config := config.ToOAuth2Config()
+	assert.Nil(t, oauth2Config, "ToOAuth2Config should return nil when disabled")
+
+	// Test validation of default (disabled) config
+	err := config.Validate()
+	assert.NoError(t, err, "Default config should be valid (disabled)")
+}
+
+func TestOAuthConfigValidate(t *testing.T) {
+	tests := []struct {
+		name      string
+		config    *OAuthConfig
+		wantError bool
+		errorMsg  string
+	}{
+		{
+			name: "valid enabled config",
+			config: &OAuthConfig{
+				ClientID:     "test-client",
+				ClientSecret: "test-secret",
+				AuthURL:      "https://example.com/auth",
+				TokenURL:     "https://example.com/token",
+				RedirectURL:  "https://example.com/callback",
+				Scopes:       []string{"read"},
+				Enabled:      true,
+			},
+			wantError: false,
+		},
+		{
+			name: "valid disabled config",
+			config: &OAuthConfig{
+				Enabled: false,
+			},
+			wantError: false,
+		},
+		{
+			name: "missing client ID",
+			config: &OAuthConfig{
+				ClientSecret: "test-secret",
+				AuthURL:      "https://example.com/auth",
+				TokenURL:     "https://example.com/token",
+				Enabled:      true,
+			},
+			wantError: true,
+			errorMsg:  "client_id is required",
+		},
+		{
+			name: "missing client secret",
+			config: &OAuthConfig{
+				ClientID: "test-client",
+				AuthURL:  "https://example.com/auth",
+				TokenURL: "https://example.com/token",
+				Enabled:  true,
+			},
+			wantError: true,
+			errorMsg:  "client_secret is required",
+		},
+		{
+			name: "missing token URL",
+			config: &OAuthConfig{
+				ClientID:     "test-client",
+				ClientSecret: "test-secret",
+				AuthURL:      "https://example.com/auth",
+				Enabled:      true,
+			},
+			wantError: true,
+			errorMsg:  "token_url is required",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.config.Validate()
+
+			if tt.wantError {
+				assert.Error(t, err)
+				assert.Contains(t, err.Error(), tt.errorMsg)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestOAuthConfigToOAuth2Config(t *testing.T) {
+	tests := []struct {
+		name     string
+		config   *OAuthConfig
+		wantNil  bool
+		testFunc func(*testing.T, *OAuthConfig)
+	}{
+		{
+			name: "disabled config returns nil",
+			config: &OAuthConfig{
+				ClientID:     "test-client",
+				ClientSecret: "test-secret",
+				Enabled:      false,
+			},
+			wantNil: true,
+		},
+		{
+			name: "enabled config returns valid oauth2.Config",
+			config: &OAuthConfig{
+				ClientID:     "test-client",
+				ClientSecret: "test-secret",
+				AuthURL:      "https://example.com/auth",
+				TokenURL:     "https://example.com/token",
+				RedirectURL:  "https://example.com/callback",
+				Scopes:       []string{"read", "write"},
+				Enabled:      true,
+			},
+			wantNil: false,
+			testFunc: func(t *testing.T, config *OAuthConfig) {
+				oauth2Config := config.ToOAuth2Config()
+				assert.NotNil(t, oauth2Config)
+				assert.Equal(t, config.ClientID, oauth2Config.ClientID)
+				assert.Equal(t, config.ClientSecret, oauth2Config.ClientSecret)
+				assert.Equal(t, config.AuthURL, oauth2Config.Endpoint.AuthURL)
+				assert.Equal(t, config.TokenURL, oauth2Config.Endpoint.TokenURL)
+				assert.Equal(t, config.RedirectURL, oauth2Config.RedirectURL)
+				assert.Equal(t, config.Scopes, oauth2Config.Scopes)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			oauth2Config := tt.config.ToOAuth2Config()
+
+			if tt.wantNil {
+				assert.Nil(t, oauth2Config)
+			} else {
+				assert.NotNil(t, oauth2Config)
+				if tt.testFunc != nil {
+					tt.testFunc(t, tt.config)
+				}
+			}
+		})
+	}
+}
+
+func TestOAuthConfigEdgeCases(t *testing.T) {
+	t.Run("empty scopes", func(t *testing.T) {
+		config := &OAuthConfig{
+			ClientID:     "test-client",
+			ClientSecret: "test-secret",
+			AuthURL:      "https://example.com/auth",
+			TokenURL:     "https://example.com/token",
+			Scopes:       []string{},
+			Enabled:      true,
+		}
+
+		err := config.Validate()
+		assert.NoError(t, err, "Empty scopes should be valid")
+
+		oauth2Config := config.ToOAuth2Config()
+		assert.NotNil(t, oauth2Config)
+		assert.Empty(t, oauth2Config.Scopes)
+	})
+
+	t.Run("nil scopes", func(t *testing.T) {
+		config := &OAuthConfig{
+			ClientID:     "test-client",
+			ClientSecret: "test-secret",
+			AuthURL:      "https://example.com/auth",
+			TokenURL:     "https://example.com/token",
+			Scopes:       nil,
+			Enabled:      true,
+		}
+
+		err := config.Validate()
+		assert.NoError(t, err, "Nil scopes should be valid")
+
+		oauth2Config := config.ToOAuth2Config()
+		assert.NotNil(t, oauth2Config)
+		assert.Nil(t, oauth2Config.Scopes)
+	})
+}
+
+func TestOAuthConfigScopesHandling(t *testing.T) {
+	config := &OAuthConfig{
+		ClientID:     "test-client",
+		ClientSecret: "test-secret",
+		AuthURL:      "https://example.com/auth",
+		TokenURL:     "https://example.com/token",
+		RedirectURL:  "https://example.com/callback",
+		Scopes:       []string{"read", "write", "admin"},
+		Enabled:      true,
+	}
+
+	oauth2Config := config.ToOAuth2Config()
+	assert.NotNil(t, oauth2Config)
+	assert.Len(t, oauth2Config.Scopes, 3)
+	assert.Contains(t, oauth2Config.Scopes, "read")
+	assert.Contains(t, oauth2Config.Scopes, "write")
+	assert.Contains(t, oauth2Config.Scopes, "admin")
+}
+
+// Benchmark tests
+func BenchmarkDefaultOAuthConfig(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		_ = DefaultOAuthConfig()
+	}
+}
+
+func BenchmarkOAuthConfigValidate(b *testing.B) {
+	cfg := DefaultOAuthConfig()
+	cfg.Enabled = true
+	cfg.ClientID = "test-client"
+	cfg.ClientSecret = "test-secret"
+	cfg.TokenURL = "https://example.com/token"
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		_ = cfg.Validate()
+	}
+}
+
+func BenchmarkOAuthConfigToOAuth2Config(b *testing.B) {
+	cfg := &OAuthConfig{
+		Enabled:      true,
+		ClientID:     "test-client",
+		ClientSecret: "test-secret",
+		AuthURL:      "https://example.com/auth",
+		TokenURL:     "https://example.com/token",
+		RedirectURL:  "https://example.com/callback",
+		Scopes:       []string{"read", "write"},
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		_ = cfg.ToOAuth2Config()
+	}
+}