Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/features.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<div class="container">
<h2 class="section-title">Features</h2>
<p class="section-subtitle">Everything you need to build MCP servers quickly and efficiently</p>
<p><strong>Version:</strong> v0.4.1 (latest release)</p>
<div class="features-grid">
<div class="feature-card">
<span class="badge-implemented">Implemented</span>
Expand Down
8 changes: 8 additions & 0 deletions internal/commands/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,11 @@ func TestGlobalFlags(t *testing.T) {
t.Error("expected global flag 'quiet' to be defined")
}
}

func TestRootCommand_ExecuteUnknown(t *testing.T) {
rootCmd := MakeRootCommand(version)
rootCmd.SetArgs([]string{"unknown"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("unexpected execute error: %v", err)
}
}
4 changes: 3 additions & 1 deletion internal/generators/python.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ func (g *PythonGenerator) generateTemplateMap(output string, templates map[strin
return nil
}

// generateEntities renders a template for each provided item.
// generateEntities iterates over the given items, converts each to template data
// using conv, and writes the resulting file to the specified directory.
// The file is named after the item with a .py extension.
func generateEntities[T any](g *PythonGenerator, output, dir, tmpl string, items []T, conv func(T) (string, interface{})) error {
for _, item := range items {
name, d := conv(item)
Expand Down
82 changes: 82 additions & 0 deletions internal/generators/python_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package generators
import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/aawadall/mcpcli/internal/core"
Expand Down Expand Up @@ -85,3 +86,84 @@ func TestPythonGenerator_GenerateWithExtras(t *testing.T) {
}
}
}

func TestPythonCreateDirectoryStructure_Error(t *testing.T) {
tmpDir := t.TempDir()
// create a file where a directory is expected
if err := os.WriteFile(filepath.Join(tmpDir, "src"), []byte("file"), 0644); err != nil {
t.Fatalf("setup failed: %v", err)
}
g := NewPythonGenerator()
if err := g.createDirectoryStructure(tmpDir); err == nil {
t.Fatal("expected error when creating directories over existing file")
}
}

func TestPythonGenerateTemplate_ReadError(t *testing.T) {
g := NewPythonGenerator()
tmpDir := t.TempDir()
err := g.generateTemplate("missing.tmpl", filepath.Join(tmpDir, "out.py"), nil)
if err == nil || !strings.Contains(err.Error(), "failed to read template") {
t.Fatalf("expected template read error, got %v", err)
}
}

func TestPythonGenerateTemplate_CreateError(t *testing.T) {
g := NewPythonGenerator()
tmpDir := t.TempDir()
tPath := "templates/python/stdio/src/main.py.tmpl"
outPath := filepath.Join(tmpDir, "nope", "file.py")
err := g.generateTemplate(tPath, outPath, nil)
if err == nil || !strings.Contains(err.Error(), "failed to create file") {
t.Fatalf("expected file creation error, got %v", err)
}
}

func TestPythonGenerateEntities_Error(t *testing.T) {
g := NewPythonGenerator()
tmpDir := t.TempDir()
items := []core.Tool{{Name: "bad"}}
conv := func(t core.Tool) (string, interface{}) { return t.Name, struct{ Tool core.Tool }{t} }
err := generateEntities(g, tmpDir, "src/tools", "missing.tmpl", items, conv)
if err == nil || !strings.Contains(err.Error(), "failed to read template") {
t.Fatalf("expected template read error, got %v", err)
}
}

func TestPythonGenerateTemplateMap_Error(t *testing.T) {
g := NewPythonGenerator()
tmpDir := t.TempDir()
templates := map[string]string{"missing.tmpl": "out.py"}
err := g.generateTemplateMap(tmpDir, templates, nil)
if err == nil || !strings.Contains(err.Error(), "failed to read template") {
t.Fatalf("expected template map error, got %v", err)
}
}

func TestPythonGenerateFromTemplates_Error(t *testing.T) {
g := NewPythonGenerator()
tmpDir := t.TempDir()
// create a file "src" to block directory creation
if err := os.WriteFile(filepath.Join(tmpDir, "src"), []byte("file"), 0644); err != nil {
t.Fatalf("setup failed: %v", err)
}
cfg := &core.ProjectConfig{Name: "bad", Language: "python", Transport: "stdio", Output: tmpDir}
data := cfg.GetTemplateData()
err := g.generateFromTemplates(tmpDir, data)
if err == nil || !strings.Contains(err.Error(), "failed to create file") {
t.Fatalf("expected file creation error, got %v", err)
}
}

func TestPythonGenerator_Generate_DirectoryError(t *testing.T) {
tmpDir := t.TempDir()
if err := os.WriteFile(filepath.Join(tmpDir, "src"), []byte("file"), 0644); err != nil {
t.Fatalf("setup failed: %v", err)
}
cfg := &core.ProjectConfig{Name: "badpython", Language: "python", Transport: "stdio", Output: tmpDir}
g := NewPythonGenerator()
err := g.Generate(cfg)
if err == nil || !strings.Contains(err.Error(), "failed to create directory structure") {
t.Fatalf("expected directory structure error, got %v", err)
}
}
31 changes: 31 additions & 0 deletions internal/handlers/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,34 @@ func TestPrintNextSteps(t *testing.T) {
t.Fatalf("output missing next steps")
}
}

func captureGenOutput(f func()) string {
r, w, _ := os.Pipe()
stdout := os.Stdout
os.Stdout = w
defer func() { os.Stdout = stdout }()
f()
w.Close()
var buf bytes.Buffer
io.Copy(&buf, r)
return buf.String()
}

func TestPrintNextSteps_OtherLanguages(t *testing.T) {
cases := []struct {
lang string
expect string
}{
{"javascript", "npm install"},
{"java", "mvn package"},
{"python", "python src/main.py"},
}
for _, c := range cases {
out := captureGenOutput(func() {
printNextSteps(&GenerateOptions{Name: "p", Language: c.lang, Output: "out"})
})
if !strings.Contains(out, c.expect) {
t.Fatalf("expected %s instructions", c.lang)
}
}
}
45 changes: 45 additions & 0 deletions internal/handlers/test_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package handlers

import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"

"github.com/aawadall/mcpcli/internal/core"
Expand Down Expand Up @@ -81,3 +85,44 @@ func TestLoadMCPConfig_Project(t *testing.T) {
t.Fatalf("expected proj name, got %s", cfg.Name)
}
}

func captureOutput(f func()) string {
r, w, _ := os.Pipe()
stdout := os.Stdout
os.Stdout = w
defer func() { os.Stdout = stdout }()
f()
w.Close()
var buf bytes.Buffer
io.Copy(&buf, r)
return buf.String()
}

func TestFormatAndPrintResult_Error(t *testing.T) {
out := captureOutput(func() {
formatAndPrintResult("Tools", nil, fmt.Errorf("boom"))
})
if !strings.Contains(out, "Failed to list tools") {
t.Fatalf("unexpected output: %s", out)
}
}

func TestFormatAndPrintResult_MCPError(t *testing.T) {
resp := &core.Response{Error: &core.Error{Message: "bad"}}
out := captureOutput(func() {
formatAndPrintResult("Tools", resp, nil)
})
if !strings.Contains(out, "MCP error") {
t.Fatalf("unexpected output: %s", out)
}
}

func TestFormatAndPrintResult_Success(t *testing.T) {
resp := &core.Response{Result: "ok"}
out := captureOutput(func() {
formatAndPrintResult("Tools", resp, nil)
})
if !strings.Contains(out, "✅ Tools: ok") {
t.Fatalf("unexpected output: %s", out)
}
}