diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-10 19:36:02 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-10 19:36:02 +0200 |
| commit | ae38b11a09964e2c291a144c72814559d12d3b96 (patch) | |
| tree | 5f75b77364b7e1013e6eb84998a3044a3cc50710 | |
| parent | 5551695f3b0d10c9a22cfacdb10c2cf7bd572421 (diff) | |
Refactor MCP server to meet project standards and remove built-in prompts
Ensures all code complies with AGENTS.md standards:
- Refactored functions to be under 50 lines each
- Removed built-in prompts - all prompts now served from database only
- Split handlePromptsCreate (72→37 lines) with helper functions
- Split handlePromptsUpdate (76→44 lines) with helper function
- Deleted internal/promptstore/builtin.go (no longer needed)
- Updated tests to create prompts dynamically instead of relying on built-ins
All tests pass with 81.5% coverage maintained.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
| -rw-r--r-- | internal/mcp/server.go | 87 | ||||
| -rw-r--r-- | internal/promptstore/builtin.go | 156 | ||||
| -rw-r--r-- | internal/promptstore/store.go | 53 | ||||
| -rw-r--r-- | internal/promptstore/store_test.go | 217 |
4 files changed, 162 insertions, 351 deletions
diff --git a/internal/mcp/server.go b/internal/mcp/server.go index f6479d9..4bc66dd 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -318,20 +318,45 @@ func (s *Server) handlePromptsCreate(req Request) { } // Validate required fields - if params.Name == "" { - s.sendError(req.ID, ErrCodeInvalidParams, "Prompt name is required") + if err := validateCreateParams(params); err != nil { + s.sendError(req.ID, ErrCodeInvalidParams, err.Error()) return } - if params.Title == "" { - s.sendError(req.ID, ErrCodeInvalidParams, "Prompt title is required") + + // Build prompt from params + prompt := buildPromptFromCreateParams(params) + + if err := s.store.Create(prompt); err != nil { + s.logger.Printf("create prompt error: %v", err) + s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to create prompt: %v", err)) return } + + result := PromptOperationResult{ + Success: true, + Message: fmt.Sprintf("Created prompt: %s", params.Name), + } + + s.logger.Printf("created prompt: %s", params.Name) + s.sendResponse(req.ID, result) +} + +// validateCreateParams validates required fields for prompt creation. +func validateCreateParams(params CreatePromptRequest) error { + if params.Name == "" { + return fmt.Errorf("prompt name is required") + } + if params.Title == "" { + return fmt.Errorf("prompt title is required") + } if len(params.Messages) == 0 { - s.sendError(req.ID, ErrCodeInvalidParams, "At least one message is required") - return + return fmt.Errorf("at least one message is required") } + return nil +} - // Create prompt +// buildPromptFromCreateParams converts CreatePromptRequest to Prompt. +func buildPromptFromCreateParams(params CreatePromptRequest) *promptstore.Prompt { prompt := &promptstore.Prompt{ Name: params.Name, Title: params.Title, @@ -361,19 +386,7 @@ func (s *Server) handlePromptsCreate(req Request) { }) } - if err := s.store.Create(prompt); err != nil { - s.logger.Printf("create prompt error: %v", err) - s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to create prompt: %v", err)) - return - } - - result := PromptOperationResult{ - Success: true, - Message: fmt.Sprintf("Created prompt: %s", params.Name), - } - - s.logger.Printf("created prompt: %s", params.Name) - s.sendResponse(req.ID, result) + return prompt } // handlePromptsUpdate processes the prompts/update request. @@ -406,6 +419,26 @@ func (s *Server) handlePromptsUpdate(req Request) { return } + // Apply updates to existing prompt + applyPromptUpdates(existing, params) + + if err := s.store.Update(existing); err != nil { + s.logger.Printf("update prompt error: %v", err) + s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to update prompt: %v", err)) + return + } + + result := PromptOperationResult{ + Success: true, + Message: fmt.Sprintf("Updated prompt: %s", params.Name), + } + + s.logger.Printf("updated prompt: %s", params.Name) + s.sendResponse(req.ID, result) +} + +// applyPromptUpdates applies update parameters to an existing prompt. +func applyPromptUpdates(existing *promptstore.Prompt, params UpdatePromptRequest) { // Update fields (only if provided) if params.Title != "" { existing.Title = params.Title @@ -440,20 +473,6 @@ func (s *Server) handlePromptsUpdate(req Request) { } existing.Updated = time.Now() - - if err := s.store.Update(existing); err != nil { - s.logger.Printf("update prompt error: %v", err) - s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to update prompt: %v", err)) - return - } - - result := PromptOperationResult{ - Success: true, - Message: fmt.Sprintf("Updated prompt: %s", params.Name), - } - - s.logger.Printf("updated prompt: %s", params.Name) - s.sendResponse(req.ID, result) } // handlePromptsDelete processes the prompts/delete request. diff --git a/internal/promptstore/builtin.go b/internal/promptstore/builtin.go deleted file mode 100644 index ac4c830..0000000 --- a/internal/promptstore/builtin.go +++ /dev/null @@ -1,156 +0,0 @@ -// Summary: Built-in prompts for common development tasks. -package promptstore - -import "time" - -// GetBuiltinPrompts returns the default set of prompts. -// These are written to default.jsonl on first run. -func GetBuiltinPrompts() []Prompt { - now := time.Now() - - return []Prompt{ - { - Name: "code_review", - Title: "Request Code Review", - Description: "Analyzes code quality, style, and suggests improvements", - Arguments: []PromptArgument{ - {Name: "code", Description: "The code to review", Required: true}, - }, - Messages: []PromptMessage{ - { - Role: "user", - Content: MessageContent{ - Type: "text", - Text: "Please review the following code for quality, style, and potential issues:\n\n{{code}}", - }, - }, - }, - Tags: []string{"development", "review", "quality"}, - Created: now, - Updated: now, - }, - { - Name: "explain_code", - Title: "Explain Code", - Description: "Provides detailed explanation of what code does", - Arguments: []PromptArgument{ - {Name: "code", Description: "The code to explain", Required: true}, - }, - Messages: []PromptMessage{ - { - Role: "user", - Content: MessageContent{ - Type: "text", - Text: "Please explain in detail what the following code does:\n\n{{code}}", - }, - }, - }, - Tags: []string{"development", "documentation", "learning"}, - Created: now, - Updated: now, - }, - { - Name: "generate_tests", - Title: "Generate Unit Tests", - Description: "Generates unit tests for a function or class", - Arguments: []PromptArgument{ - {Name: "code", Description: "The code to test", Required: true}, - {Name: "language", Description: "Programming language", Required: false}, - }, - Messages: []PromptMessage{ - { - Role: "user", - Content: MessageContent{ - Type: "text", - Text: "Generate comprehensive unit tests for the following code:\n\nLanguage: {{language}}\n\n{{code}}", - }, - }, - }, - Tags: []string{"development", "testing", "tdd"}, - Created: now, - Updated: now, - }, - { - Name: "document_function", - Title: "Generate Documentation", - Description: "Generates documentation comments and docstrings", - Arguments: []PromptArgument{ - {Name: "code", Description: "The code to document", Required: true}, - }, - Messages: []PromptMessage{ - { - Role: "user", - Content: MessageContent{ - Type: "text", - Text: "Generate comprehensive documentation for the following code:\n\n{{code}}", - }, - }, - }, - Tags: []string{"development", "documentation"}, - Created: now, - Updated: now, - }, - { - Name: "simplify_code", - Title: "Simplify Code", - Description: "Simplifies complex code while preserving behavior", - Arguments: []PromptArgument{ - {Name: "code", Description: "The code to simplify", Required: true}, - }, - Messages: []PromptMessage{ - { - Role: "user", - Content: MessageContent{ - Type: "text", - Text: "Simplify the following code while preserving its behavior:\n\n{{code}}", - }, - }, - }, - Tags: []string{"development", "refactoring", "quality"}, - Created: now, - Updated: now, - }, - { - Name: "fix_bugs", - Title: "Analyze and Fix Bugs", - Description: "Analyzes code for bugs and suggests fixes", - Arguments: []PromptArgument{ - {Name: "code", Description: "The code to analyze", Required: true}, - {Name: "error", Description: "Error message or symptoms", Required: false}, - }, - Messages: []PromptMessage{ - { - Role: "user", - Content: MessageContent{ - Type: "text", - Text: "Analyze the following code for bugs and suggest fixes:\n\nError: {{error}}\n\nCode:\n{{code}}", - }, - }, - }, - Tags: []string{"development", "debugging", "bug-fix"}, - Created: now, - Updated: now, - }, - { - Name: "refactor_extract", - Title: "Extract Function", - Description: "Extracts code into a separate, reusable function", - Arguments: []PromptArgument{ - {Name: "code", Description: "The code to extract", Required: true}, - {Name: "function_name", Description: "Desired function name", Required: false}, - }, - Messages: []PromptMessage{ - { - Role: "user", - Content: MessageContent{ - Type: "text", - Text: "Extract the following code into a separate function named {{function_name}}:\n\n{{code}}", - }, - }, - }, - Tags: []string{"development", "refactoring"}, - Created: now, - Updated: now, - }, - } -} diff --git a/internal/promptstore/store.go b/internal/promptstore/store.go index c1fcb9f..e789dda 100644 --- a/internal/promptstore/store.go +++ b/internal/promptstore/store.go @@ -72,39 +72,9 @@ func NewJSONLStore(dataDir string) (PromptStore, error) { maxBackups: 10, // Keep last 10 backups } - // Initialize default.jsonl with built-in prompts if it doesn't exist - defaultPath := filepath.Join(dataDir, "default.jsonl") - if _, err := os.Stat(defaultPath); os.IsNotExist(err) { - if err := store.writeBuiltinPrompts(); err != nil { - return nil, fmt.Errorf("cannot write built-in prompts: %w", err) - } - } - return store, nil } -// writeBuiltinPrompts writes the built-in prompts to default.jsonl. -func (s *JSONLStore) writeBuiltinPrompts() error { - prompts := GetBuiltinPrompts() - defaultPath := filepath.Join(s.dataDir, "default.jsonl") - - var lines []byte - for _, p := range prompts { - data, err := json.Marshal(p) - if err != nil { - return fmt.Errorf("marshal built-in prompt: %w", err) - } - lines = append(lines, data...) - lines = append(lines, '\n') - } - - if err := s.writeFileFn(defaultPath, lines, 0o644); err != nil { - return fmt.Errorf("write default.jsonl: %w", err) - } - - return nil -} - // List returns prompts with pagination. // cursor format: "<file>:<offset>" where file is "default" or "user", offset is line number. func (s *JSONLStore) List(cursor string, limit int) ([]Prompt, string, error) { @@ -321,33 +291,14 @@ func (s *JSONLStore) hasAllTags(promptTags, searchTags []string) bool { return true } -// loadAllPrompts loads prompts from both default.jsonl and user.jsonl. +// loadAllPrompts loads prompts from user.jsonl. func (s *JSONLStore) loadAllPrompts() ([]Prompt, error) { - defaultPrompts, err := s.loadPromptsFromFile("default.jsonl") - if err != nil && !os.IsNotExist(err) { - return nil, err - } - userPrompts, err := s.loadPromptsFromFile("user.jsonl") if err != nil && !os.IsNotExist(err) { return nil, err } - // Merge (user prompts override built-ins by name) - promptMap := make(map[string]Prompt) - for _, p := range defaultPrompts { - promptMap[p.Name] = p - } - for _, p := range userPrompts { - promptMap[p.Name] = p - } - - var all []Prompt - for _, p := range promptMap { - all = append(all, p) - } - - return all, nil + return userPrompts, nil } // loadPromptsFromFile reads prompts from a JSONL file. diff --git a/internal/promptstore/store_test.go b/internal/promptstore/store_test.go index 8dd506a..167dcf6 100644 --- a/internal/promptstore/store_test.go +++ b/internal/promptstore/store_test.go @@ -2,60 +2,57 @@ package promptstore import ( - "os" - "path/filepath" + "fmt" "testing" "time" ) func TestJSONLStore_Get(t *testing.T) { - tests := []struct { - name string - promptName string - want *Prompt - wantErr bool - }{ - { - name: "get built-in prompt", - promptName: "code_review", - want: &Prompt{ - Name: "code_review", - Title: "Request Code Review", - Description: "Analyzes code quality, style, and suggests improvements", - }, - wantErr: false, - }, - { - name: "prompt not found", - promptName: "nonexistent", - want: nil, - wantErr: true, - }, - } + t.Run("get existing prompt", func(t *testing.T) { + tmpDir := t.TempDir() + store, err := NewJSONLStore(tmpDir) + if err != nil { + t.Fatalf("NewJSONLStore() error = %v", err) + } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tmpDir := t.TempDir() - store, err := NewJSONLStore(tmpDir) - if err != nil { - t.Fatalf("NewJSONLStore() error = %v", err) - } - - got, err := store.Get(tt.promptName) - if (err != nil) != tt.wantErr { - t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr { - if got.Name != tt.want.Name { - t.Errorf("Get() name = %v, want %v", got.Name, tt.want.Name) - } - if got.Title != tt.want.Title { - t.Errorf("Get() title = %v, want %v", got.Title, tt.want.Title) - } - } - }) - } + // Create a test prompt first + testPrompt := &Prompt{ + Name: "test_prompt", + Title: "Test Prompt", + Description: "A test prompt", + Messages: []PromptMessage{{Role: "user", Content: MessageContent{Type: "text", Text: "Test"}}}, + Created: time.Now(), + Updated: time.Now(), + } + if err := store.Create(testPrompt); err != nil { + t.Fatalf("Create() error = %v", err) + } + + // Now get it + got, err := store.Get("test_prompt") + if err != nil { + t.Fatalf("Get() error = %v", err) + } + if got.Name != "test_prompt" { + t.Errorf("Get() name = %v, want test_prompt", got.Name) + } + if got.Title != "Test Prompt" { + t.Errorf("Get() title = %v, want Test Prompt", got.Title) + } + }) + + t.Run("prompt not found", func(t *testing.T) { + tmpDir := t.TempDir() + store, err := NewJSONLStore(tmpDir) + if err != nil { + t.Fatalf("NewJSONLStore() error = %v", err) + } + + _, err = store.Get("nonexistent") + if err == nil { + t.Error("Get() expected error for nonexistent prompt, got nil") + } + }) } func TestJSONLStore_List(t *testing.T) { @@ -65,15 +62,29 @@ func TestJSONLStore_List(t *testing.T) { t.Fatalf("NewJSONLStore() error = %v", err) } + // Create test prompts + for i := 0; i < 7; i++ { + prompt := &Prompt{ + Name: fmt.Sprintf("test%d", i), + Title: fmt.Sprintf("Test %d", i), + Messages: []PromptMessage{{Role: "user", Content: MessageContent{Type: "text", Text: "Test"}}}, + Created: time.Now(), + Updated: time.Now(), + } + if err := store.Create(prompt); err != nil { + t.Fatalf("Create() error = %v", err) + } + } + // List all prompts prompts, cursor, err := store.List("", 100) if err != nil { t.Fatalf("List() error = %v", err) } - // Should have built-in prompts - if len(prompts) < 5 { - t.Errorf("List() got %d prompts, want at least 5", len(prompts)) + // Should have all prompts + if len(prompts) != 7 { + t.Errorf("List() got %d prompts, want 7", len(prompts)) } // No cursor for full list @@ -98,10 +109,10 @@ func TestJSONLStore_List(t *testing.T) { if err != nil { t.Fatalf("List() error = %v", err) } - if len(prompts2) == 0 { - t.Error("List() second page empty") + if len(prompts2) != 3 { + t.Errorf("List() got %d prompts on page 2, want 3", len(prompts2)) } - if cursor2 == "" && len(prompts) > 6 { + if cursor2 == "" { t.Error("List() expected cursor2, got empty") } } @@ -236,14 +247,50 @@ func TestJSONLStore_SearchByTags(t *testing.T) { t.Fatalf("NewJSONLStore() error = %v", err) } + // Create test prompts with tags + prompt1 := &Prompt{ + Name: "test1", + Title: "Test 1", + Tags: []string{"development", "review"}, + Messages: []PromptMessage{{Role: "user", Content: MessageContent{Type: "text", Text: "Test 1"}}}, + Created: time.Now(), + Updated: time.Now(), + } + prompt2 := &Prompt{ + Name: "test2", + Title: "Test 2", + Tags: []string{"development"}, + Messages: []PromptMessage{{Role: "user", Content: MessageContent{Type: "text", Text: "Test 2"}}}, + Created: time.Now(), + Updated: time.Now(), + } + prompt3 := &Prompt{ + Name: "test3", + Title: "Test 3", + Tags: []string{"testing"}, + Messages: []PromptMessage{{Role: "user", Content: MessageContent{Type: "text", Text: "Test 3"}}}, + Created: time.Now(), + Updated: time.Now(), + } + + if err := store.Create(prompt1); err != nil { + t.Fatalf("Create() error = %v", err) + } + if err := store.Create(prompt2); err != nil { + t.Fatalf("Create() error = %v", err) + } + if err := store.Create(prompt3); err != nil { + t.Fatalf("Create() error = %v", err) + } + // Search for development tag results, err := store.SearchByTags([]string{"development"}) if err != nil { t.Fatalf("SearchByTags() error = %v", err) } - if len(results) < 3 { - t.Errorf("SearchByTags() got %d results, want at least 3", len(results)) + if len(results) != 2 { + t.Errorf("SearchByTags() got %d results, want 2", len(results)) } // Search for multiple tags @@ -252,60 +299,10 @@ func TestJSONLStore_SearchByTags(t *testing.T) { t.Fatalf("SearchByTags() error = %v", err) } - // Should find code_review prompt - found := false - for _, p := range results { - if p.Name == "code_review" { - found = true - break - } - } - if !found { - t.Error("SearchByTags() should find code_review prompt") - } -} - -func TestBuiltinPrompts(t *testing.T) { - prompts := GetBuiltinPrompts() - - if len(prompts) < 5 { - t.Errorf("GetBuiltinPrompts() got %d prompts, want at least 5", len(prompts)) - } - - // Verify each prompt has required fields - for _, p := range prompts { - if p.Name == "" { - t.Error("Prompt missing name") - } - if p.Title == "" { - t.Errorf("Prompt %s missing title", p.Name) - } - if len(p.Messages) == 0 { - t.Errorf("Prompt %s has no messages", p.Name) - } + if len(results) != 1 { + t.Errorf("SearchByTags() got %d results, want 1", len(results)) } -} - -func TestJSONLStore_DefaultFileCreation(t *testing.T) { - tmpDir := t.TempDir() - _, err := NewJSONLStore(tmpDir) - if err != nil { - t.Fatalf("NewJSONLStore() error = %v", err) - } - - // Verify default.jsonl was created - defaultPath := filepath.Join(tmpDir, "default.jsonl") - if _, err := os.Stat(defaultPath); os.IsNotExist(err) { - t.Error("default.jsonl was not created") - } - - // Verify it contains prompts - data, err := os.ReadFile(defaultPath) - if err != nil { - t.Fatalf("ReadFile() error = %v", err) - } - - if len(data) == 0 { - t.Error("default.jsonl is empty") + if results[0].Name != "test1" { + t.Errorf("SearchByTags() got prompt %s, want test1", results[0].Name) } } |
