summaryrefslogtreecommitdiff
path: root/cmd/hexai-mcp-server
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-16 03:10:55 +0200
committerPaul Buetow <paul@buetow.org>2026-03-16 03:10:55 +0200
commit1fc1611fa99993cab5dc8bf0844183285296e3b2 (patch)
treec5c9b8b5abac5b5d4c0d56ed90b0580184cc4383 /cmd/hexai-mcp-server
parent12090f25a3677291863dbb80277bdad3eaec0324 (diff)
Release v0.24.0v0.24.0
Bring unit test coverage from ~75% to 85.1% project-wide. All internal packages now exceed 80% coverage. Refactored cmd entrypoints to extract testable run() functions with injectable seams. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'cmd/hexai-mcp-server')
-rw-r--r--cmd/hexai-mcp-server/main.go65
-rw-r--r--cmd/hexai-mcp-server/main_test.go166
2 files changed, 213 insertions, 18 deletions
diff --git a/cmd/hexai-mcp-server/main.go b/cmd/hexai-mcp-server/main.go
index 557172f..1f52616 100644
--- a/cmd/hexai-mcp-server/main.go
+++ b/cmd/hexai-mcp-server/main.go
@@ -4,7 +4,7 @@ package main
import (
"flag"
"fmt"
- "log"
+ "io"
"os"
"codeberg.org/snonux/hexai/internal"
@@ -12,6 +12,12 @@ import (
"codeberg.org/snonux/hexai/internal/hexaimcp"
)
+// Seams for testing: override in tests to avoid launching real MCP server.
+var (
+ runMCP = hexaimcp.Run
+ runBackfill = hexaimcp.RunBackfill
+)
+
// printDeprecationWarning outputs a deprecation notice to stderr explaining
// that hexai-mcp-server is experimental and not actively maintained.
func printDeprecationWarning() {
@@ -36,6 +42,17 @@ Use at your own risk.
fmt.Fprintln(os.Stderr, warning)
}
+// mcpOptions holds the parsed command-line flags for the MCP server.
+type mcpOptions struct {
+ logPath string
+ configPath string
+ promptsDir string
+ slashCommandSync bool
+ slashCommandDir string
+ syncAll bool
+ showVersion bool
+}
+
func main() {
printDeprecationWarning()
@@ -49,33 +66,45 @@ func main() {
showVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
- if *showVersion {
- fmt.Println(internal.Version)
- return
+ opts := mcpOptions{
+ logPath: *logPath,
+ configPath: *configPath,
+ promptsDir: *promptsDir,
+ slashCommandSync: *slashCommandSync,
+ slashCommandDir: *slashCommandDir,
+ syncAll: *syncAll,
+ showVersion: *showVersion,
+ }
+ if err := run(opts, os.Stdin, os.Stdout, os.Stderr); err != nil {
+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
+ os.Exit(1)
}
+}
- // If prompts-dir is specified, set environment variable for RunWithFactory
- if *promptsDir != "" {
- os.Setenv("HEXAI_MCP_PROMPTS_DIR", *promptsDir)
+// run executes the MCP server logic with the given options and I/O streams.
+func run(opts mcpOptions, stdin io.Reader, stdout, stderr io.Writer) error {
+ // Set environment variables for RunWithFactory based on flag values
+ if opts.promptsDir != "" {
+ os.Setenv("HEXAI_MCP_PROMPTS_DIR", opts.promptsDir)
}
- if *slashCommandSync {
+ if opts.slashCommandSync {
os.Setenv("HEXAI_MCP_SLASHCOMMAND_SYNC", "true")
}
- if *slashCommandDir != "" {
- os.Setenv("HEXAI_MCP_SLASHCOMMAND_DIR", *slashCommandDir)
+ if opts.slashCommandDir != "" {
+ os.Setenv("HEXAI_MCP_SLASHCOMMAND_DIR", opts.slashCommandDir)
}
- // Handle backfill operation
- if *syncAll {
- if err := hexaimcp.RunBackfill(*logPath, *configPath); err != nil {
- log.Fatalf("backfill error: %v", err)
- }
- return
+ if opts.showVersion {
+ fmt.Fprintln(stdout, internal.Version)
+ return nil
}
- if err := hexaimcp.Run(*logPath, *configPath, os.Stdin, os.Stdout, os.Stderr); err != nil {
- log.Fatalf("server error: %v", err)
+ // Handle backfill operation
+ if opts.syncAll {
+ return runBackfill(opts.logPath, opts.configPath)
}
+
+ return runMCP(opts.logPath, opts.configPath, stdin, stdout, stderr)
}
// defaultLogPath returns the default MCP log file path in the state directory.
diff --git a/cmd/hexai-mcp-server/main_test.go b/cmd/hexai-mcp-server/main_test.go
new file mode 100644
index 0000000..cf48954
--- /dev/null
+++ b/cmd/hexai-mcp-server/main_test.go
@@ -0,0 +1,166 @@
+package main
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "os"
+ "strings"
+ "testing"
+
+ "codeberg.org/snonux/hexai/internal"
+)
+
+func TestPrintDeprecationWarning(t *testing.T) {
+ r, w, err := os.Pipe()
+ if err != nil {
+ t.Fatalf("failed to create pipe: %v", err)
+ }
+
+ oldStderr := os.Stderr
+ os.Stderr = w
+ defer func() { os.Stderr = oldStderr }()
+
+ printDeprecationWarning()
+
+ if err := w.Close(); err != nil {
+ t.Fatalf("failed to close pipe writer: %v", err)
+ }
+
+ b, err := io.ReadAll(r)
+ if err != nil {
+ t.Fatalf("failed to read pipe: %v", err)
+ }
+
+ output := string(b)
+ for _, want := range []string{"DEPRECATION NOTICE", "EXPERIMENTAL", "NOT ACTIVELY MAINTAINED"} {
+ if !strings.Contains(output, want) {
+ t.Errorf("expected %q in output, got %q", want, output)
+ }
+ }
+}
+
+func TestDefaultLogPath(t *testing.T) {
+ path := defaultLogPath()
+ if path == "" {
+ t.Fatal("expected non-empty log path")
+ }
+ if !strings.HasSuffix(path, "hexai-mcp-server.log") {
+ t.Errorf("expected path to end with hexai-mcp-server.log, got %q", path)
+ }
+}
+
+func TestRun_ShowVersion(t *testing.T) {
+ var stdout bytes.Buffer
+ opts := mcpOptions{showVersion: true}
+ if err := run(opts, nil, &stdout, nil); err != nil {
+ t.Fatalf("run --version: %v", err)
+ }
+ got := strings.TrimSpace(stdout.String())
+ if got != internal.Version {
+ t.Fatalf("expected version %q, got %q", internal.Version, got)
+ }
+}
+
+func TestRun_SetsPromptsDir(t *testing.T) {
+ t.Setenv("HEXAI_MCP_PROMPTS_DIR", "")
+ opts := mcpOptions{showVersion: true, promptsDir: "/tmp/test-prompts"}
+ var stdout bytes.Buffer
+ if err := run(opts, nil, &stdout, nil); err != nil {
+ t.Fatalf("run: %v", err)
+ }
+ if got := os.Getenv("HEXAI_MCP_PROMPTS_DIR"); got != "/tmp/test-prompts" {
+ t.Fatalf("expected HEXAI_MCP_PROMPTS_DIR=/tmp/test-prompts, got %q", got)
+ }
+}
+
+func TestRun_SetsSlashCommandSync(t *testing.T) {
+ t.Setenv("HEXAI_MCP_SLASHCOMMAND_SYNC", "")
+ opts := mcpOptions{showVersion: true, slashCommandSync: true}
+ var stdout bytes.Buffer
+ if err := run(opts, nil, &stdout, nil); err != nil {
+ t.Fatalf("run: %v", err)
+ }
+ if got := os.Getenv("HEXAI_MCP_SLASHCOMMAND_SYNC"); got != "true" {
+ t.Fatalf("expected HEXAI_MCP_SLASHCOMMAND_SYNC=true, got %q", got)
+ }
+}
+
+func TestRun_SetsSlashCommandDir(t *testing.T) {
+ t.Setenv("HEXAI_MCP_SLASHCOMMAND_DIR", "")
+ opts := mcpOptions{showVersion: true, slashCommandDir: "/tmp/test-cmds"}
+ var stdout bytes.Buffer
+ if err := run(opts, nil, &stdout, nil); err != nil {
+ t.Fatalf("run: %v", err)
+ }
+ if got := os.Getenv("HEXAI_MCP_SLASHCOMMAND_DIR"); got != "/tmp/test-cmds" {
+ t.Fatalf("expected HEXAI_MCP_SLASHCOMMAND_DIR=/tmp/test-cmds, got %q", got)
+ }
+}
+
+func TestRun_SyncAll(t *testing.T) {
+ old := runBackfill
+ t.Cleanup(func() { runBackfill = old })
+
+ var gotLog, gotConfig string
+ runBackfill = func(logPath, configPath string) error {
+ gotLog = logPath
+ gotConfig = configPath
+ return nil
+ }
+
+ opts := mcpOptions{syncAll: true, logPath: "/tmp/test.log", configPath: "/tmp/cfg.toml"}
+ if err := run(opts, nil, nil, nil); err != nil {
+ t.Fatalf("run syncAll: %v", err)
+ }
+ if gotLog != "/tmp/test.log" {
+ t.Fatalf("expected logPath=/tmp/test.log, got %q", gotLog)
+ }
+ if gotConfig != "/tmp/cfg.toml" {
+ t.Fatalf("expected configPath=/tmp/cfg.toml, got %q", gotConfig)
+ }
+}
+
+func TestRun_SyncAllError(t *testing.T) {
+ old := runBackfill
+ t.Cleanup(func() { runBackfill = old })
+
+ wantErr := errors.New("backfill failed")
+ runBackfill = func(_, _ string) error { return wantErr }
+
+ opts := mcpOptions{syncAll: true}
+ if err := run(opts, nil, nil, nil); !errors.Is(err, wantErr) {
+ t.Fatalf("expected backfill error, got: %v", err)
+ }
+}
+
+func TestRun_MCPServer(t *testing.T) {
+ old := runMCP
+ t.Cleanup(func() { runMCP = old })
+
+ called := false
+ runMCP = func(logPath, configPath string, stdin io.Reader, stdout, stderr io.Writer) error {
+ called = true
+ return nil
+ }
+
+ opts := mcpOptions{logPath: "/tmp/mcp.log"}
+ if err := run(opts, nil, nil, nil); err != nil {
+ t.Fatalf("run MCP: %v", err)
+ }
+ if !called {
+ t.Fatal("expected runMCP to be called")
+ }
+}
+
+func TestRun_MCPServerError(t *testing.T) {
+ old := runMCP
+ t.Cleanup(func() { runMCP = old })
+
+ wantErr := errors.New("server failed")
+ runMCP = func(_, _ string, _ io.Reader, _, _ io.Writer) error { return wantErr }
+
+ if err := run(mcpOptions{}, nil, nil, nil); !errors.Is(err, wantErr) {
+ t.Fatalf("expected server error, got: %v", err)
+ }
+}