#
tokens: 22628/50000 12/12 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── .goreleaser.yml
├── CHANGELOG.md
├── cmd
│   └── mcp-todo-server
│       └── main.go
├── go.mod
├── go.sum
├── internal
│   ├── config
│   │   └── config.go
│   ├── mcp
│   │   ├── server.go
│   │   └── tools.go
│   └── services
│       ├── changelog
│       │   └── changelog.go
│       ├── file
│       │   └── file.go
│       └── todo
│           └── todo.go
├── LICENSE
├── README.md
└── TODO.md
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
build/
dist/
notes/
tmp/

```

--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------

```yaml
project_name: mcp-todo-server

before:
  hooks:
    - go mod tidy

builds:
  - env:
      - CGO_ENABLED=0
    goos:
      - linux
      - windows
      - darwin
    goarch:
      - amd64
      - arm64
    main: ./cmd/mcp-todo-server
    ldflags:
      - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}

archives:
  - format: tar.gz
    name_template: >-
      {{ .ProjectName }}_
      {{- .Version }}_
      {{- .Os }}_
      {{- .Arch }}
    format_overrides:
      - goos: windows
        format: zip
    files:
      - README.md
      - LICENSE

checksum:
  name_template: 'checksums.txt'

snapshot:
  name_template: "{{ incpatch .Version }}-next"

changelog:
  sort: asc
  filters:
    exclude:
      - '^docs:'
      - '^test:'
      - '^ci:'
      - Merge pull request
      - Merge branch

# Homebrew
brews:
  - tap:
      owner: mutker
      name: homebrew-tap
      token: "{{ .Env.GITHUB_TOKEN }}"
    folder: Formula
    homepage: https://codeberg.org/mutker/mcp-todo-server
    description: MCP server for managing TODO.md and CHANGELOG.md files
    license: MIT
    test: |
      system "#{bin}/mcp-todo-server --version"
    install: |
      bin.install "mcp-todo-server"
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# mcp-todo-server

Model Context Protocol (MCP) server for managing TODO.md and CHANGELOG.md files.

## Features

- Precise, line-based editing and reading of file contents.
- Efficient partial file access using line ranges, for efficient LLM tool usage.
- Retrieve specific file content by specifying line ranges.
- Fetch multiple line ranges from multiple files in a single request.
- Apply line-based patches, correctly adjusting for line number changes.
- Supports a wide range of character encodings (utf-8, shift_jis, latin1, etc.).
- Perform atomic operations across multiple files.
- Robust error handling using custom error types.
- Adheres to Semantic Versioning and Keep a Changelog conventions.

## Requirements

- Go v1.23+
- Linux, macOS, or Windows
- File system permissions for read/write operations

## Installation

```bash
go install codeberg.org/mutker/mcp-todo-server/cmd/mcp-todo-server@latest
```

## Usage examples:

- Ask "What are my current tasks for version 0.2.0?"
- Say "Add a new task to implement OAuth authentication for version 0.2.0"
- Request "Generate a changelog entry for version 0.1.0 based on completed tasks"
- Say "Import my existing TODO.md file from /path/to/my/TODO.md"

The server intelligently handles task parsing, version management, and provides rich semantic understanding of tasks and changelog entries.

## Available MCP Tools

### TODO.md Operations

- `get-todo-tasks` - Get all tasks from TODO.md
- `get-todo-tasks-by-version` - Get tasks for a specific version
- `add-todo-task` - Add a new task for a specific version
- `update-todo-task` - Update an existing task
- `add-todo-version` - Add a new version section
- `import-todo` - Import and format an existing TODO.md

### CHANGELOG.md Operations

- `get-changelog` - Get all changelog entries
- `get-changelog-by-version` - Get changelog entries for a specific version
- `add-changelog-entry` - Add a new changelog version entry
- `update-changelog-entry` - Update an existing changelog entry
- `import-changelog` - Import and format an existing CHANGELOG.md
- `generate-changelog-from-todo` - Generate a new CHANGELOG.md entry based on completed tasks in TODO.md

## Thanks

- [tumf/mcp-text-editor](https://github.com/tumf/mcp-text-editor) for the inspiration.

## License

This project is licensed under the MIT License. See [LICENSE](LICENSE) for the full license text.

```

--------------------------------------------------------------------------------
/internal/mcp/server.go:
--------------------------------------------------------------------------------

```go
package mcp

import (
	"encoding/json"
	"github.com/mark3labs/mcp-go/mcp"
)

// Helper function to create a tool result with JSON content
func newToolResultJSON(v any) (*mcp.CallToolResult, error) {
	jsonBytes, err := json.Marshal(v)
	if err != nil {
		return nil, err
	}
	return mcp.NewToolResultText(string(jsonBytes)), nil
}

// convertParams is a utility function that converts parameters from JSON
// into a structured type. This is useful for processing tool parameters.
func convertParams(params interface{}, dest interface{}) error {
	if params == nil {
		return nil
	}

	// Convert to JSON and unmarshal into the destination struct
	paramsJSON, err := json.Marshal(params)
	if err != nil {
		return err
	}

	return json.Unmarshal(paramsJSON, dest)
}

```

--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------

```go
package config

import (
	"os"
)

// Config holds the application configuration
type Config struct {
	Files FilesConfig
}

// FilesConfig holds configuration related to file operations
type FilesConfig struct {
	DefaultEncoding string
	AutoDetection   bool
}

// Load loads configuration from environment variables
func Load() (*Config, error) {
	// Set defaults
	cfg := &Config{
		Files: FilesConfig{
			DefaultEncoding: "utf-8",
			AutoDetection:   true,
		},
	}

	// Override with environment variables
	if encoding := os.Getenv("MCP_DEFAULT_ENCODING"); encoding != "" {
		cfg.Files.DefaultEncoding = encoding
	}
	
	if autoDetect := os.Getenv("MCP_AUTO_DETECT_ENCODING"); autoDetect != "" {
		cfg.Files.AutoDetection = autoDetect == "1" || autoDetect == "true" || autoDetect == "yes"
	}

	return cfg, nil
}
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
# Changelog

## [0.3.0] - 2025-03-05

### Added

- Support for mcp-go library integration
  - Complete refactoring to use mcp-go for protocol handling
  - Improved type safety with mcp-go parameter handling
- Enhanced character encoding support
  - Automatic encoding detection for files
  - Support for additional character encodings (Chinese, Korean, and more)
  - Format conversion utilities for seamless encoding management

### Changed

- Simplified server implementation by leveraging mcp-go functionality
- Enhanced tool parameter definitions with proper schema support
- Improved error handling with consistent error responses
- Updated tool implementations to maintain backward compatibility

### Removed

- Custom MCP protocol implementation in favor of the standard mcp-go library

## [0.2.0] - 2025-03-04

### Added

- Model Context Protocol (MCP) server implementation
  - JSON-RPC communication layer
  - MCP protocol initialization and capability negotiation
  - Tool registration and execution system
  - Stdin/stdout transport support
- MCP tools for TODO.md
  - Get all tasks
  - Get tasks for a specific version
  - Add a new task
  - Update an existing task
  - Add a new version section
  - Import and format an existing TODO.md
- MCP tools for CHANGELOG.md
  - Get all changelog items
  - Get changelog items for a specific version
  - Add a new changelog version entry
  - Update existing changelog entries
  - Import and format an existing CHANGELOG.md
  - Generate a new CHANGELOG.md based on completed tasks in TODO.md

### Changed

- Updated README with MCP server documentation
- Improved error handling and validation for tools

## [0.1.0] - 2025-03-03

### Added

- Initial implementation of MCP Todo Server
- Server framework with error handling and middleware
- File operations with line-based reading and editing
- Hash-based validation for concurrent editing
- TODO.md management operations
  - Parse TODO format
  - Read and update tasks
  - Add tasks and versions
- CHANGELOG.md management operations
  - Parse CHANGELOG format
  - Read and update entries
  - Add new entries
- Multi-file atomic operations
- Comprehensive encoding support (utf-8, shift_jis, latin1, etc.)
- Goreleaser configuration for multi-platform builds

```

--------------------------------------------------------------------------------
/cmd/mcp-todo-server/main.go:
--------------------------------------------------------------------------------

```go
package main

import (
	"context"
	"flag"
	"fmt"
	"log/slog"
	"os"
	"os/signal"
	"syscall"
	"time"

	"codeberg.org/mutker/mcp-todo-server/internal/config"
	"codeberg.org/mutker/mcp-todo-server/internal/mcp"
	"github.com/mark3labs/mcp-go/server"
)

var (
	version     = "0.3.0"
	verboseFlag = flag.Bool("verbose", false, "Enable verbose logging")
	versionFlag = flag.Bool("version", false, "Show version information")
)

func main() {
	// Define command-line flags
	flag.Parse()

	// Show version and exit if requested
	if *versionFlag {
		fmt.Printf("mcp-todo-server version %s\n", version)
		os.Exit(0)
	}

	// Initialize logger to write to stderr instead of stdout
	// This is necessary because stdout is reserved for MCP JSON-RPC communication
	logLevel := slog.LevelInfo
	if *verboseFlag {
		logLevel = slog.LevelDebug
	}
	
	// Create a custom handler that formats logs in a more human-readable format
	handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
		Level: logLevel,
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			// Format timestamp to be more human readable
			if a.Key == "time" {
				if t, ok := a.Value.Any().(time.Time); ok {
					return slog.Attr{
						Key:   "time",
						Value: slog.StringValue(t.Format("2006-01-02 15:04:05")),
					}
				}
			}
			return a
		},
	})
	
	logger := slog.New(handler)
	slog.SetDefault(logger)

	// Load configuration
	cfg, err := config.Load()
	if err != nil {
		logger.Error("Failed to load configuration", "error", err)
		os.Exit(1)
	}

	// Print version information
	logger.Info(fmt.Sprintf("Starting MCP Todo Server v%s", version))

	// Create context for graceful shutdown
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Create a channel to listen for OS signals
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

	// Create and start the MCP server
	mcpServer := server.NewMCPServer(
		"mcp-todo-server",
		version,
		server.WithResourceCapabilities(false, false), // Disable resources for now
		server.WithLogging(),
	)

	// Register tools
	mcp.RegisterTools(ctx, mcpServer, cfg)

	// Start the MCP server in a goroutine
	go func() {
		logger.Info("MCP server ready and listening on stdin/stdout")
		if err := server.ServeStdio(mcpServer); err != nil {
			logger.Error(fmt.Sprintf("MCP server error: %v", err))
			os.Exit(1)
		}
	}()

	// Wait for signal
	<-quit
	logger.Info("Received shutdown signal, closing server...")

	// Cancel the context to initiate shutdown
	cancel()

	// Give it a moment to clean up
	// time.Sleep(500 * time.Millisecond) // No need with mcp-go

	logger.Info("MCP Todo Server has shut down gracefully")
}

```

--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------

```markdown
# TODO

## v0.1.0

- [x] Set up project structure
  - [x] Create Go module
  - [x] Set up directory structure (cmd, internal, pkg)
  - [x] Add gitignore file
- [x] Implement file operations
  - [x] Design file service interfaces
  - [x] Implement line-based file reading with range support
  - [x] Implement line-based editing operations
  - [x] Add hash-based validation for concurrent editing
- [x] Implement TODO.md operations
  - [x] Parse TODO.md format
  - [x] Implement operations for reading TODO items
  - [x] Implement operations for updating task status
  - [x] Implement operations for adding new tasks and versions
- [x] Implement CHANGELOG.md operations
  - [x] Parse CHANGELOG.md format
  - [x] Implement operations for reading changelog entries
  - [x] Implement operations for adding new entries
  - [x] Implement operations for updating existing entries
- [x] Add multi-file atomic operations
  - [x] Implement transaction-like behavior for multi-file edits
  - [x] Add rollback capability for failed operations
- [x] Add encoding support
  - [x] Implement detection and handling of various encodings
  - [x] Add support for utf-8, shift_jis, latin1
- [x] Set up goreleaser configuration
  - [x] Create .goreleaser.yml
  - [x] Configure build settings for different platforms
  - [x] Set up release workflow

## v0.2.0

- [x] Implement MCP server
  - [x] Create JSON-RPC communication layer
  - [x] Implement MCP protocol initialization
  - [x] Add tool registration and execution
  - [x] Support stdin/stdout transport
- [x] Implement MCP tools for TODO.md
  - [x] Get all tasks
  - [x] Get tasks for a specific version
  - [x] Add a new task
  - [x] Add a new task for a specific version
  - [x] Update an existing task
  - [x] Add a new version section
  - [x] Import and format an existing TODO.md
- [x] Implement MCP tools for CHANGELOG.md
  - [x] Get all changelog items
  - [x] Get changelog items for a specific version
  - [x] Add a new changelog version entry
  - [x] Add a new changelog entry for a specific version
  - [x] Update a existing changelog entry
  - [x] Import and format an existing CHANGELOG.md
  - [x] Generate a new CHANGELOG.md based on completed tasks in TODO.md

## v0.3.0

- [x] Refactor to use `mcp-go` library
  - [x] Replace custom MCP implementation with `mcp-go`
  - [x] Adapt tool handlers to `mcp-go` API
  - [x] Ensure all TODO.md operations are supported
  - [x] Ensure all CHANGELOG.md operations are supported
- [x] Clean up legacy MCP server code
  - [x] Remove obsolete code replaced by mcp-go
  - [x] Refactor server initialization
  - [x] Update error handling for MCP protocol
- [x] Support for additional character encodings and format detection
  - [x] Add automatic encoding detection
  - [x] Support for more exotic character encodings
  - [x] Format conversion utilities

```

--------------------------------------------------------------------------------
/internal/services/changelog/changelog.go:
--------------------------------------------------------------------------------

```go
package changelog

import (
	"errors"
	"fmt"
	"regexp"
	"strings"

	"codeberg.org/mutker/mcp-todo-server/internal/config"
	"codeberg.org/mutker/mcp-todo-server/internal/services/file"
)

const (
	// Default CHANGELOG file name
	changelogFileName = "CHANGELOG.md"
)

var (
	// ErrVersionNotFound is returned when a version is not found
	ErrVersionNotFound = errors.New("version not found")
	
	// ErrVersionExists is returned when a version already exists
	ErrVersionExists = errors.New("version already exists")
)

// versionRegex matches a version header (e.g., "## [1.0.0] - 2023-01-01")
var versionRegex = regexp.MustCompile(`^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$`)

// sectionRegex matches a section header (e.g., "### Added" or "### Fixed")
var sectionRegex = regexp.MustCompile(`^### ([A-Za-z]+)$`)

// listItemRegex matches a list item (e.g., "- List item")
var listItemRegex = regexp.MustCompile(`^(\s*)- (.+)$`)

// ChangelogContent represents the content of a changelog entry
type ChangelogContent struct {
	Added      []string `json:"added,omitempty"`
	Changed    []string `json:"changed,omitempty"`
	Deprecated []string `json:"deprecated,omitempty"`
	Removed    []string `json:"removed,omitempty"`
	Fixed      []string `json:"fixed,omitempty"`
	Security   []string `json:"security,omitempty"`
}

// VersionEntry represents a version entry in the changelog
type VersionEntry struct {
	Version string           `json:"version"`
	Date    string           `json:"date"`
	Content *ChangelogContent `json:"content"`
}

// Changelog represents the entire CHANGELOG.md file
type Changelog struct {
	Versions []*VersionEntry `json:"versions"`
}

// Service provides changelog operations
type Service struct {
	config     *config.Config
	fileService *file.Service
}

// NewService creates a new changelog service
func NewService(cfg *config.Config) *Service {
	fileService := file.NewService(cfg.Files.DefaultEncoding, cfg.Files.AutoDetection)
	
	return &Service{
		config:     cfg,
		fileService: fileService,
	}
}

// GetAll retrieves the entire changelog
func (s *Service) GetAll() (*Changelog, error) {
	lines, _, err := s.fileService.ReadLines(changelogFileName, s.config.Files.DefaultEncoding)
	if err != nil {
		if errors.Is(err, file.ErrFileNotFound) {
			// Return empty changelog if file doesn't exist
			return &Changelog{Versions: []*VersionEntry{}}, nil
		}
		return nil, fmt.Errorf("failed to read CHANGELOG file: %w", err)
	}
	
	return s.parseChangelogFile(lines)
}

// GetByVersion retrieves a specific version entry
func (s *Service) GetByVersion(version string) (*VersionEntry, error) {
	changelog, err := s.GetAll()
	if err != nil {
		return nil, err
	}
	
	// Find the requested version
	for _, v := range changelog.Versions {
		if v.Version == version {
			return v, nil
		}
	}
	
	return nil, nil
}

// AddEntry adds a new changelog entry
func (s *Service) AddEntry(version string, date string, content *ChangelogContent) error {
	lines, hash, err := s.fileService.ReadLines(changelogFileName, s.config.Files.DefaultEncoding)
	if err != nil && !errors.Is(err, file.ErrFileNotFound) {
		return fmt.Errorf("failed to read CHANGELOG file: %w", err)
	}
	
	// If file doesn't exist, create it
	if errors.Is(err, file.ErrFileNotFound) {
		newLines := []string{
			"# Changelog",
			"",
		}
		err = s.fileService.InsertLines(changelogFileName, "", s.config.Files.DefaultEncoding, 1, newLines)
		if err != nil {
			return fmt.Errorf("failed to create CHANGELOG file: %w", err)
		}
		
		// Re-read the file
		lines, hash, err = s.fileService.ReadLines(changelogFileName, s.config.Files.DefaultEncoding)
		if err != nil {
			return fmt.Errorf("failed to read CHANGELOG file: %w", err)
		}
	}
	
	// Parse the file to check if version already exists
	changelog, err := s.parseChangelogFile(lines)
	if err != nil {
		return fmt.Errorf("failed to parse CHANGELOG file: %w", err)
	}
	
	// Check if version already exists
	for _, v := range changelog.Versions {
		if v.Version == version {
			return ErrVersionExists
		}
	}
	
	// Generate the entry
	entryLines := []string{}
	
	// Add version header
	entryLines = append(entryLines, fmt.Sprintf("## [%s] - %s", version, date))
	entryLines = append(entryLines, "")
	
	// Add sections
	if len(content.Added) > 0 {
		entryLines = append(entryLines, "### Added")
		entryLines = append(entryLines, "")
		for _, item := range content.Added {
			entryLines = append(entryLines, fmt.Sprintf("- %s", item))
		}
		entryLines = append(entryLines, "")
	}
	
	if len(content.Changed) > 0 {
		entryLines = append(entryLines, "### Changed")
		entryLines = append(entryLines, "")
		for _, item := range content.Changed {
			entryLines = append(entryLines, fmt.Sprintf("- %s", item))
		}
		entryLines = append(entryLines, "")
	}
	
	if len(content.Deprecated) > 0 {
		entryLines = append(entryLines, "### Deprecated")
		entryLines = append(entryLines, "")
		for _, item := range content.Deprecated {
			entryLines = append(entryLines, fmt.Sprintf("- %s", item))
		}
		entryLines = append(entryLines, "")
	}
	
	if len(content.Removed) > 0 {
		entryLines = append(entryLines, "### Removed")
		entryLines = append(entryLines, "")
		for _, item := range content.Removed {
			entryLines = append(entryLines, fmt.Sprintf("- %s", item))
		}
		entryLines = append(entryLines, "")
	}
	
	if len(content.Fixed) > 0 {
		entryLines = append(entryLines, "### Fixed")
		entryLines = append(entryLines, "")
		for _, item := range content.Fixed {
			entryLines = append(entryLines, fmt.Sprintf("- %s", item))
		}
		entryLines = append(entryLines, "")
	}
	
	if len(content.Security) > 0 {
		entryLines = append(entryLines, "### Security")
		entryLines = append(entryLines, "")
		for _, item := range content.Security {
			entryLines = append(entryLines, fmt.Sprintf("- %s", item))
		}
		entryLines = append(entryLines, "")
	}
	
	// Find position to insert the new entry
	// We'll add it after the title and before any other version
	position := 2 // After "# Changelog" and empty line
	for i, line := range lines {
		if versionRegex.MatchString(line) {
			position = i
			break
		}
	}
	
	// Insert the entry
	err = s.fileService.InsertLines(changelogFileName, hash, s.config.Files.DefaultEncoding, position, entryLines)
	if err != nil {
		return fmt.Errorf("failed to add entry: %w", err)
	}
	
	return nil
}

// UpdateEntry updates an existing changelog entry
func (s *Service) UpdateEntry(version string, date string, content *ChangelogContent) error {
	lines, hash, err := s.fileService.ReadLines(changelogFileName, s.config.Files.DefaultEncoding)
	if err != nil {
		return fmt.Errorf("failed to read CHANGELOG file: %w", err)
	}
	
	// Parse the file to locate the version
	changelog, err := s.parseChangelogFile(lines)
	if err != nil {
		return fmt.Errorf("failed to parse CHANGELOG file: %w", err)
	}
	
	// Find the version entry
	var targetVersion *VersionEntry
	for _, v := range changelog.Versions {
		if v.Version == version {
			targetVersion = v
			break
		}
	}
	
	if targetVersion == nil {
		return ErrVersionNotFound
	}
	
	// Find the start and end line numbers for this version
	startLine := 0
	endLine := len(lines)
	
	for i, line := range lines {
		if versionRegex.MatchString(line) {
			versionMatch := versionRegex.FindStringSubmatch(line)
			if versionMatch[1] == version {
				startLine = i
				
				// Find the end line (next version or end of file)
				for j := i + 1; j < len(lines); j++ {
					if versionRegex.MatchString(lines[j]) {
						endLine = j
						break
					}
				}
				break
			}
		}
	}
	
	// Generate new version entry
	newLines := []string{}
	
	// Update version header if date is provided
	if date != "" {
		newLines = append(newLines, fmt.Sprintf("## [%s] - %s", version, date))
	} else {
		newLines = append(newLines, lines[startLine]) // Keep original date
	}
	newLines = append(newLines, "")
	
	// Add sections
	// We'll generate completely new content rather than trying to modify existing content
	if content.Added != nil && len(content.Added) > 0 {
		newLines = append(newLines, "### Added")
		newLines = append(newLines, "")
		for _, item := range content.Added {
			newLines = append(newLines, fmt.Sprintf("- %s", item))
		}
		newLines = append(newLines, "")
	}
	
	if content.Changed != nil && len(content.Changed) > 0 {
		newLines = append(newLines, "### Changed")
		newLines = append(newLines, "")
		for _, item := range content.Changed {
			newLines = append(newLines, fmt.Sprintf("- %s", item))
		}
		newLines = append(newLines, "")
	}
	
	if content.Deprecated != nil && len(content.Deprecated) > 0 {
		newLines = append(newLines, "### Deprecated")
		newLines = append(newLines, "")
		for _, item := range content.Deprecated {
			newLines = append(newLines, fmt.Sprintf("- %s", item))
		}
		newLines = append(newLines, "")
	}
	
	if content.Removed != nil && len(content.Removed) > 0 {
		newLines = append(newLines, "### Removed")
		newLines = append(newLines, "")
		for _, item := range content.Removed {
			newLines = append(newLines, fmt.Sprintf("- %s", item))
		}
		newLines = append(newLines, "")
	}
	
	if content.Fixed != nil && len(content.Fixed) > 0 {
		newLines = append(newLines, "### Fixed")
		newLines = append(newLines, "")
		for _, item := range content.Fixed {
			newLines = append(newLines, fmt.Sprintf("- %s", item))
		}
		newLines = append(newLines, "")
	}
	
	if content.Security != nil && len(content.Security) > 0 {
		newLines = append(newLines, "### Security")
		newLines = append(newLines, "")
		for _, item := range content.Security {
			newLines = append(newLines, fmt.Sprintf("- %s", item))
		}
		newLines = append(newLines, "")
	}
	
	// Delete the old version entry
	var lineNumbers []int
	for i := startLine; i < endLine; i++ {
		lineNumbers = append(lineNumbers, i+1)
	}
	
	err = s.fileService.DeleteLines(changelogFileName, hash, s.config.Files.DefaultEncoding, lineNumbers)
	if err != nil {
		return fmt.Errorf("failed to delete old entry: %w", err)
	}
	
	// Re-read the file to get the updated hash
	lines, hash, err = s.fileService.ReadLines(changelogFileName, s.config.Files.DefaultEncoding)
	if err != nil {
		return fmt.Errorf("failed to read CHANGELOG file: %w", err)
	}
	
	// Insert the new version entry
	err = s.fileService.InsertLines(changelogFileName, hash, s.config.Files.DefaultEncoding, startLine+1, newLines)
	if err != nil {
		return fmt.Errorf("failed to insert new entry: %w", err)
	}
	
	return nil
}

// parseChangelogFile parses a CHANGELOG.md file into a structured representation
func (s *Service) parseChangelogFile(lines []string) (*Changelog, error) {
	changelog := &Changelog{
		Versions: []*VersionEntry{},
	}
	
	var currentVersion *VersionEntry
	var currentSection string
	
	for _, line := range lines {
		// Skip empty lines and title
		if line == "" || line == "# Changelog" {
			continue
		}
		
		// Check if it's a version header
		if versionMatch := versionRegex.FindStringSubmatch(line); versionMatch != nil {
			version := versionMatch[1]
			date := versionMatch[2]
			
			currentVersion = &VersionEntry{
				Version: version,
				Date:    date,
				Content: &ChangelogContent{},
			}
			changelog.Versions = append(changelog.Versions, currentVersion)
			currentSection = ""
			continue
		}
		
		// If no current version, skip
		if currentVersion == nil {
			continue
		}
		
		// Check if it's a section header
		if sectionMatch := sectionRegex.FindStringSubmatch(line); sectionMatch != nil {
			currentSection = strings.ToLower(sectionMatch[1])
			continue
		}
		
		// Check if it's a list item
		if listItemMatch := listItemRegex.FindStringSubmatch(line); listItemMatch != nil {
			item := listItemMatch[2]
			
			// Add to the appropriate section
			switch currentSection {
			case "added":
				currentVersion.Content.Added = append(currentVersion.Content.Added, item)
			case "changed":
				currentVersion.Content.Changed = append(currentVersion.Content.Changed, item)
			case "deprecated":
				currentVersion.Content.Deprecated = append(currentVersion.Content.Deprecated, item)
			case "removed":
				currentVersion.Content.Removed = append(currentVersion.Content.Removed, item)
			case "fixed":
				currentVersion.Content.Fixed = append(currentVersion.Content.Fixed, item)
			case "security":
				currentVersion.Content.Security = append(currentVersion.Content.Security, item)
			}
		}
	}
	
	return changelog, nil
}
```

--------------------------------------------------------------------------------
/internal/services/todo/todo.go:
--------------------------------------------------------------------------------

```go
package todo

import (
	"errors"
	"fmt"
	"regexp"
	"strings"

	"codeberg.org/mutker/mcp-todo-server/internal/config"
	"codeberg.org/mutker/mcp-todo-server/internal/services/file"
)

const (
	// Default TODO file name
	todoFileName = "TODO.md"
)

var (
	// ErrTaskNotFound is returned when a task is not found
	ErrTaskNotFound = errors.New("task not found")
	
	// ErrVersionNotFound is returned when a version is not found
	ErrVersionNotFound = errors.New("version not found")
	
	// ErrVersionExists is returned when a version already exists
	ErrVersionExists = errors.New("version already exists")
)

// versionRegex matches a version header (e.g., "## v1.0.0")
var versionRegex = regexp.MustCompile(`^## v(\d+\.\d+\.\d+)$`)

// taskRegex matches a task line (e.g., "- [ ] Task description" or "- [x] Completed task")
var taskRegex = regexp.MustCompile(`^(\s*)- \[([ xX])\] (.+)$`)

// Task represents a todo task
type Task struct {
	ID          string   `json:"id"`
	Description string   `json:"description"`
	Completed   bool     `json:"completed"`
	Version     string   `json:"version"`
	LineNumber  int      `json:"line_number"`
	Indent      int      `json:"indent"`
	SubTasks    []*Task  `json:"subtasks,omitempty"`
}

// VersionTasks represents tasks grouped by version
type VersionTasks struct {
	Version string  `json:"version"`
	Tasks   []*Task `json:"tasks"`
}

// TodoList represents the entire TODO.md file
type TodoList struct {
	Versions []*VersionTasks `json:"versions"`
}

// Service provides todo operations
type Service struct {
	config     *config.Config
	fileService *file.Service
}

// NewService creates a new todo service
func NewService(cfg *config.Config) *Service {
	fileService := file.NewService(cfg.Files.DefaultEncoding, cfg.Files.AutoDetection)
	
	return &Service{
		config:     cfg,
		fileService: fileService,
	}
}

// GetAll retrieves all tasks from TODO.md
func (s *Service) GetAll() (*TodoList, error) {
	lines, _, err := s.fileService.ReadLines(todoFileName, s.config.Files.DefaultEncoding)
	if err != nil {
		if errors.Is(err, file.ErrFileNotFound) {
			// Return empty todo list if file doesn't exist
			return &TodoList{Versions: []*VersionTasks{}}, nil
		}
		return nil, fmt.Errorf("failed to read TODO file: %w", err)
	}
	
	return s.parseTodoFile(lines)
}

// GetByVersion retrieves tasks for a specific version
func (s *Service) GetByVersion(version string) (*VersionTasks, error) {
	todoList, err := s.GetAll()
	if err != nil {
		return nil, err
	}
	
	// Find the requested version
	for _, v := range todoList.Versions {
		if v.Version == version {
			return v, nil
		}
	}
	
	return nil, nil
}

// AddTask adds a new task to TODO.md
func (s *Service) AddTask(version string, description string, parentID string) (string, error) {
	lines, hash, err := s.fileService.ReadLines(todoFileName, s.config.Files.DefaultEncoding)
	if err != nil && !errors.Is(err, file.ErrFileNotFound) {
		return "", fmt.Errorf("failed to read TODO file: %w", err)
	}
	
	// If file doesn't exist, create it with the version
	if errors.Is(err, file.ErrFileNotFound) {
		err = s.fileService.InsertLines(todoFileName, "", s.config.Files.DefaultEncoding, 1, []string{
			"# TODO",
			"",
			fmt.Sprintf("## v%s", version),
		})
		if err != nil {
			return "", fmt.Errorf("failed to create TODO file: %w", err)
		}
		
		// Re-read the file
		lines, hash, err = s.fileService.ReadLines(todoFileName, s.config.Files.DefaultEncoding)
		if err != nil {
			return "", fmt.Errorf("failed to read TODO file: %w", err)
		}
	}
	
	// Parse the file to locate the version and determine insert position
	todoList, err := s.parseTodoFile(lines)
	if err != nil {
		return "", fmt.Errorf("failed to parse TODO file: %w", err)
	}
	
	// Check if version exists
	versionExists := false
	var versionTasksObj *VersionTasks
	
	for _, v := range todoList.Versions {
		if v.Version == version {
			versionExists = true
			versionTasksObj = v
			break
		}
	}
	
	// If version doesn't exist, add it
	if !versionExists {
		// Find the position to insert the new version
		// We'll add it after the last version or at the end if no versions exist
		position := len(lines) + 1
		for i := len(lines) - 1; i >= 0; i-- {
			if versionRegex.MatchString(lines[i]) {
				position = i + 1
				break
			}
		}
		
		// Insert the version header
		err = s.fileService.InsertLines(todoFileName, hash, s.config.Files.DefaultEncoding, position, []string{
			"",
			fmt.Sprintf("## v%s", version),
		})
		if err != nil {
			return "", fmt.Errorf("failed to add version: %w", err)
		}
		
		// Re-read the file
		lines, hash, err = s.fileService.ReadLines(todoFileName, s.config.Files.DefaultEncoding)
		if err != nil {
			return "", fmt.Errorf("failed to read TODO file: %w", err)
		}
		
		// Re-parse the file
		todoList, err = s.parseTodoFile(lines)
		if err != nil {
			return "", fmt.Errorf("failed to parse TODO file: %w", err)
		}
		
		// Find the version again
		for _, v := range todoList.Versions {
			if v.Version == version {
				versionTasksObj = v
				break
			}
		}
	}
	
	// Determine insertion position and indentation
	var insertPosition int
	var indentation string
	
	if parentID == "" {
		// If no parent task, add at the end of the version section
		if len(versionTasksObj.Tasks) == 0 {
			// If no tasks in this version, add right after the version header
			for i, line := range lines {
				if line == fmt.Sprintf("## v%s", version) {
					insertPosition = i + 1
					break
				}
			}
		} else {
			// Find the last task in this version
			lastTask := versionTasksObj.Tasks[len(versionTasksObj.Tasks)-1]
			
			// Handle case where last task has subtasks
			// We need to find the last subtask recursively
			var findLastLine func(*Task) int
			findLastLine = func(t *Task) int {
				if len(t.SubTasks) == 0 {
					return t.LineNumber
				}
				return findLastLine(t.SubTasks[len(t.SubTasks)-1])
			}
			
			lastLine := findLastLine(lastTask)
			insertPosition = lastLine
		}
	} else {
		// Find the parent task
		var parentTask *Task
		var findTask func([]*Task) *Task
		findTask = func(tasks []*Task) *Task {
			for _, t := range tasks {
				if t.ID == parentID {
					return t
				}
				if result := findTask(t.SubTasks); result != nil {
					return result
				}
			}
			return nil
		}
		
		for _, v := range todoList.Versions {
			if parent := findTask(v.Tasks); parent != nil {
				parentTask = parent
				break
			}
		}
		
		if parentTask == nil {
			return "", ErrTaskNotFound
		}
		
		// Add as a subtask of the parent
		insertPosition = parentTask.LineNumber
		indentation = strings.Repeat(" ", parentTask.Indent+2)
	}
	
	// Add the task
	newTaskLine := fmt.Sprintf("%s- [ ] %s", indentation, description)
	err = s.fileService.InsertLines(todoFileName, hash, s.config.Files.DefaultEncoding, insertPosition+1, []string{newTaskLine})
	if err != nil {
		return "", fmt.Errorf("failed to add task: %w", err)
	}
	
	// Re-read and parse to get the ID of the new task
	lines, _, err = s.fileService.ReadLines(todoFileName, s.config.Files.DefaultEncoding)
	if err != nil {
		return "", fmt.Errorf("failed to read TODO file: %w", err)
	}
	
	todoList, err = s.parseTodoFile(lines)
	if err != nil {
		return "", fmt.Errorf("failed to parse TODO file: %w", err)
	}
	
	// Find the task we just added to get its ID
	var newTaskID string
	for _, v := range todoList.Versions {
		if v.Version == version {
			for _, t := range v.Tasks {
				if t.LineNumber == insertPosition+1 && t.Description == description {
					newTaskID = t.ID
					return newTaskID, nil
				}
				
				// Also check subtasks
				var findNewTask func([]*Task) string
				findNewTask = func(tasks []*Task) string {
					for _, st := range tasks {
						if st.LineNumber == insertPosition+1 && st.Description == description {
							return st.ID
						}
						if id := findNewTask(st.SubTasks); id != "" {
							return id
						}
					}
					return ""
				}
				
				if id := findNewTask(t.SubTasks); id != "" {
					return id, nil
				}
			}
		}
	}
	
	// If we couldn't find the new task (shouldn't happen), generate a best-guess ID
	return generateTaskID(version, description), nil
}

// UpdateTask updates an existing task
func (s *Service) UpdateTask(taskID string, completed *bool, description *string) error {
	lines, hash, err := s.fileService.ReadLines(todoFileName, s.config.Files.DefaultEncoding)
	if err != nil {
		return fmt.Errorf("failed to read TODO file: %w", err)
	}
	
	todoList, err := s.parseTodoFile(lines)
	if err != nil {
		return fmt.Errorf("failed to parse TODO file: %w", err)
	}
	
	// Find the task
	var taskToUpdate *Task
	var findTask func([]*Task) *Task
	findTask = func(tasks []*Task) *Task {
		for _, t := range tasks {
			if t.ID == taskID {
				return t
			}
			if result := findTask(t.SubTasks); result != nil {
				return result
			}
		}
		return nil
	}
	
	for _, v := range todoList.Versions {
		if task := findTask(v.Tasks); task != nil {
			taskToUpdate = task
			break
		}
	}
	
	if taskToUpdate == nil {
		return ErrTaskNotFound
	}
	
	// Parse the line to extract indentation and create the new line
	indentMatch := strings.Repeat(" ", taskToUpdate.Indent)
	
	// Determine completion status
	completionStatus := " "
	if completed != nil {
		if *completed {
			completionStatus = "x"
		}
	} else if taskToUpdate.Completed {
		completionStatus = "x"
	}
	
	// Determine description
	desc := taskToUpdate.Description
	if description != nil {
		desc = *description
	}
	
	// Create the new line
	newLine := fmt.Sprintf("%s- [%s] %s", indentMatch, completionStatus, desc)
	
	// Update the line
	lineEdits := map[int]string{
		taskToUpdate.LineNumber: newLine,
	}
	
	err = s.fileService.EditLines(todoFileName, hash, s.config.Files.DefaultEncoding, lineEdits)
	if err != nil {
		return fmt.Errorf("failed to update task: %w", err)
	}
	
	return nil
}

// AddVersion adds a new version section to TODO.md
func (s *Service) AddVersion(version string) error {
	lines, hash, err := s.fileService.ReadLines(todoFileName, s.config.Files.DefaultEncoding)
	if err != nil && !errors.Is(err, file.ErrFileNotFound) {
		return fmt.Errorf("failed to read TODO file: %w", err)
	}
	
	// If file doesn't exist, create it
	if errors.Is(err, file.ErrFileNotFound) {
		err = s.fileService.InsertLines(todoFileName, "", s.config.Files.DefaultEncoding, 1, []string{
			"# TODO",
			"",
			fmt.Sprintf("## v%s", version),
		})
		if err != nil {
			return fmt.Errorf("failed to create TODO file: %w", err)
		}
		return nil
	}
	
	// Check if version already exists
	todoList, err := s.parseTodoFile(lines)
	if err != nil {
		return fmt.Errorf("failed to parse TODO file: %w", err)
	}
	
	for _, v := range todoList.Versions {
		if v.Version == version {
			return ErrVersionExists
		}
	}
	
	// Find the position to insert the new version
	// We'll add it before the first greater version or at the end if no greater version exists
	position := len(lines) + 1
	
	// Make sure todoList.Versions is sorted by version
	// For simplicity, we'll just add it at the end for now
	// In a real implementation, we would sort the versions semantically
	
	// Insert the version
	err = s.fileService.InsertLines(todoFileName, hash, s.config.Files.DefaultEncoding, position, []string{
		"",
		fmt.Sprintf("## v%s", version),
	})
	if err != nil {
		return fmt.Errorf("failed to add version: %w", err)
	}
	
	return nil
}

// parseTodoFile parses a TODO.md file into a structured representation
func (s *Service) parseTodoFile(lines []string) (*TodoList, error) {
	todoList := &TodoList{
		Versions: []*VersionTasks{},
	}
	
	var currentVersion *VersionTasks
	var taskStack [][]*Task
	
	for i, line := range lines {
		lineNum := i + 1
		
		// Skip empty lines and the title
		if line == "" || line == "# TODO" {
			continue
		}
		
		// Check if it's a version header
		if versionMatch := versionRegex.FindStringSubmatch(line); versionMatch != nil {
			version := versionMatch[1]
			currentVersion = &VersionTasks{
				Version: version,
				Tasks:   []*Task{},
			}
			todoList.Versions = append(todoList.Versions, currentVersion)
			taskStack = nil
			continue
		}
		
		// If no current version, skip
		if currentVersion == nil {
			continue
		}
		
		// Check if it's a task
		if taskMatch := taskRegex.FindStringSubmatch(line); taskMatch != nil {
			indentation := taskMatch[1]
			completed := taskMatch[2] == "x" || taskMatch[2] == "X"
			description := taskMatch[3]
			indent := len(indentation)
			
			task := &Task{
				ID:          generateTaskID(currentVersion.Version, description),
				Description: description,
				Completed:   completed,
				Version:     currentVersion.Version,
				LineNumber:  lineNum,
				Indent:      indent,
				SubTasks:    []*Task{},
			}
			
			// Handle task hierarchy based on indentation
			if indent == 0 {
				// Top-level task
				currentVersion.Tasks = append(currentVersion.Tasks, task)
				taskStack = [][]*Task{{task}}
			} else {
				// Find the parent task based on indentation
				level := indent / 2
				
				// Ensure the stack has enough levels
				for len(taskStack) <= level {
					taskStack = append(taskStack, []*Task{})
				}
				
				// Add the task to its level
				taskStack[level] = append(taskStack[level], task)
				
				// Add as a subtask to the parent
				if level > 0 {
					parentLevel := level - 1
					parentTasks := taskStack[parentLevel]
					if len(parentTasks) > 0 {
						parent := parentTasks[len(parentTasks)-1]
						parent.SubTasks = append(parent.SubTasks, task)
					}
				}
			}
		}
	}
	
	return todoList, nil
}

// generateTaskID generates a unique ID for a task
func generateTaskID(version string, description string) string {
	// Generate a simple ID based on the version and description
	// In a real implementation, you might want to use something more robust
	cleanDesc := strings.ToLower(description)
	cleanDesc = strings.Replace(cleanDesc, " ", "-", -1)
	cleanDesc = regexp.MustCompile(`[^a-z0-9-]`).ReplaceAllString(cleanDesc, "")
	
	if len(cleanDesc) > 20 {
		cleanDesc = cleanDesc[:20]
	}
	
	return fmt.Sprintf("%s-%s", version, cleanDesc)
}
```

--------------------------------------------------------------------------------
/internal/services/file/file.go:
--------------------------------------------------------------------------------

```go
package file

import (
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"golang.org/x/net/html/charset"
	"golang.org/x/text/encoding"
	"golang.org/x/text/encoding/charmap"
	"golang.org/x/text/encoding/japanese"
	"golang.org/x/text/encoding/korean"
	"golang.org/x/text/encoding/simplifiedchinese"
	"golang.org/x/text/encoding/traditionalchinese"
	"golang.org/x/text/encoding/unicode"
)

var (
	// ErrFileNotFound is returned when a file does not exist
	ErrFileNotFound = errors.New("file not found")
	
	// ErrContentChanged is returned when the file content has changed since it was last read
	ErrContentChanged = errors.New("file content has changed")
	
	// ErrInvalidRange is returned when an invalid range is specified
	ErrInvalidRange = errors.New("invalid range specified")
	
	// ErrInvalidEncoding is returned when an unsupported encoding is specified
	ErrInvalidEncoding = errors.New("invalid encoding specified")
)

// LineRange represents a range of lines in a file
type LineRange struct {
	Start int
	End   int
}

// Service provides file operation methods
type Service struct {
	defaultEncoding      string
	autoDetectEncoding   bool
}

// NewService creates a new file service
func NewService(defaultEncoding string, autoDetect bool) *Service {
	if defaultEncoding == "" {
		defaultEncoding = "utf-8"
	}
	
	return &Service{
		defaultEncoding:    defaultEncoding,
		autoDetectEncoding: autoDetect,
	}
}

// ReadLines reads lines from a file with optional range specification
func (s *Service) ReadLines(filePath string, encName string, ranges ...LineRange) ([]string, string, error) {
	// Validate file existence
	if _, err := os.Stat(filePath); os.IsNotExist(err) {
		return nil, "", ErrFileNotFound
	}
	
	// Handle automatic encoding detection
	var enc encoding.Encoding
	var err error
	
	if strings.ToLower(encName) == "auto" {
		// Attempt to detect the encoding
		enc, encName, err = s.DetectEncoding(filePath)
		if err != nil {
			// Fall back to default encoding on detection failure
			enc, err = s.getEncoding(s.defaultEncoding)
			if err != nil {
				return nil, "", err
			}
		}
	} else {
		// Get the encoding specified by the user
		enc, err = s.getEncoding(encName)
		if err != nil {
			return nil, "", err
		}
	}
	
	// Open the file
	file, err := os.Open(filePath)
	if err != nil {
		return nil, "", fmt.Errorf("failed to open file: %w", err)
	}
	defer file.Close()
	
	// Read the entire file and calculate its hash
	content, err := io.ReadAll(file)
	if err != nil {
		return nil, "", fmt.Errorf("failed to read file: %w", err)
	}
	
	// Calculate hash of the content
	hash := sha256.Sum256(content)
	hashStr := hex.EncodeToString(hash[:])
	
	// Decode content if needed
	var decodedContent []byte
	if enc != nil {
		decoder := enc.NewDecoder()
		decodedContent, err = decoder.Bytes(content)
		if err != nil {
			return nil, "", fmt.Errorf("failed to decode content: %w", err)
		}
	} else {
		decodedContent = content
	}
	
	// Split into lines
	lines := strings.Split(string(decodedContent), "\n")
	
	// If no ranges provided, return all lines
	if len(ranges) == 0 {
		return lines, hashStr, nil
	}
	
	// Process each range
	var result []string
	for _, r := range ranges {
		// Validate range
		if r.Start < 0 || r.End > len(lines) || r.Start > r.End {
			return nil, "", ErrInvalidRange
		}
		
		// Add lines from this range
		result = append(result, lines[r.Start-1:r.End]...)
	}
	
	return result, hashStr, nil
}

// EditLines edits lines in a file with hash validation
func (s *Service) EditLines(filePath string, oldHash string, encName string, lineEdits map[int]string) error {
	// Validate file existence
	if _, err := os.Stat(filePath); os.IsNotExist(err) {
		return ErrFileNotFound
	}
	
	// Handle automatic encoding detection
	var enc encoding.Encoding
	var err error
	
	if strings.ToLower(encName) == "auto" {
		// Attempt to detect the encoding
		enc, encName, err = s.DetectEncoding(filePath)
		if err != nil {
			// Fall back to default encoding on detection failure
			enc, err = s.getEncoding(s.defaultEncoding)
			if err != nil {
				return err
			}
		}
	} else {
		// Get the encoding specified by the user
		enc, err = s.getEncoding(encName)
		if err != nil {
			return err
		}
	}
	
	// Read current file content
	file, err := os.ReadFile(filePath)
	if err != nil {
		return fmt.Errorf("failed to read file: %w", err)
	}
	
	// Verify hash if provided
	if oldHash != "" {
		hash := sha256.Sum256(file)
		currentHash := hex.EncodeToString(hash[:])
		
		if currentHash != oldHash {
			return ErrContentChanged
		}
	}
	
	// Decode content if needed
	var decodedContent []byte
	if enc != nil {
		decoder := enc.NewDecoder()
		decodedContent, err = decoder.Bytes(file)
		if err != nil {
			return fmt.Errorf("failed to decode content: %w", err)
		}
	} else {
		decodedContent = file
	}
	
	// Split into lines
	lines := strings.Split(string(decodedContent), "\n")
	
	// Apply the edits
	for lineNum, newContent := range lineEdits {
		if lineNum < 1 || lineNum > len(lines) {
			return ErrInvalidRange
		}
		
		lines[lineNum-1] = newContent
	}
	
	// Combine back into a single string
	newContent := strings.Join(lines, "\n")
	
	// Encode the content if needed
	var finalContent []byte
	if enc != nil {
		encoder := enc.NewEncoder()
		finalContent, err = encoder.Bytes([]byte(newContent))
		if err != nil {
			return fmt.Errorf("failed to encode content: %w", err)
		}
	} else {
		finalContent = []byte(newContent)
	}
	
	// Create the directory if it doesn't exist
	dir := filepath.Dir(filePath)
	if _, err := os.Stat(dir); os.IsNotExist(err) {
		if err := os.MkdirAll(dir, 0755); err != nil {
			return fmt.Errorf("failed to create directory: %w", err)
		}
	}
	
	// Write the file
	err = os.WriteFile(filePath, finalContent, 0644)
	if err != nil {
		return fmt.Errorf("failed to write file: %w", err)
	}
	
	return nil
}

// AppendLines appends lines to a file
func (s *Service) AppendLines(filePath string, encName string, lines []string) error {
	// Check if file exists for encoding detection
	fileExists := false
	if _, err := os.Stat(filePath); err == nil {
		fileExists = true
	}
	
	// Handle automatic encoding detection or get the specified encoding
	var enc encoding.Encoding
	var err error
	
	if strings.ToLower(encName) == "auto" {
		if fileExists {
			// Attempt to detect the encoding for existing files
			enc, encName, err = s.DetectEncoding(filePath)
			if err != nil {
				// Fall back to default encoding on detection failure
				enc, err = s.getEncoding(s.defaultEncoding)
				if err != nil {
					return err
				}
			}
		} else {
			// For new files, use the default encoding
			enc, err = s.getEncoding(s.defaultEncoding)
			if err != nil {
				return err
			}
		}
	} else {
		// Get the encoding specified by the user
		enc, err = s.getEncoding(encName)
		if err != nil {
			return err
		}
	}
	
	// Convert lines to a single string
	newContent := strings.Join(lines, "\n")
	
	// Add a newline if the file exists and doesn't end with one
	if _, err := os.Stat(filePath); err == nil {
		content, err := os.ReadFile(filePath)
		if err != nil {
			return fmt.Errorf("failed to read file: %w", err)
		}
		
		if len(content) > 0 && content[len(content)-1] != '\n' {
			newContent = "\n" + newContent
		}
	}
	
	// Encode the content if needed
	var finalContent []byte
	if enc != nil {
		encoder := enc.NewEncoder()
		finalContent, err = encoder.Bytes([]byte(newContent))
		if err != nil {
			return fmt.Errorf("failed to encode content: %w", err)
		}
	} else {
		finalContent = []byte(newContent)
	}
	
	// Create the directory if it doesn't exist
	dir := filepath.Dir(filePath)
	if _, err := os.Stat(dir); os.IsNotExist(err) {
		if err := os.MkdirAll(dir, 0755); err != nil {
			return fmt.Errorf("failed to create directory: %w", err)
		}
	}
	
	// Open file in append mode
	f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return fmt.Errorf("failed to open file: %w", err)
	}
	defer f.Close()
	
	// Write the content
	if _, err := f.Write(finalContent); err != nil {
		return fmt.Errorf("failed to append to file: %w", err)
	}
	
	return nil
}

// DeleteLines deletes lines from a file with hash validation
func (s *Service) DeleteLines(filePath string, oldHash string, encName string, lineNumbers []int) error {
	// Validate file existence
	if _, err := os.Stat(filePath); os.IsNotExist(err) {
		return ErrFileNotFound
	}
	
	// Handle automatic encoding detection
	var enc encoding.Encoding
	var err error
	
	if strings.ToLower(encName) == "auto" {
		// Attempt to detect the encoding
		enc, encName, err = s.DetectEncoding(filePath)
		if err != nil {
			// Fall back to default encoding on detection failure
			enc, err = s.getEncoding(s.defaultEncoding)
			if err != nil {
				return err
			}
		}
	} else {
		// Get the encoding specified by the user
		enc, err = s.getEncoding(encName)
		if err != nil {
			return err
		}
	}
	
	// Read current file content
	file, err := os.ReadFile(filePath)
	if err != nil {
		return fmt.Errorf("failed to read file: %w", err)
	}
	
	// Verify hash if provided
	if oldHash != "" {
		hash := sha256.Sum256(file)
		currentHash := hex.EncodeToString(hash[:])
		
		if currentHash != oldHash {
			return ErrContentChanged
		}
	}
	
	// Decode content if needed
	var decodedContent []byte
	if enc != nil {
		decoder := enc.NewDecoder()
		decodedContent, err = decoder.Bytes(file)
		if err != nil {
			return fmt.Errorf("failed to decode content: %w", err)
		}
	} else {
		decodedContent = file
	}
	
	// Split into lines
	lines := strings.Split(string(decodedContent), "\n")
	
	// Create a map of lines to delete for O(1) lookup
	toDelete := make(map[int]bool)
	for _, lineNum := range lineNumbers {
		if lineNum < 1 || lineNum > len(lines) {
			return ErrInvalidRange
		}
		toDelete[lineNum-1] = true
	}
	
	// Create a new slice without the deleted lines
	var newLines []string
	for i, line := range lines {
		if !toDelete[i] {
			newLines = append(newLines, line)
		}
	}
	
	// Combine back into a single string
	newContent := strings.Join(newLines, "\n")
	
	// Encode the content if needed
	var finalContent []byte
	if enc != nil {
		encoder := enc.NewEncoder()
		finalContent, err = encoder.Bytes([]byte(newContent))
		if err != nil {
			return fmt.Errorf("failed to encode content: %w", err)
		}
	} else {
		finalContent = []byte(newContent)
	}
	
	// Write the file
	err = os.WriteFile(filePath, finalContent, 0644)
	if err != nil {
		return fmt.Errorf("failed to write file: %w", err)
	}
	
	return nil
}

// InsertLines inserts lines at a specific position with hash validation
func (s *Service) InsertLines(filePath string, oldHash string, encName string, position int, newLines []string) error {
	// Validate file existence
	isNewFile := false
	if _, err := os.Stat(filePath); os.IsNotExist(err) {
		isNewFile = true
		
		// If this is a new file and position is not 1, return an error
		if position != 1 {
			return ErrInvalidRange
		}
	}
	
	// Handle automatic encoding detection or get the specified encoding
	var enc encoding.Encoding
	var err error
	
	if strings.ToLower(encName) == "auto" {
		if !isNewFile {
			// Attempt to detect the encoding for existing files
			enc, encName, err = s.DetectEncoding(filePath)
			if err != nil {
				// Fall back to default encoding on detection failure
				enc, err = s.getEncoding(s.defaultEncoding)
				if err != nil {
					return err
				}
			}
		} else {
			// For new files, use the default encoding
			enc, err = s.getEncoding(s.defaultEncoding)
			if err != nil {
				return err
			}
		}
	} else {
		// Get the encoding specified by the user
		enc, err = s.getEncoding(encName)
		if err != nil {
			return err
		}
	}
	
	var lines []string
	
	if !isNewFile {
		// Read current file content
		file, err := os.ReadFile(filePath)
		if err != nil {
			return fmt.Errorf("failed to read file: %w", err)
		}
		
		// Verify hash if provided
		if oldHash != "" {
			hash := sha256.Sum256(file)
			currentHash := hex.EncodeToString(hash[:])
			
			if currentHash != oldHash {
				return ErrContentChanged
			}
		}
		
		// Decode content if needed
		var decodedContent []byte
		if enc != nil {
			decoder := enc.NewDecoder()
			decodedContent, err = decoder.Bytes(file)
			if err != nil {
				return fmt.Errorf("failed to decode content: %w", err)
			}
		} else {
			decodedContent = file
		}
		
		// Split into lines
		lines = strings.Split(string(decodedContent), "\n")
		
		// Validate position
		if position < 1 || position > len(lines)+1 {
			return ErrInvalidRange
		}
	} else {
		lines = []string{}
	}
	
	// Insert the new lines
	var resultLines []string
	
	if position == 1 {
		resultLines = append(newLines, lines...)
	} else if position == len(lines)+1 {
		resultLines = append(lines, newLines...)
	} else {
		resultLines = append(resultLines, lines[:position-1]...)
		resultLines = append(resultLines, newLines...)
		resultLines = append(resultLines, lines[position-1:]...)
	}
	
	// Combine back into a single string
	newContent := strings.Join(resultLines, "\n")
	
	// Encode the content if needed
	var finalContent []byte
	if enc != nil {
		encoder := enc.NewEncoder()
		finalContent, err = encoder.Bytes([]byte(newContent))
		if err != nil {
			return fmt.Errorf("failed to encode content: %w", err)
		}
	} else {
		finalContent = []byte(newContent)
	}
	
	// Create the directory if it doesn't exist
	dir := filepath.Dir(filePath)
	if _, err := os.Stat(dir); os.IsNotExist(err) {
		if err := os.MkdirAll(dir, 0755); err != nil {
			return fmt.Errorf("failed to create directory: %w", err)
		}
	}
	
	// Write the file
	err = os.WriteFile(filePath, finalContent, 0644)
	if err != nil {
		return fmt.Errorf("failed to write file: %w", err)
	}
	
	return nil
}

// DetectEncoding attempts to detect the encoding of a file
func (s *Service) DetectEncoding(filePath string) (encoding.Encoding, string, error) {
	// Open the file
	file, err := os.Open(filePath)
	if err != nil {
		return nil, "", fmt.Errorf("failed to open file: %w", err)
	}
	defer file.Close()
	
	// Read a small portion of the file for detection
	var buf [1024]byte
	n, err := file.Read(buf[:])
	if err != nil && err != io.EOF {
		return nil, "", fmt.Errorf("failed to read file: %w", err)
	}
	
	// Reset the file pointer to the beginning
	_, err = file.Seek(0, 0)
	if err != nil {
		return nil, "", fmt.Errorf("failed to reset file position: %w", err)
	}
	
	// Detect encoding
	e, name, _ := charset.DetermineEncoding(buf[:n], "")
	
	// Normalize name to our standard format
	normalizedName := s.normalizeEncodingName(name)
	
	return e, normalizedName, nil
}

// normalizeEncodingName normalizes encoding names to our standard format
func (s *Service) normalizeEncodingName(name string) string {
	name = strings.ToLower(name)
	switch name {
	case "utf-8", "utf8":
		return "utf-8"
	case "windows-1252", "cp1252":
		return "windows-1252"
	case "iso-8859-1":
		return "latin1"
	case "shift_jis", "shift-jis", "shiftjis":
		return "shift_jis"
	case "gbk", "gb18030", "gb2312":
		return "gbk"
	case "big5", "big5-hkscs":
		return "big5"
	case "euc-jp":
		return "euc-jp"
	case "euc-kr":
		return "euc-kr"
	default:
		return name
	}
}

// GetEncoding returns the appropriate encoding based on the provided name
// If name is "auto", it will attempt to detect the encoding
func (s *Service) getEncoding(encName string) (encoding.Encoding, error) {
	if encName == "" {
		encName = s.defaultEncoding
	}
	
	switch strings.ToLower(encName) {
	case "utf-8", "utf8":
		return nil, nil // No encoding/decoding needed for UTF-8
	case "utf-16", "utf16":
		return unicode.UTF16(unicode.LittleEndian, unicode.UseBOM), nil
	case "utf-16be", "utf16be":
		return unicode.UTF16(unicode.BigEndian, unicode.UseBOM), nil
	case "utf-16le", "utf16le":
		return unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), nil
	
	// Japanese encodings
	case "shift_jis", "shift-jis", "shiftjis":
		return japanese.ShiftJIS, nil
	case "euc-jp":
		return japanese.EUCJP, nil
	case "iso-2022-jp":
		return japanese.ISO2022JP, nil
	
	// Chinese encodings
	case "gbk", "gb18030", "gb2312":
		return simplifiedchinese.GBK, nil
	case "big5", "big5-hkscs":
		return traditionalchinese.Big5, nil
	
	// Korean encodings
	case "euc-kr":
		return korean.EUCKR, nil
	
	// Western encodings
	case "iso-8859-1", "latin1":
		return charmap.ISO8859_1, nil
	case "iso-8859-2", "latin2":
		return charmap.ISO8859_2, nil
	case "iso-8859-3", "latin3":
		return charmap.ISO8859_3, nil
	case "iso-8859-4", "latin4":
		return charmap.ISO8859_4, nil
	case "iso-8859-5":
		return charmap.ISO8859_5, nil
	case "iso-8859-6":
		return charmap.ISO8859_6, nil
	case "iso-8859-7":
		return charmap.ISO8859_7, nil
	case "iso-8859-8":
		return charmap.ISO8859_8, nil
	case "iso-8859-9", "latin5":
		return charmap.ISO8859_9, nil
	case "iso-8859-10", "latin6":
		return charmap.ISO8859_10, nil
	case "iso-8859-13", "latin7":
		return charmap.ISO8859_13, nil
	case "iso-8859-14", "latin8":
		return charmap.ISO8859_14, nil
	case "iso-8859-15", "latin9":
		return charmap.ISO8859_15, nil
	case "iso-8859-16":
		return charmap.ISO8859_16, nil
	
	// Windows encodings
	case "windows-1250":
		return charmap.Windows1250, nil
	case "windows-1251":
		return charmap.Windows1251, nil
	case "windows-1252", "cp1252":
		return charmap.Windows1252, nil
	case "windows-1253":
		return charmap.Windows1253, nil
	case "windows-1254":
		return charmap.Windows1254, nil
	case "windows-1255":
		return charmap.Windows1255, nil
	case "windows-1256":
		return charmap.Windows1256, nil
	case "windows-1257":
		return charmap.Windows1257, nil
	case "windows-1258":
		return charmap.Windows1258, nil
	
	default:
		return nil, ErrInvalidEncoding
	}
}
```

--------------------------------------------------------------------------------
/internal/mcp/tools.go:
--------------------------------------------------------------------------------

```go
package mcp

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"codeberg.org/mutker/mcp-todo-server/internal/config"
	"codeberg.org/mutker/mcp-todo-server/internal/services/changelog"
	"codeberg.org/mutker/mcp-todo-server/internal/services/todo"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

// This function is now defined in server.go

// registerTodoTools registers TODO.md related tools
func RegisterTools(ctx context.Context, s *server.MCPServer, cfg *config.Config) {
	todoService := todo.NewService(cfg)
	changelogService := changelog.NewService(cfg)

	// Get all tasks
	s.AddTool(mcp.NewTool("get-todo-tasks",
		mcp.WithDescription("Get all tasks from TODO.md"),
	),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			todos, err := todoService.GetAll()
			if err != nil {
				return mcp.NewToolResultError(fmt.Sprintf("failed to get todos: %w", err)), nil
			}
			result, err := newToolResultJSON(todos)
			return result, err
		},
	)

	// Get tasks for a specific version
	s.AddTool(mcp.NewTool("get-todo-tasks-by-version",
		mcp.WithDescription("Get tasks for a specific version from TODO.md"),
		mcp.WithString("version", 
			mcp.PropertyOption(mcp.Required()),
			mcp.PropertyOption(mcp.Description("Version string (e.g., '1.0.0')")),
		),
	),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			version, ok := request.Params.Arguments["version"].(string)
			if !ok {
				return mcp.NewToolResultError("version parameter must be a string"), nil
			}

			todos, err := todoService.GetByVersion(version)
			if err != nil {
				return mcp.NewToolResultError(fmt.Sprintf("failed to get todos for version %s: %w", version, err)), nil
			}
			if todos == nil {
				return mcp.NewToolResultError(fmt.Sprintf("version not found: %s", version)), nil
			}
			result, err := newToolResultJSON(todos)
			return result, err
		},
	)

	// Add a new task
	s.AddTool(mcp.NewTool("add-todo-task",
		mcp.WithDescription("Add a new task to TODO.md"),
		mcp.WithString("version",
			mcp.PropertyOption(mcp.Required()),
			mcp.PropertyOption(mcp.Description("Version string (e.g., '1.0.0')")),
		),
		mcp.WithString("description",
			mcp.PropertyOption(mcp.Required()),
			mcp.PropertyOption(mcp.Description("Task description")),
		),
		mcp.WithString("parent_id",
			mcp.PropertyOption(mcp.Description("ID of parent task (for subtasks)")),
		),
	),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			version, ok := request.Params.Arguments["version"].(string)
			if !ok {
				return mcp.NewToolResultError("version parameter must be a string"), nil
			}

			description, ok := request.Params.Arguments["description"].(string)
			if !ok {
				return mcp.NewToolResultError("description parameter must be a string"), nil
			}

			var parentID string
			if parent, ok := request.Params.Arguments["parent_id"].(string); ok {
				parentID = parent
			}

			taskID, err := todoService.AddTask(version, description, parentID)
			if err != nil {
				return mcp.NewToolResultError(fmt.Sprintf("failed to add task: %w", err)), nil
			}

			result, err := newToolResultJSON(map[string]string{"id": taskID})
			return result, err
		},
	)

	// Update an existing task
	toolSchema := map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"id": map[string]interface{}{
				"type":        "string",
				"description": "Task ID",
			},
			"completed": map[string]interface{}{
				"type":        "boolean",
				"description": "Task completion status",
			},
			"description": map[string]interface{}{
				"type":        "string",
				"description": "Updated task description",
			},
		},
		"required": []string{"id"},
	}
	schemaBytes, _ := json.Marshal(toolSchema)
	s.AddTool(mcp.NewToolWithRawSchema("update-todo-task", 
		"Update an existing task in TODO.md", 
		json.RawMessage(schemaBytes)),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			taskID, ok := request.Params.Arguments["id"].(string)
			if !ok {
				return mcp.NewToolResultError("id parameter must be a string"), nil
			}

			var completed *bool
			if c, ok := request.Params.Arguments["completed"].(bool); ok {
				completed = &c
			}

			var description *string
			if d, ok := request.Params.Arguments["description"].(string); ok {
				description = &d
			}

			if completed == nil && description == nil {
				return mcp.NewToolResultError("at least one of completed or description must be provided"), nil
			}

			err := todoService.UpdateTask(taskID, completed, description)
			if err != nil {
				if err == todo.ErrTaskNotFound {
					return mcp.NewToolResultError(fmt.Sprintf("task not found: %s", taskID)), nil
				}
				return mcp.NewToolResultError(fmt.Sprintf("failed to update task: %w", err)), nil
			}

			result, err := newToolResultJSON(map[string]bool{"success": true})
			return result, err
		},
	)

	// Add a new version section
	s.AddTool(mcp.NewTool("add-todo-version",
		mcp.WithDescription("Add a new version section to TODO.md"),
		mcp.WithString("version",
			mcp.PropertyOption(mcp.Required()),
			mcp.PropertyOption(mcp.Description("Version string (e.g., '1.0.0')")),
		),
	),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			version, ok := request.Params.Arguments["version"].(string)
			if !ok {
				return mcp.NewToolResultError("version parameter must be a string"), nil
			}

			err := todoService.AddVersion(version)
			if err != nil {
				if err == todo.ErrVersionExists {
					return mcp.NewToolResultError(fmt.Sprintf("version already exists: %s", version)), nil
				}
				return mcp.NewToolResultError(fmt.Sprintf("failed to add version: %w", err)), nil
			}

			result, err := newToolResultJSON(map[string]bool{"success": true})
			return result, err
		},
	)

	// Import and format an existing TODO.md
	s.AddTool(mcp.NewTool("import-todo",
		mcp.WithDescription("Import and format an existing TODO.md file"),
		mcp.WithString("source_path",
			mcp.PropertyOption(mcp.Required()),
			mcp.PropertyOption(mcp.Description("Path to the source TODO.md file")),
		),
	),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			sourcePath, ok := request.Params.Arguments["source_path"].(string)
			if !ok {
				return mcp.NewToolResultError("source_path parameter must be a string"), nil
			}

			// Create an absolute path if a relative path is provided
			if !filepath.IsAbs(sourcePath) {
				cwd, err := os.Getwd()
				if err != nil {
					return mcp.NewToolResultError(fmt.Sprintf("failed to get current working directory: %w", err)), nil
				}
				sourcePath = filepath.Join(cwd, sourcePath)
			}

			// Read the source file
			content, err := os.ReadFile(sourcePath)
			if err != nil {
				return mcp.NewToolResultError(fmt.Sprintf("failed to read source file: %w", err)), nil
			}

			// Split into lines
			lines := strings.Split(string(content), "\n")

			// Parse the content
			todoList, err := todoService.GetAll()
			if err != nil {
				return mcp.NewToolResultError(fmt.Sprintf("failed to load existing TODO file: %w", err)), nil
			}

			// Only proceed if the current TODO.md is empty
			if len(todoList.Versions) > 0 {
				return mcp.NewToolResultError("TODO.md already exists and has content, cannot import"), nil
			}

			// Process each line
			var currentVersion string
			var tasks []struct {
				Version     string
				Description string
				Completed   bool
			}

			for _, line := range lines {
				// Skip empty lines and title
				if line == "" || line == "# TODO" {
					continue
				}

				// Check for version headers
				if strings.HasPrefix(line, "## ") {
					versionStr := strings.TrimPrefix(line, "## ")
					// Handle both v1.0.0 and 1.0.0 formats
					if strings.HasPrefix(versionStr, "v") {
						currentVersion = strings.TrimPrefix(versionStr, "v")
					} else {
						currentVersion = versionStr
					}

					if currentVersion != "" {
						// Add the version
						err := todoService.AddVersion(currentVersion)
						if err != nil && err != todo.ErrVersionExists {
							return mcp.NewToolResultError(fmt.Sprintf("failed to add version %s: %w", currentVersion, err)), nil
						}
					}
					continue
				}

				// Check for task lines
				if strings.Contains(line, "- [ ]") || strings.Contains(line, "- [x]") {
					completed := strings.Contains(line, "- [x]")

					var description string
					if completed {
						description = strings.TrimSpace(strings.Split(line, "- [x]")[1])
					} else {
						description = strings.TrimSpace(strings.Split(line, "- [ ]")[1])
					}

					if currentVersion != "" && description != "" {
						tasks = append(tasks, struct {
							Version     string
							Description string
							Completed   bool
						}{
							Version:     currentVersion,
							Description: description,
							Completed:   completed,
						})
					}
				}
			}

			// Add all tasks
			for _, t := range tasks {
				taskID, err := todoService.AddTask(t.Version, t.Description, "")
				if err != nil {
					return mcp.NewToolResultError(fmt.Sprintf("failed to add task: %w", err)), nil
				}

				if t.Completed {
					completed := true
					err = todoService.UpdateTask(taskID, &completed, nil)
					if err != nil {
						return mcp.NewToolResultError(fmt.Sprintf("failed to mark task as completed: %w", err)), nil
					}
				}
			}

			result, err := newToolResultJSON(map[string]interface{}{
				"success":        true,
				"versions_added": len(todoList.Versions),
				"tasks_added":    len(tasks),
			})
			return result, err
		},
	)

	// Get all changelog items
	s.AddTool(mcp.NewTool("get-changelog",
		mcp.WithDescription("Get all changelog entries"),
	),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			changelogEntries, err := changelogService.GetAll()
			if err != nil {
				return mcp.NewToolResultError(fmt.Sprintf("failed to get changelog entries: %w", err)), nil
			}
			result, err := newToolResultJSON(changelogEntries)
			return result, err
		},
	)

	// Get changelog items for a specific version
	s.AddTool(mcp.NewTool("get-changelog-by-version",
		mcp.WithDescription("Get changelog entries for a specific version"),
		mcp.WithString("version",
			mcp.PropertyOption(mcp.Required()),
			mcp.PropertyOption(mcp.Description("Version string (e.g., '1.0.0')")),
		),
	),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			version, ok := request.Params.Arguments["version"].(string)
			if !ok {
				return mcp.NewToolResultError("version parameter must be a string"), nil
			}

			entry, err := changelogService.GetByVersion(version)
			if err != nil {
				return mcp.NewToolResultError(fmt.Sprintf("failed to get changelog entry for version %s: %w", version, err)), nil
			}
			if entry == nil {
				return mcp.NewToolResultError(fmt.Sprintf("version not found: %s", version)), nil
			}
			result, err := newToolResultJSON(entry)
			return result, err
		},
	)

	// Add a new changelog version entry - using raw schema
	changelogSchema := map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"version": map[string]interface{}{
				"type":        "string",
				"description": "Version string (e.g., '1.0.0')",
			},
			"date": map[string]interface{}{
				"type":        "string",
				"description": "Release date (YYYY-MM-DD format)",
			},
			"added": map[string]interface{}{
				"type":        "array",
				"description": "List of new features added",
				"items": map[string]interface{}{
					"type": "string",
				},
			},
			"changed": map[string]interface{}{
				"type":        "array",
				"description": "List of changes to existing functionality",
				"items": map[string]interface{}{
					"type": "string",
				},
			},
			"deprecated": map[string]interface{}{
				"type":        "array",
				"description": "List of deprecated features",
				"items": map[string]interface{}{
					"type": "string",
				},
			},
			"removed": map[string]interface{}{
				"type":        "array",
				"description": "List of removed features",
				"items": map[string]interface{}{
					"type": "string",
				},
			},
			"fixed": map[string]interface{}{
				"type":        "array",
				"description": "List of bug fixes",
				"items": map[string]interface{}{
					"type": "string",
				},
			},
			"security": map[string]interface{}{
				"type":        "array",
				"description": "List of security fixes",
				"items": map[string]interface{}{
					"type": "string",
				},
			},
		},
		"required": []string{"version"},
	}
	changelogSchemaBytes, _ := json.Marshal(changelogSchema)
	s.AddTool(mcp.NewToolWithRawSchema("add-changelog-entry", 
		"Add a new changelog version entry", 
		json.RawMessage(changelogSchemaBytes)),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			version, ok := request.Params.Arguments["version"].(string)
			if !ok {
				return mcp.NewToolResultError("version parameter must be a string"), nil
			}

			date, ok := request.Params.Arguments["date"].(string)
			if !ok {
				// Default to today's date
				date = time.Now().Format("2006-01-02")
			}

			// Process content sections
			content := &changelog.ChangelogContent{}

			if added, ok := getStringArray(request.Params.Arguments, "added"); ok {
				content.Added = added
			}
			if changed, ok := getStringArray(request.Params.Arguments, "changed"); ok {
				content.Changed = changed
			}
			if deprecated, ok := getStringArray(request.Params.Arguments, "deprecated"); ok {
				content.Deprecated = deprecated
			}
			if removed, ok := getStringArray(request.Params.Arguments, "removed"); ok {
				content.Removed = removed
			}
			if fixed, ok := getStringArray(request.Params.Arguments, "fixed"); ok {
				content.Fixed = fixed
			}
			if security, ok := getStringArray(request.Params.Arguments, "security"); ok {
				content.Security = security
			}

			err := changelogService.AddEntry(version, date, content)
			if err != nil {
				if err == changelog.ErrVersionExists {
					return mcp.NewToolResultError(fmt.Sprintf("version already exists: %s", version)), nil
				}
				return mcp.NewToolResultError(fmt.Sprintf("failed to add changelog entry: %w", err)), nil
			}

			result, err := newToolResultJSON(map[string]bool{"success": true})
			return result, err
		},
	)

	// Update existing changelog entry - using raw schema
	updateChangelogSchema := map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"version": map[string]interface{}{
				"type":        "string",
				"description": "Version string (e.g., '1.0.0')",
			},
			"date": map[string]interface{}{
				"type":        "string",
				"description": "Updated release date (YYYY-MM-DD format)",
			},
			"added": map[string]interface{}{
				"type":        "array",
				"description": "Updated list of new features added",
				"items": map[string]interface{}{
					"type": "string",
				},
			},
			"changed": map[string]interface{}{
				"type":        "array",
				"description": "Updated list of changes to existing functionality",
				"items": map[string]interface{}{
					"type": "string",
				},
			},
			"deprecated": map[string]interface{}{
				"type":        "array",
				"description": "Updated list of deprecated features",
				"items": map[string]interface{}{
					"type": "string",
				},
			},
			"removed": map[string]interface{}{
				"type":        "array",
				"description": "Updated list of removed features",
				"items": map[string]interface{}{
					"type": "string",
				},
			},
			"fixed": map[string]interface{}{
				"type":        "array",
				"description": "Updated list of bug fixes",
				"items": map[string]interface{}{
					"type": "string",
				},
			},
			"security": map[string]interface{}{
				"type":        "array",
				"description": "Updated list of security fixes",
				"items": map[string]interface{}{
					"type": "string",
				},
			},
		},
		"required": []string{"version"},
	}
	updateChangelogSchemaBytes, _ := json.Marshal(updateChangelogSchema)
	s.AddTool(mcp.NewToolWithRawSchema("update-changelog-entry", 
		"Update an existing changelog entry", 
		json.RawMessage(updateChangelogSchemaBytes)),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			version, ok := request.Params.Arguments["version"].(string)
			if !ok {
				return mcp.NewToolResultError("version parameter must be a string"), nil
			}

			date, ok := request.Params.Arguments["date"].(string)
			if !ok {
				date = ""
			}

			// Process content sections
			content := &changelog.ChangelogContent{}

			if added, ok := getStringArray(request.Params.Arguments, "added"); ok {
				content.Added = added
			}
			if changed, ok := getStringArray(request.Params.Arguments, "changed"); ok {
				content.Changed = changed
			}
			if deprecated, ok := getStringArray(request.Params.Arguments, "deprecated"); ok {
				content.Deprecated = deprecated
			}
			if removed, ok := getStringArray(request.Params.Arguments, "removed"); ok {
				content.Removed = removed
			}
			if fixed, ok := getStringArray(request.Params.Arguments, "fixed"); ok {
				content.Fixed = fixed
			}
			if security, ok := getStringArray(request.Params.Arguments, "security"); ok {
				content.Security = security
			}

			err := changelogService.UpdateEntry(version, date, content)
			if err != nil {
				if err == changelog.ErrVersionNotFound {
					return mcp.NewToolResultError(fmt.Sprintf("version not found: %s", version)), nil
				}
				return mcp.NewToolResultError(fmt.Sprintf("failed to update changelog entry: %w", err)), nil
			}
			result, err := newToolResultJSON(map[string]bool{"success": true})
			return result, err
		},
	)

	// Import and format an existing CHANGELOG.md
	s.AddTool(mcp.NewTool("import-changelog",
		mcp.WithDescription("Import and format an existing CHANGELOG.md file"),
		mcp.WithString("source_path",
			mcp.PropertyOption(mcp.Required()),
			mcp.PropertyOption(mcp.Description("Path to the source CHANGELOG.md file")),
		),
	),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			sourcePath, ok := request.Params.Arguments["source_path"].(string)
			if !ok {
				return mcp.NewToolResultError("source_path parameter must be a string"), nil
			}

			// Create an absolute path if a relative path is provided
			if !filepath.IsAbs(sourcePath) {
				cwd, err := os.Getwd()
				if err != nil {
					return mcp.NewToolResultError(fmt.Sprintf("failed to get current working directory: %w", err)), nil
				}
				sourcePath = filepath.Join(cwd, sourcePath)
			}

			// Read the source file
			content, err := os.ReadFile(sourcePath)
			if err != nil {
				return mcp.NewToolResultError(fmt.Sprintf("failed to read source file: %w", err)), nil
			}

			// Split into lines
			lines := strings.Split(string(content), "\n")

			// Parse the content
			changelogEntries, err := changelogService.GetAll()
			if err != nil {
				return mcp.NewToolResultError(fmt.Sprintf("failed to load existing CHANGELOG file: %w", err)), nil
			}

			// Only proceed if the current CHANGELOG.md is empty
			if len(changelogEntries.Versions) > 0 {
				return mcp.NewToolResultError("CHANGELOG.md already exists and has content, cannot import"), nil
			}

			// Process the content
			var currentVersion string
			var currentDate string
			var currentSection string
			var entriesAdded int

			var added []string
			var changed []string
			var deprecated []string
			var removed []string
			var fixed []string
			var security []string

			for _, line := range lines {
				// Skip empty lines and title
				if line == "" || line == "# Changelog" {
					continue
				}

				// Check for version headers
				if strings.HasPrefix(line, "## [") && strings.Contains(line, "] - ") {
					// Save the previous version if exists
					if currentVersion != "" {
						content := &changelog.ChangelogContent{
							Added:      added,
							Changed:    changed,
							Deprecated: deprecated,
							Removed:    removed,
							Fixed:      fixed,
							Security:   security,
						}

						err := changelogService.AddEntry(currentVersion, currentDate, content)
						if err != nil && err != changelog.ErrVersionExists {
							return mcp.NewToolResultError(fmt.Sprintf("failed to add changelog entry: %w", err)), nil
						}
						entriesAdded++

						// Reset for next version
						added = nil
						changed = nil
						deprecated = nil
						removed = nil
						fixed = nil
						security = nil
					}

					// Parse version and date
					parts := strings.Split(line, "] - ")
					versionStr := strings.TrimPrefix(parts[0], "## [")
					currentVersion = versionStr
					currentDate = parts[1]
					currentSection = ""
					continue
				}

				// Check for section headers
				if strings.HasPrefix(line, "### ") {
					currentSection = strings.ToLower(strings.TrimPrefix(line, "### "))
					continue
				}

				// Process list items
				if strings.HasPrefix(line, "- ") && currentSection != "" {
					item := strings.TrimPrefix(line, "- ")

					switch currentSection {
					case "added":
						added = append(added, item)
					case "changed":
						changed = append(changed, item)
					case "deprecated":
						deprecated = append(deprecated, item)
					case "removed":
						removed = append(removed, item)
					case "fixed":
						fixed = append(fixed, item)
					case "security":
						security = append(security, item)
					}
				}
			}

			// Add the last version if exists
			if currentVersion != "" {
				content := &changelog.ChangelogContent{
					Added:      added,
					Changed:    changed,
					Deprecated: deprecated,
					Removed:    removed,
					Fixed:      fixed,
					Security:   security,
				}

				err := changelogService.AddEntry(currentVersion, currentDate, content)
				if err != nil && err != changelog.ErrVersionExists {
					return mcp.NewToolResultError(fmt.Sprintf("failed to add changelog entry: %w", err)), nil
				}
				entriesAdded++
			}
			result, err := newToolResultJSON(map[string]interface{}{
				"success":       true,
				"entries_added": entriesAdded,
			})
			return result, err
		},
	)

	// Generate a new CHANGELOG.md based on completed tasks in TODO.md
	s.AddTool(mcp.NewTool("generate-changelog-from-todo",
		mcp.WithDescription("Generate a new CHANGELOG.md entry based on completed tasks in TODO.md"),
		mcp.WithString("version",
			mcp.PropertyOption(mcp.Required()),
			mcp.PropertyOption(mcp.Description("Version string (e.g., '1.0.0')")),
		),
		mcp.WithString("date",
			mcp.PropertyOption(mcp.Description("Release date (YYYY-MM-DD format)")),
		),
	),
		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			version, ok := request.Params.Arguments["version"].(string)
			if !ok {
				return mcp.NewToolResultError("version parameter must be a string"), nil
			}

			date, ok := request.Params.Arguments["date"].(string)
			if !ok {
				// Default to today's date
				date = time.Now().Format("2006-01-02")
			}

			// Get tasks for the specified version
			versionTasks, err := todoService.GetByVersion(version)
			if err != nil {
				return mcp.NewToolResultError(fmt.Sprintf("failed to get tasks for version %s: %w", version, err)), nil
			}

			if versionTasks == nil {
				return mcp.NewToolResultError(fmt.Sprintf("version not found in TODO.md: %s", version)), nil
			}

			// Check if this version already exists in CHANGELOG
			existingEntry, err := changelogService.GetByVersion(version)
			if err != nil {
				return mcp.NewToolResultError(fmt.Sprintf("failed to check if changelog entry exists: %w", err)), nil
			}

			if existingEntry != nil {
				return mcp.NewToolResultError(fmt.Sprintf("changelog entry already exists for version %s", version)), nil
			}

			// Categorize completed tasks
			var added []string
			var changed []string
			var fixed []string

			// Helper function to process tasks recursively
			var processTasks func(tasks []*todo.Task)
			processTasks = func(tasks []*todo.Task) {
				for _, task := range tasks {
					if task.Completed {
						// Categorize based on common prefixes/keywords
						desc := task.Description
						lowerDesc := strings.ToLower(desc)

						if strings.HasPrefix(lowerDesc, "add") ||
							strings.HasPrefix(lowerDesc, "implement") ||
							strings.HasPrefix(lowerDesc, "create") {
							added = append(added, desc)
						} else if strings.HasPrefix(lowerDesc, "update") ||
							strings.HasPrefix(lowerDesc, "change") ||
							strings.HasPrefix(lowerDesc, "modify") ||
							strings.HasPrefix(lowerDesc, "enhance") ||
							strings.HasPrefix(lowerDesc, "improve") {
							changed = append(changed, desc)
						} else if strings.HasPrefix(lowerDesc, "fix") ||
							strings.HasPrefix(lowerDesc, "correct") ||
							strings.HasPrefix(lowerDesc, "resolve") {
							fixed = append(fixed, desc)
						} else {
							// Default to Added if cannot categorize
							added = append(added, desc)
						}
					}

					// Process subtasks
					if len(task.SubTasks) > 0 {
						processTasks(task.SubTasks)
					}
				}
			}

			processTasks(versionTasks.Tasks)

			// Create changelog entry
			content := &changelog.ChangelogContent{
				Added:   added,
				Changed: changed,
				Fixed:   fixed,
			}

			err = changelogService.AddEntry(version, date, content)
			if err != nil {
				return mcp.NewToolResultError(fmt.Sprintf("failed to add changelog entry: %w", err)), nil
			}

			result, err := newToolResultJSON(map[string]interface{}{
				"success":       true,
				"added_items":   len(added),
				"changed_items": len(changed),
				"fixed_items":   len(fixed),
			})
			return result, err
		},
	)
}

// Helper function to extract string arrays from the arguments
func getStringArray(args map[string]any, key string) ([]string, bool) {
	if val, ok := args[key]; ok {
		if arr, ok := val.([]interface{}); ok {
			result := make([]string, 0, len(arr))
			for _, item := range arr {
				if strItem, ok := item.(string); ok {
					result = append(result, strItem)
				}
			}
			return result, true
		} else if arrStr, ok := val.([]string); ok {
			return arrStr, true
		}
	}
	return nil, false
}

```