Compare commits
No commits in common. "master" and "v0.4.0" have entirely different histories.
55
CHANGELOG.md
55
CHANGELOG.md
@ -1,58 +1,3 @@
|
|||||||
## [v0.5.0] - 2026-04-06
|
|
||||||
|
|
||||||
MCP server mode arrives, ready to serve your AI coding whims.
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Implement `grokkit mcp` command for MCP server over stdio.
|
|
||||||
- Add ListRecipes and ListAvailablePrompts functions.
|
|
||||||
- Export analysis, docs, and testgen functions for MCP tools.
|
|
||||||
- Add tools: lint_code, analyze_code, generate_docs, generate_tests, generate_commit_msg, run_recipe.
|
|
||||||
- Register resources: recipes://local, recipes://global, prompts://language.
|
|
||||||
- Add MCP config options and dependencies.
|
|
||||||
- Add unit tests for analyze, docs, mcp, prompts, recipe, and testgen packages.
|
|
||||||
- Add user guide for MCP.
|
|
||||||
- Add detailed plan in todo/queued/mcp-feature.md.
|
|
||||||
- Add MCP server mode to queued tasks in TODO README.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Update MCP server handlers for local/global recipes and prompts.
|
|
||||||
- Update README with MCP details.
|
|
||||||
## [v0.4.0] - 2026-03-31
|
|
||||||
|
|
||||||
Workon command arrives: because who needs manual todos when AI can branch out for you?
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Add workon command for automating todo/fix workflows.
|
|
||||||
- Add AI-powered work plan generation using Grok.
|
|
||||||
- Add git branching for features and fixes.
|
|
||||||
- Add todo package with bootstrap for directory structure.
|
|
||||||
- Add flags for fix mode (-f), complete mode (-c), and custom messages (-M).
|
|
||||||
- Add config options for workon.model and workon.ide.
|
|
||||||
- Add IDE integration to open on task start.
|
|
||||||
- Add unit tests for commands, config, git, linter, logger, prompts, todo, and workon.
|
|
||||||
- Add prompts loading tests with fallback logic.
|
|
||||||
- Add todo/queued/workon.md documentation.
|
|
||||||
- Add workon guide to README and user docs.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Update workon command to handle modes: todo, fix, complete.
|
|
||||||
- Change custom message flag from -m to -M.
|
|
||||||
- Implement branch prefixing with feature/ or fix/.
|
|
||||||
- Update README index on task completion.
|
|
||||||
- Refine error handling, logging, and colorized output.
|
|
||||||
- Replace placeholder with actual Grok client calls.
|
|
||||||
- Remove temporary stubs and add TODOs for integrations.
|
|
||||||
- Reorder queued tasks and update workon descriptions in docs.
|
|
||||||
- Update workon.md to use 'todo item' instead of 'feature'.
|
|
||||||
- Clarify -c flag usage and exclusivity in docs.
|
|
||||||
- Refine workon command steps, usage, and benefits in docs.
|
|
||||||
- Move workon todo item from queued to doing, then completed.
|
|
||||||
- Mark recipe feature as completed in todo structure.
|
|
||||||
- Bump version to 0.3.9 in README and scripts (prior release).
|
|
||||||
- Update expected git diff ranges in tests.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Discard errors explicitly from git commands in tests to satisfy linters.
|
|
||||||
## [v0.3.9] - 2026-03-31
|
## [v0.3.9] - 2026-03-31
|
||||||
|
|
||||||
Another tiny step forward, now with origin-al git flair.
|
Another tiny step forward, now with origin-al git flair.
|
||||||
|
|||||||
10
README.md
10
README.md
@ -19,13 +19,13 @@ Grokkit is a fast Go CLI integrating Grok AI with git workflows and general chat
|
|||||||
|
|
||||||
**Bash (Linux/macOS):**
|
**Bash (Linux/macOS):**
|
||||||
```bash
|
```bash
|
||||||
export VERSION=0.4.0
|
export VERSION=0.3.9
|
||||||
curl -fsSL https://repos.gmgauthier.com/gmgauthier/grokkit/releases/download/v${VERSION}/grokkit-install.sh | bash -s -- --verify
|
curl -fsSL https://repos.gmgauthier.com/gmgauthier/grokkit/releases/download/v${VERSION}/grokkit-install.sh | bash -s -- --verify
|
||||||
```
|
```
|
||||||
|
|
||||||
**PowerShell (Windows):**
|
**PowerShell (Windows):**
|
||||||
```pwsh
|
```pwsh
|
||||||
$env:VERSION="0.4.0"
|
$env:VERSION="0.3.9"
|
||||||
irm https://repos.gmgauthier.com/gmgauthier/grokkit/releases/download/v$env:VERSION/grokkit-install.ps1 | iex
|
irm https://repos.gmgauthier.com/gmgauthier/grokkit/releases/download/v$env:VERSION/grokkit-install.ps1 | iex
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ Both installers feature:
|
|||||||
**Bash (Linux/macOS):**
|
**Bash (Linux/macOS):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export VERSION=0.4.0
|
export VERSION=0.3.9
|
||||||
curl -fsSL https://repos.gmgauthier.com/gmgauthier/grokkit/releases/download/v${VERSION}/grokkit-install.sh -o grokkit-install.sh
|
curl -fsSL https://repos.gmgauthier.com/gmgauthier/grokkit/releases/download/v${VERSION}/grokkit-install.sh -o grokkit-install.sh
|
||||||
bash grokkit-install.sh --verify
|
bash grokkit-install.sh --verify
|
||||||
```
|
```
|
||||||
@ -48,7 +48,7 @@ bash grokkit-install.sh --verify
|
|||||||
**PowerShell (Windows):**
|
**PowerShell (Windows):**
|
||||||
|
|
||||||
```pwsh
|
```pwsh
|
||||||
$env:VERSION="0.4.0"
|
$env:VERSION="0.3.9"
|
||||||
irm https://repos.gmgauthier.com/gmgauthier/grokkit/releases/download/v$env:VERSION/grokkit-install.ps1 -OutFile grokkit-install.ps1
|
irm https://repos.gmgauthier.com/gmgauthier/grokkit/releases/download/v$env:VERSION/grokkit-install.ps1 -OutFile grokkit-install.ps1
|
||||||
.\grokkit-install.ps1 -Verify
|
.\grokkit-install.ps1 -Verify
|
||||||
```
|
```
|
||||||
@ -109,8 +109,6 @@ history_file = "~/.config/grokkit/chat_history.json"
|
|||||||
|
|
||||||
See [The User Guide](docs/user-guide/index.md) for complete details on command usage.
|
See [The User Guide](docs/user-guide/index.md) for complete details on command usage.
|
||||||
|
|
||||||
**New:** Grokkit can now run as an **[MCP Server](docs/user-guide/mcp.md)** (`grokkit mcp`), allowing AI coding agents like Claude Code to call its tools directly.
|
|
||||||
|
|
||||||
## 🛡️ Safety & Change Management
|
## 🛡️ Safety & Change Management
|
||||||
|
|
||||||
Grokkit is designed to work seamlessly with Git, using version control instead of redundant backup files to manage changes and rollbacks.
|
Grokkit is designed to work seamlessly with Git, using version control instead of redundant backup files to manage changes and rollbacks.
|
||||||
|
|||||||
@ -39,7 +39,7 @@ Uses language-specific prompts discovered in .grokkit/prompts/ or ~/.config/grok
|
|||||||
logger.Warn("Not inside a git repository. Git metadata in report will be limited.")
|
logger.Warn("Not inside a git repository. Git metadata in report will be limited.")
|
||||||
}
|
}
|
||||||
|
|
||||||
files, err := DiscoverSourceFiles(dir)
|
files, err := discoverSourceFiles(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to discover source files", "dir", dir, "error", err)
|
logger.Error("Failed to discover source files", "dir", dir, "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -71,7 +71,7 @@ Uses language-specific prompts discovered in .grokkit/prompts/ or ~/.config/grok
|
|||||||
}
|
}
|
||||||
promptContent = strings.Replace(promptContent, "[Inferred Project Name]", projectName, 1)
|
promptContent = strings.Replace(promptContent, "[Inferred Project Name]", projectName, 1)
|
||||||
|
|
||||||
context := BuildProjectContext(dir, files)
|
context := buildProjectContext(dir, files)
|
||||||
|
|
||||||
messages := []map[string]string{
|
messages := []map[string]string{
|
||||||
{"role": "system", "content": promptContent},
|
{"role": "system", "content": promptContent},
|
||||||
@ -118,12 +118,9 @@ func init() {
|
|||||||
analyzeCmd.Flags().String("dir", ".", "Repository root to analyze")
|
analyzeCmd.Flags().String("dir", ".", "Repository root to analyze")
|
||||||
analyzeCmd.Flags().StringP("output", "o", "analyze.md", "Output file (use - for stdout)")
|
analyzeCmd.Flags().StringP("output", "o", "analyze.md", "Output file (use - for stdout)")
|
||||||
analyzeCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
|
analyzeCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
|
||||||
|
|
||||||
// Export analysis functions for MCP use
|
|
||||||
// mcp.RegisterAnalyzeHelpers(DiscoverSourceFiles, BuildProjectContext)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DiscoverSourceFiles(root string) ([]string, error) {
|
func discoverSourceFiles(root string) ([]string, error) {
|
||||||
var files []string
|
var files []string
|
||||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -157,7 +154,7 @@ func previewLines(content string, n int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildProjectContext(dir string, files []string) string {
|
func buildProjectContext(dir string, files []string) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("Project Root: " + dir + "\n\n")
|
sb.WriteString("Project Root: " + dir + "\n\n")
|
||||||
_, err := fmt.Fprintf(&sb, "Total source files discovered: %d\n\n", len(files))
|
_, err := fmt.Fprintf(&sb, "Total source files discovered: %d\n\n", len(files))
|
||||||
|
|||||||
@ -62,9 +62,9 @@ func TestDiscoverSourceFiles(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
files, err := DiscoverSourceFiles(tmpDir)
|
files, err := discoverSourceFiles(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("DiscoverSourceFiles failed: %v", err)
|
t.Fatalf("discoverSourceFiles failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(files) < 2 {
|
if len(files) < 2 {
|
||||||
@ -105,9 +105,9 @@ func TestDiscoverSourceFilesSkipsDotDirs(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
files, err := DiscoverSourceFiles(tmpDir)
|
files, err := discoverSourceFiles(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("DiscoverSourceFiles failed: %v", err)
|
t.Fatalf("discoverSourceFiles failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
@ -141,9 +141,9 @@ func TestDiscoverSourceFilesSkipsBuildAndDist(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
files, err := DiscoverSourceFiles(tmpDir)
|
files, err := discoverSourceFiles(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("DiscoverSourceFiles failed: %v", err)
|
t.Fatalf("discoverSourceFiles failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(files) != 1 {
|
if len(files) != 1 {
|
||||||
@ -154,9 +154,9 @@ func TestDiscoverSourceFilesSkipsBuildAndDist(t *testing.T) {
|
|||||||
func TestDiscoverSourceFilesEmptyDir(t *testing.T) {
|
func TestDiscoverSourceFilesEmptyDir(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
files, err := DiscoverSourceFiles(tmpDir)
|
files, err := discoverSourceFiles(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("DiscoverSourceFiles failed: %v", err)
|
t.Fatalf("discoverSourceFiles failed: %v", err)
|
||||||
}
|
}
|
||||||
if len(files) != 0 {
|
if len(files) != 0 {
|
||||||
t.Errorf("expected 0 files for empty dir, got %d", len(files))
|
t.Errorf("expected 0 files for empty dir, got %d", len(files))
|
||||||
@ -171,7 +171,7 @@ func TestBuildProjectContext(t *testing.T) {
|
|||||||
filepath.Join(tmpDir, "cmd", "root.go"),
|
filepath.Join(tmpDir, "cmd", "root.go"),
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := BuildProjectContext(tmpDir, files)
|
ctx := buildProjectContext(tmpDir, files)
|
||||||
|
|
||||||
if !strings.Contains(ctx, "Project Root:") {
|
if !strings.Contains(ctx, "Project Root:") {
|
||||||
t.Error("expected context to contain 'Project Root:'")
|
t.Error("expected context to contain 'Project Root:'")
|
||||||
@ -229,7 +229,7 @@ func TestBuildProjectContextLimitsSize(t *testing.T) {
|
|||||||
files = append(files, filepath.Join(tmpDir, "file_with_a_long_name_for_testing_"+strings.Repeat("x", 20)+".go"))
|
files = append(files, filepath.Join(tmpDir, "file_with_a_long_name_for_testing_"+strings.Repeat("x", 20)+".go"))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := BuildProjectContext(tmpDir, files)
|
ctx := buildProjectContext(tmpDir, files)
|
||||||
|
|
||||||
// Should be reasonably bounded (the 2500 byte check truncates the file list)
|
// Should be reasonably bounded (the 2500 byte check truncates the file list)
|
||||||
if len(ctx) > 4000 {
|
if len(ctx) > 4000 {
|
||||||
|
|||||||
@ -48,11 +48,11 @@ func runDocs(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
client := newGrokClient()
|
client := newGrokClient()
|
||||||
for _, filePath := range args {
|
for _, filePath := range args {
|
||||||
ProcessDocsFile(client, model, filePath)
|
processDocsFile(client, model, filePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProcessDocsFile(client grok.AIClient, model, filePath string) {
|
func processDocsFile(client grok.AIClient, model, filePath string) {
|
||||||
logger.Info("starting docs operation", "file", filePath)
|
logger.Info("starting docs operation", "file", filePath)
|
||||||
|
|
||||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
@ -79,7 +79,7 @@ func ProcessDocsFile(client grok.AIClient, model, filePath string) {
|
|||||||
color.Cyan("📝 Generating %s docs for: %s", lang.Name, filePath)
|
color.Cyan("📝 Generating %s docs for: %s", lang.Name, filePath)
|
||||||
logger.Info("requesting AI documentation", "file", filePath, "language", lang.Name)
|
logger.Info("requesting AI documentation", "file", filePath, "language", lang.Name)
|
||||||
|
|
||||||
messages := BuildDocsMessages(lang.Name, string(originalContent))
|
messages := buildDocsMessages(lang.Name, string(originalContent))
|
||||||
response := client.StreamSilent(messages, model)
|
response := client.StreamSilent(messages, model)
|
||||||
|
|
||||||
if response == "" {
|
if response == "" {
|
||||||
@ -134,7 +134,7 @@ func ProcessDocsFile(client grok.AIClient, model, filePath string) {
|
|||||||
color.Green("✅ Documentation applied: %s", filePath)
|
color.Green("✅ Documentation applied: %s", filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildDocsMessages(language, code string) []map[string]string {
|
func buildDocsMessages(language, code string) []map[string]string {
|
||||||
style := docStyle(language)
|
style := docStyle(language)
|
||||||
systemPrompt := fmt.Sprintf(
|
systemPrompt := fmt.Sprintf(
|
||||||
"You are a documentation expert. Add %s documentation comments to the provided code. "+
|
"You are a documentation expert. Add %s documentation comments to the provided code. "+
|
||||||
|
|||||||
@ -25,7 +25,7 @@ func TestBuildDocsMessages(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.language, func(t *testing.T) {
|
t.Run(tt.language, func(t *testing.T) {
|
||||||
msgs := BuildDocsMessages(tt.language, tt.code)
|
msgs := buildDocsMessages(tt.language, tt.code)
|
||||||
|
|
||||||
if len(msgs) != 2 {
|
if len(msgs) != 2 {
|
||||||
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
||||||
|
|||||||
50
cmd/mcp.go
50
cmd/mcp.go
@ -1,50 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"gmgauthier.com/grokkit/internal/logger"
|
|
||||||
"gmgauthier.com/grokkit/internal/mcp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var mcpCmd = &cobra.Command{
|
|
||||||
Use: "mcp",
|
|
||||||
Short: "Start Grokkit as an MCP (Model Context Protocol) server",
|
|
||||||
Long: `Starts Grokkit in MCP server mode over stdio.
|
|
||||||
|
|
||||||
This allows MCP-compatible clients (such as Claude Code, Cursor, or other AI coding agents)
|
|
||||||
to call Grokkit tools like lint_code, analyze_code, generate_docs, etc. directly.
|
|
||||||
|
|
||||||
The server runs until the client closes the connection.`,
|
|
||||||
Run: runMCP,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(mcpCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runMCP(cmd *cobra.Command, args []string) {
|
|
||||||
logger.Info("mcp command started")
|
|
||||||
|
|
||||||
// Check if MCP is enabled in config
|
|
||||||
if !viper.GetBool("mcp.enabled") {
|
|
||||||
fmt.Println("MCP is disabled in configuration.")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv, err := mcp.NewServer()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Failed to create MCP server: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
if err := srv.Run(ctx); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "MCP server error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -396,7 +396,7 @@ func TestRunLintFileNotFound(t *testing.T) {
|
|||||||
|
|
||||||
func TestProcessDocsFileNotFound(t *testing.T) {
|
func TestProcessDocsFileNotFound(t *testing.T) {
|
||||||
mock := &mockStreamer{}
|
mock := &mockStreamer{}
|
||||||
ProcessDocsFile(mock, "grok-4", "/nonexistent/path/file.go")
|
processDocsFile(mock, "grok-4", "/nonexistent/path/file.go")
|
||||||
|
|
||||||
if mock.calls != 0 {
|
if mock.calls != 0 {
|
||||||
t.Errorf("expected 0 AI calls for missing file, got %d", mock.calls)
|
t.Errorf("expected 0 AI calls for missing file, got %d", mock.calls)
|
||||||
@ -415,7 +415,7 @@ func TestProcessDocsFileUnsupportedLanguage(t *testing.T) {
|
|||||||
defer func() { _ = os.Remove(f.Name()) }()
|
defer func() { _ = os.Remove(f.Name()) }()
|
||||||
|
|
||||||
mock := &mockStreamer{}
|
mock := &mockStreamer{}
|
||||||
ProcessDocsFile(mock, "grok-4", f.Name())
|
processDocsFile(mock, "grok-4", f.Name())
|
||||||
|
|
||||||
if mock.calls != 0 {
|
if mock.calls != 0 {
|
||||||
t.Errorf("expected 0 AI calls for unsupported language, got %d", mock.calls)
|
t.Errorf("expected 0 AI calls for unsupported language, got %d", mock.calls)
|
||||||
@ -453,7 +453,7 @@ func TestProcessDocsFilePreviewAndCancel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { os.Stdin = origStdin }()
|
defer func() { os.Stdin = origStdin }()
|
||||||
|
|
||||||
ProcessDocsFile(mock, "grok-4", f.Name())
|
processDocsFile(mock, "grok-4", f.Name())
|
||||||
|
|
||||||
if mock.calls != 1 {
|
if mock.calls != 1 {
|
||||||
t.Errorf("expected 1 AI call, got %d", mock.calls)
|
t.Errorf("expected 1 AI call, got %d", mock.calls)
|
||||||
@ -483,7 +483,7 @@ func TestProcessDocsFileAutoApply(t *testing.T) {
|
|||||||
autoApply = true
|
autoApply = true
|
||||||
defer func() { autoApply = origAutoApply }()
|
defer func() { autoApply = origAutoApply }()
|
||||||
|
|
||||||
ProcessDocsFile(mock, "grok-4", f.Name())
|
processDocsFile(mock, "grok-4", f.Name())
|
||||||
|
|
||||||
if mock.calls != 1 {
|
if mock.calls != 1 {
|
||||||
t.Errorf("expected 1 AI call, got %d", mock.calls)
|
t.Errorf("expected 1 AI call, got %d", mock.calls)
|
||||||
|
|||||||
@ -41,11 +41,6 @@ func Load() {
|
|||||||
viper.SetDefault("commands.workon.model", "grok-4-1-fast-non-reasoning")
|
viper.SetDefault("commands.workon.model", "grok-4-1-fast-non-reasoning")
|
||||||
viper.SetDefault("commands.workon.ide", "")
|
viper.SetDefault("commands.workon.ide", "")
|
||||||
|
|
||||||
// MCP configuration
|
|
||||||
viper.SetDefault("mcp.enabled", true)
|
|
||||||
viper.SetDefault("mcp.tools", []string{"lint_code", "analyze_code", "generate_docs", "generate_tests", "generate_commit_msg", "run_recipe"})
|
|
||||||
viper.SetDefault("mcp.resources", []string{"recipes://local", "recipes://global", "prompts://language"})
|
|
||||||
|
|
||||||
// Config file is optional, so we ignore read errors
|
// Config file is optional, so we ignore read errors
|
||||||
_ = viper.ReadInConfig()
|
_ = viper.ReadInConfig()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,61 +0,0 @@
|
|||||||
# MCP Server Mode (`grokkit mcp`)
|
|
||||||
|
|
||||||
Grokkit can run as an **MCP (Model Context Protocol)** server, allowing AI coding agents like **Claude Code**, Cursor, or other MCP-compatible clients to call Grokkit tools directly during their workflows.
|
|
||||||
|
|
||||||
## What is MCP?
|
|
||||||
|
|
||||||
MCP is a protocol that lets LLMs call external tools and access resources in a standardized way. By running `grokkit mcp`, you turn Grokkit into a service that other AI tools can use.
|
|
||||||
|
|
||||||
## Starting the MCP Server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
grokkit mcp
|
|
||||||
```
|
|
||||||
|
|
||||||
This starts the server over **stdio** (standard input/output), which is the default transport used by Claude Code and most MCP clients.
|
|
||||||
|
|
||||||
The server will run until the client disconnects.
|
|
||||||
|
|
||||||
## Available Tools
|
|
||||||
|
|
||||||
When running in MCP mode, the following tools are available:
|
|
||||||
|
|
||||||
- **`lint_code`** — Run linter on a file and return results
|
|
||||||
- **`analyze_code`** — Deep project analysis with educational report
|
|
||||||
- **`generate_docs`** — Generate documentation comments for a source file
|
|
||||||
- **`generate_tests`** — Generate unit tests for supported languages
|
|
||||||
- **`generate_commit_msg`** — Generate conventional commit message from staged changes
|
|
||||||
- **`run_recipe`** — Execute a named recipe from `.grokkit/recipes/`
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
You can control MCP behavior in your config file (`~/.config/grokkit/config.toml` or `./config.toml`):
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[mcp]
|
|
||||||
enabled = true
|
|
||||||
tools = ["lint_code", "analyze_code", "generate_docs"]
|
|
||||||
resources = ["recipes://local", "prompts://language"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example Usage with Claude Code
|
|
||||||
|
|
||||||
Once `grokkit mcp` is running, you can tell Claude Code:
|
|
||||||
|
|
||||||
> "Use the grokkit MCP server to lint this file and then generate tests for it."
|
|
||||||
|
|
||||||
Claude can then call the tools directly.
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
MCP also exposes resources:
|
|
||||||
|
|
||||||
- `recipes://local` — List of local recipes
|
|
||||||
- `recipes://global` — List of global recipes
|
|
||||||
- `prompts://language` — Available language prompts
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**See also:**
|
|
||||||
- [Developer Guide](../developer-guide/mcp.md) for technical details
|
|
||||||
- [Main README](../../README.md) for installation
|
|
||||||
6
go.mod
6
go.mod
@ -7,17 +7,13 @@ require (
|
|||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/google/jsonschema-go v0.4.2 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/mark3labs/mcp-go v0.47.0 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
@ -28,8 +24,8 @@ require (
|
|||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
8
go.sum
8
go.sum
@ -11,18 +11,12 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
|
|||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
|
|
||||||
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mark3labs/mcp-go v0.47.0 h1:h44yeM3DduDyQgzImYWu4pt6VRkqP/0p/95AGhWngnA=
|
|
||||||
github.com/mark3labs/mcp-go v0.47.0/go.mod h1:JKTC7R2LLVagkEWK7Kwu7DbmA6iIvnNAod6yrHiQMag=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
@ -54,8 +48,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
|
||||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
package analyze
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gmgauthier.com/grokkit/internal/git"
|
|
||||||
"gmgauthier.com/grokkit/internal/linter"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DiscoverSourceFiles finds all supported source files in a directory tree.
|
|
||||||
// Exported for use by MCP server and other packages.
|
|
||||||
func DiscoverSourceFiles(root string) ([]string, error) {
|
|
||||||
var files []string
|
|
||||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
name := info.Name()
|
|
||||||
if info.IsDir() {
|
|
||||||
if strings.HasPrefix(name, ".") && name != "." && name != ".." ||
|
|
||||||
name == "node_modules" || name == "vendor" || name == "build" || name == "dist" {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, detectErr := linter.DetectLanguage(path); detectErr == nil {
|
|
||||||
files = append(files, path)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return files, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildProjectContext builds a context string for analysis prompts.
|
|
||||||
// Exported for use by MCP server.
|
|
||||||
func BuildProjectContext(dir string, files []string) string {
|
|
||||||
var sb strings.Builder
|
|
||||||
sb.WriteString("Project Root: " + dir + "\n\n")
|
|
||||||
_, _ = fmt.Fprintf(&sb, "Total source files discovered: %d\n\n", len(files))
|
|
||||||
|
|
||||||
sb.WriteString("Key files (top-level view):\n")
|
|
||||||
for _, f := range files {
|
|
||||||
rel, _ := filepath.Rel(dir, f)
|
|
||||||
if strings.Count(rel, string(filepath.Separator)) <= 2 {
|
|
||||||
sb.WriteString(" - " + rel + "\n")
|
|
||||||
}
|
|
||||||
if sb.Len() > 2500 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add git remotes if available
|
|
||||||
if out, err := git.Run([]string{"remote", "-v"}); err == nil && out != "" {
|
|
||||||
sb.WriteString("\nGit Remotes:\n" + out + "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
package analyze
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDiscoverSourceFiles(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
|
|
||||||
// Create some source files
|
|
||||||
if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte("package main"), 0644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(tmpDir, "helper.go"), []byte("package main"), 0644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// Create a non-source file (should be ignored)
|
|
||||||
if err := os.WriteFile(filepath.Join(tmpDir, "notes.txt"), []byte("notes"), 0644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := DiscoverSourceFiles(tmpDir)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.GreaterOrEqual(t, len(files), 2)
|
|
||||||
|
|
||||||
// Verify .txt was not included
|
|
||||||
for _, f := range files {
|
|
||||||
assert.False(t, strings.HasSuffix(f, ".txt"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildProjectContext(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
|
|
||||||
files := []string{
|
|
||||||
filepath.Join(tmpDir, "main.go"),
|
|
||||||
filepath.Join(tmpDir, "cmd", "root.go"),
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := BuildProjectContext(tmpDir, files)
|
|
||||||
|
|
||||||
assert.Contains(t, ctx, "Project Root:")
|
|
||||||
assert.Contains(t, ctx, "Total source files discovered: 2")
|
|
||||||
assert.Contains(t, ctx, "Key files")
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
package docs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BuildDocsMessages builds the messages for documentation generation.
|
|
||||||
// Exported for use by MCP server.
|
|
||||||
func BuildDocsMessages(language, code string) []map[string]string {
|
|
||||||
style := docStyle(language)
|
|
||||||
systemPrompt := fmt.Sprintf(
|
|
||||||
"You are a documentation expert. Add %s documentation comments to the provided code. "+
|
|
||||||
"Return ONLY the documented code with no explanations, markdown, or extra text. "+
|
|
||||||
"Do NOT include markdown code fences. Document all public functions, methods, types, and constants.",
|
|
||||||
style,
|
|
||||||
)
|
|
||||||
|
|
||||||
userPrompt := fmt.Sprintf("Add documentation comments to the following %s code:\n\n%s", language, code)
|
|
||||||
|
|
||||||
return []map[string]string{
|
|
||||||
{"role": "system", "content": systemPrompt},
|
|
||||||
{"role": "user", "content": userPrompt},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func docStyle(language string) string {
|
|
||||||
switch strings.ToLower(language) {
|
|
||||||
case "go":
|
|
||||||
return "godoc"
|
|
||||||
case "python":
|
|
||||||
return "PEP 257 docstring"
|
|
||||||
case "c", "c++":
|
|
||||||
return "doxygen style"
|
|
||||||
case "javascript", "typescript":
|
|
||||||
return "JSDoc"
|
|
||||||
case "rust":
|
|
||||||
return "rustdoc"
|
|
||||||
case "ruby":
|
|
||||||
return "YARD"
|
|
||||||
case "java":
|
|
||||||
return "javadoc"
|
|
||||||
case "shell":
|
|
||||||
return "shell style comments"
|
|
||||||
default:
|
|
||||||
return "standard"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
package docs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBuildDocsMessages(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
language string
|
|
||||||
code string
|
|
||||||
styleCheck string
|
|
||||||
}{
|
|
||||||
{language: "Go", code: "func Foo() {}", styleCheck: "godoc"},
|
|
||||||
{language: "Python", code: "def foo(): pass", styleCheck: "PEP 257"},
|
|
||||||
{language: "JavaScript", code: "function foo() {}", styleCheck: "JSDoc"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.language, func(t *testing.T) {
|
|
||||||
msgs := BuildDocsMessages(tt.language, tt.code)
|
|
||||||
|
|
||||||
assert.Len(t, msgs, 2)
|
|
||||||
assert.Equal(t, "system", msgs[0]["role"])
|
|
||||||
assert.Equal(t, "user", msgs[1]["role"])
|
|
||||||
assert.Contains(t, msgs[0]["content"], tt.styleCheck)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,423 +0,0 @@
|
|||||||
package mcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"github.com/mark3labs/mcp-go/server"
|
|
||||||
|
|
||||||
"gmgauthier.com/grokkit/internal/analyze"
|
|
||||||
"gmgauthier.com/grokkit/internal/docs"
|
|
||||||
"gmgauthier.com/grokkit/internal/git"
|
|
||||||
"gmgauthier.com/grokkit/internal/grok"
|
|
||||||
"gmgauthier.com/grokkit/internal/linter"
|
|
||||||
"gmgauthier.com/grokkit/internal/logger"
|
|
||||||
"gmgauthier.com/grokkit/internal/prompts"
|
|
||||||
"gmgauthier.com/grokkit/internal/recipe"
|
|
||||||
"gmgauthier.com/grokkit/internal/testgen"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Server wraps an MCP server and provides Grokkit-specific tools.
|
|
||||||
type Server struct {
|
|
||||||
mcpServer *server.MCPServer
|
|
||||||
grokClient *grok.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewServer creates a new MCP server with Grokkit tools.
|
|
||||||
func NewServer() (*Server, error) {
|
|
||||||
// Create the underlying MCP server
|
|
||||||
s := server.NewMCPServer(
|
|
||||||
"grokkit",
|
|
||||||
"0.1.0",
|
|
||||||
server.WithToolCapabilities(false),
|
|
||||||
server.WithRecovery(),
|
|
||||||
)
|
|
||||||
|
|
||||||
grokClient := grok.NewClient()
|
|
||||||
|
|
||||||
mcpSrv := &Server{
|
|
||||||
mcpServer: s,
|
|
||||||
grokClient: grokClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mcpSrv.registerTools(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to register tools: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mcpSrv.registerResources()
|
|
||||||
|
|
||||||
return mcpSrv, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// registerTools registers all available Grokkit tools.
|
|
||||||
func (s *Server) registerTools() error {
|
|
||||||
// lint_code tool
|
|
||||||
lintTool := mcp.NewTool("lint_code",
|
|
||||||
mcp.WithDescription("Run a linter on a source file and return results"),
|
|
||||||
mcp.WithString("file_path", mcp.Required(), mcp.Description("Path to the file to lint")),
|
|
||||||
)
|
|
||||||
s.mcpServer.AddTool(lintTool, s.handleLintCode)
|
|
||||||
|
|
||||||
// analyze_code tool
|
|
||||||
analyzeTool := mcp.NewTool("analyze_code",
|
|
||||||
mcp.WithDescription("Perform deep project analysis and return an educational Markdown report"),
|
|
||||||
mcp.WithString("dir", mcp.Description("Directory to analyze (default: current directory)")),
|
|
||||||
)
|
|
||||||
s.mcpServer.AddTool(analyzeTool, s.handleAnalyzeCode)
|
|
||||||
|
|
||||||
// generate_docs tool
|
|
||||||
docsTool := mcp.NewTool("generate_docs",
|
|
||||||
mcp.WithDescription("Generate documentation comments for a source file"),
|
|
||||||
mcp.WithString("file_path", mcp.Required(), mcp.Description("Path to the file to document")),
|
|
||||||
mcp.WithString("style", mcp.Description("Documentation style (optional)")),
|
|
||||||
)
|
|
||||||
s.mcpServer.AddTool(docsTool, s.handleGenerateDocs)
|
|
||||||
|
|
||||||
// generate_tests tool
|
|
||||||
testsTool := mcp.NewTool("generate_tests",
|
|
||||||
mcp.WithDescription("Generate unit tests for a source file"),
|
|
||||||
mcp.WithString("file_path", mcp.Required(), mcp.Description("Path to the file to generate tests for")),
|
|
||||||
)
|
|
||||||
s.mcpServer.AddTool(testsTool, s.handleGenerateTests)
|
|
||||||
|
|
||||||
// generate_commit_msg tool
|
|
||||||
commitTool := mcp.NewTool("generate_commit_msg",
|
|
||||||
mcp.WithDescription("Generate a conventional commit message from staged changes"),
|
|
||||||
)
|
|
||||||
s.mcpServer.AddTool(commitTool, s.handleGenerateCommitMsg)
|
|
||||||
|
|
||||||
// run_recipe tool
|
|
||||||
recipeTool := mcp.NewTool("run_recipe",
|
|
||||||
mcp.WithDescription("Execute a named recipe from .grokkit/recipes"),
|
|
||||||
mcp.WithString("name", mcp.Required(), mcp.Description("Name of the recipe to run")),
|
|
||||||
mcp.WithObject("params", mcp.Description("Parameters for the recipe (optional)")),
|
|
||||||
)
|
|
||||||
s.mcpServer.AddTool(recipeTool, s.handleRunRecipe)
|
|
||||||
|
|
||||||
logger.Info("MCP tools registered", "count", 6)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleLintCode implements the lint_code MCP tool.
|
|
||||||
func (s *Server) handleLintCode(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
filePath, err := req.RequireString("file_path")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultText("Error: file_path is required"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("MCP lint_code called", "file", filePath)
|
|
||||||
|
|
||||||
result, err := linter.LintFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultText(fmt.Sprintf("Linting failed: %v", err)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
output := fmt.Sprintf("✅ Lint completed for %s\n", filePath)
|
|
||||||
output += fmt.Sprintf("Language: %s\n", result.Language)
|
|
||||||
output += fmt.Sprintf("Linter: %s\n", result.LinterUsed)
|
|
||||||
output += fmt.Sprintf("Has issues: %v\n", result.HasIssues)
|
|
||||||
|
|
||||||
if result.Output != "" {
|
|
||||||
output += "\n--- Linter Output ---\n" + result.Output
|
|
||||||
}
|
|
||||||
|
|
||||||
return mcp.NewToolResultText(output), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleAnalyzeCode implements the analyze_code MCP tool.
|
|
||||||
func (s *Server) handleAnalyzeCode(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
dir := "."
|
|
||||||
if d := req.GetString("dir", ""); d != "" {
|
|
||||||
dir = d
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("MCP analyze_code called", "dir", dir)
|
|
||||||
|
|
||||||
// Fully functional implementation using logic from cmd/analyze.go
|
|
||||||
files, err := analyze.DiscoverSourceFiles(dir)
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultText(fmt.Sprintf("Failed to discover files: %v", err)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(files) == 0 {
|
|
||||||
return mcp.NewToolResultText("No supported source files found in directory."), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lang := linter.DetectPrimaryLanguage(files)
|
|
||||||
if lang == "" {
|
|
||||||
lang = "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
_, promptContent, err := prompts.LoadAnalysisPrompt(dir, lang)
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultText(fmt.Sprintf("Failed to load analysis prompt: %v", err)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
context := analyze.BuildProjectContext(dir, files)
|
|
||||||
|
|
||||||
messages := []map[string]string{
|
|
||||||
{"role": "system", "content": promptContent},
|
|
||||||
{"role": "user", "content": fmt.Sprintf("Analyze this %s project and generate the full educational Markdown report now:\n\n%s", lang, context)},
|
|
||||||
}
|
|
||||||
|
|
||||||
report := s.grokClient.StreamSilent(messages, "grok-4")
|
|
||||||
|
|
||||||
result := fmt.Sprintf("✅ Analysis complete for %s\n", dir)
|
|
||||||
result += fmt.Sprintf("Files analyzed: %d\n", len(files))
|
|
||||||
result += fmt.Sprintf("Primary language: %s\n\n", lang)
|
|
||||||
result += report
|
|
||||||
|
|
||||||
return mcp.NewToolResultText(result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleGenerateDocs implements the generate_docs MCP tool.
|
|
||||||
func (s *Server) handleGenerateDocs(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
filePath, err := req.RequireString("file_path")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultText("Error: file_path is required"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
style := req.GetString("style", "")
|
|
||||||
|
|
||||||
logger.Info("MCP generate_docs called", "file", filePath, "style", style)
|
|
||||||
|
|
||||||
// Fully functional implementation using logic from cmd/docs.go
|
|
||||||
lang, err := linter.DetectLanguage(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultText(fmt.Sprintf("Unsupported language: %v", err)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := os.ReadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultText(fmt.Sprintf("Failed to read file: %v", err)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
messages := docs.BuildDocsMessages(lang.Name, string(content))
|
|
||||||
response := s.grokClient.StreamSilent(messages, "grok-4")
|
|
||||||
|
|
||||||
documented := grok.CleanCodeResponse(response)
|
|
||||||
|
|
||||||
result := fmt.Sprintf("✅ Documentation generated for %s\n", filePath)
|
|
||||||
result += fmt.Sprintf("Language: %s\n", lang.Name)
|
|
||||||
result += "\n--- Documented Code ---\n"
|
|
||||||
result += documented
|
|
||||||
|
|
||||||
return mcp.NewToolResultText(result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleGenerateTests implements the generate_tests MCP tool.
|
|
||||||
func (s *Server) handleGenerateTests(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
filePath, err := req.RequireString("file_path")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultText("Error: file_path is required"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("MCP generate_tests called", "file", filePath)
|
|
||||||
|
|
||||||
// Fully functional using logic from cmd/testgen.go
|
|
||||||
langObj, err := linter.DetectLanguage(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultText(fmt.Sprintf("Unsupported language: %v", err)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lang := langObj.Name
|
|
||||||
if lang == "C/C++" {
|
|
||||||
ext := strings.ToLower(filepath.Ext(filePath))
|
|
||||||
if ext == ".cpp" || ext == ".cc" || ext == ".cxx" {
|
|
||||||
lang = "C++"
|
|
||||||
} else {
|
|
||||||
lang = "C"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt := testgen.GetTestPrompt(lang)
|
|
||||||
if prompt == "" {
|
|
||||||
return mcp.NewToolResultText("No test prompt available for this language"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := os.ReadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultText(fmt.Sprintf("Failed to read file: %v", err)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
messages := []map[string]string{
|
|
||||||
{"role": "system", "content": prompt},
|
|
||||||
{"role": "user", "content": fmt.Sprintf("Generate comprehensive tests for the following %s code:\n\n%s", lang, string(content))},
|
|
||||||
}
|
|
||||||
|
|
||||||
response := s.grokClient.StreamSilent(messages, "grok-4")
|
|
||||||
cleaned := grok.CleanCodeResponse(response)
|
|
||||||
|
|
||||||
result := fmt.Sprintf("✅ Tests generated for %s\n", filePath)
|
|
||||||
result += fmt.Sprintf("Language: %s\n\n", lang)
|
|
||||||
result += "--- Generated Test Code ---\n"
|
|
||||||
result += cleaned
|
|
||||||
|
|
||||||
return mcp.NewToolResultText(result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleGenerateCommitMsg implements the generate_commit_msg MCP tool.
|
|
||||||
func (s *Server) handleGenerateCommitMsg(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
logger.Info("MCP generate_commit_msg called")
|
|
||||||
|
|
||||||
// Fully functional implementation using git diff + Grok
|
|
||||||
if !git.IsRepo() {
|
|
||||||
return mcp.NewToolResultText("Error: Not in a git repository"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
diff, err := git.Run([]string{"diff", "--cached"})
|
|
||||||
if err != nil || strings.TrimSpace(diff) == "" {
|
|
||||||
return mcp.NewToolResultText("No staged changes found. Stage some changes with `git add` first."), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt := `You are an expert at writing conventional commit messages.
|
|
||||||
|
|
||||||
Generate a single, concise conventional commit message for the following git diff.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
- Use format: <type>(<scope>): <description>
|
|
||||||
- Types: feat, fix, docs, style, refactor, perf, test, chore, ci, build
|
|
||||||
- Keep subject line under 72 characters
|
|
||||||
- Do not include body unless absolutely necessary
|
|
||||||
- Return ONLY the commit message, no explanations, no markdown, no quotes.
|
|
||||||
|
|
||||||
Diff:
|
|
||||||
` + diff
|
|
||||||
|
|
||||||
messages := []map[string]string{
|
|
||||||
{"role": "system", "content": prompt},
|
|
||||||
{"role": "user", "content": "Generate the commit message now."},
|
|
||||||
}
|
|
||||||
|
|
||||||
response := s.grokClient.StreamSilent(messages, "grok-4")
|
|
||||||
cleaned := strings.TrimSpace(grok.CleanCodeResponse(response))
|
|
||||||
|
|
||||||
result := "✅ Conventional commit message generated:\n\n"
|
|
||||||
result += cleaned
|
|
||||||
|
|
||||||
return mcp.NewToolResultText(result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleRunRecipe implements the run_recipe MCP tool.
|
|
||||||
func (s *Server) handleRunRecipe(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
name, err := req.RequireString("name")
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultText("Error: name is required"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("MCP run_recipe called", "recipe", name)
|
|
||||||
|
|
||||||
// Fully functional using internal/recipe package
|
|
||||||
r, err := recipe.Load(name, nil) // no custom params for now
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultText(fmt.Sprintf("Failed to load recipe '%s': %v", name, err)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
client := grok.NewClient()
|
|
||||||
runner := recipe.NewRunner(r, client, "grok-4")
|
|
||||||
|
|
||||||
err = runner.Run()
|
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultText(fmt.Sprintf("Recipe execution failed: %v", err)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := fmt.Sprintf("✅ Recipe '%s' executed successfully\n", name)
|
|
||||||
result += "Check the terminal output above for full details.\n"
|
|
||||||
|
|
||||||
return mcp.NewToolResultText(result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// registerResources registers MCP resources (recipes and prompts).
|
|
||||||
func (s *Server) registerResources() {
|
|
||||||
// Recipes as resources
|
|
||||||
s.mcpServer.AddResource(
|
|
||||||
mcp.NewResource("recipes://local", "Local Recipes",
|
|
||||||
mcp.WithResourceDescription("Project-local workflow recipes from .grokkit/recipes/"),
|
|
||||||
mcp.WithMIMEType("application/json"),
|
|
||||||
),
|
|
||||||
s.handleRecipesLocal,
|
|
||||||
)
|
|
||||||
|
|
||||||
s.mcpServer.AddResource(
|
|
||||||
mcp.NewResource("recipes://global", "Global Recipes",
|
|
||||||
mcp.WithResourceDescription("Global recipes from ~/.config/grokkit/recipes/"),
|
|
||||||
mcp.WithMIMEType("application/json"),
|
|
||||||
),
|
|
||||||
s.handleRecipesGlobal,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Prompts as resources
|
|
||||||
s.mcpServer.AddResource(
|
|
||||||
mcp.NewResource("prompts://language", "Language Prompts",
|
|
||||||
mcp.WithResourceDescription("Language-specific analysis prompts"),
|
|
||||||
mcp.WithMIMEType("text/markdown"),
|
|
||||||
),
|
|
||||||
s.handlePrompts,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.Info("MCP resources registered", "count", 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleRecipesLocal returns local recipes as a resource
|
|
||||||
func (s *Server) handleRecipesLocal(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
|
|
||||||
recipes, err := recipe.ListRecipes(".")
|
|
||||||
if err != nil {
|
|
||||||
recipes = []string{} // fallback to empty list
|
|
||||||
}
|
|
||||||
|
|
||||||
json := fmt.Sprintf(`{"type": "local", "recipes": %v}`, recipes)
|
|
||||||
|
|
||||||
return []mcp.ResourceContents{
|
|
||||||
mcp.TextResourceContents{
|
|
||||||
URI: "recipes://local",
|
|
||||||
MIMEType: "application/json",
|
|
||||||
Text: json,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleRecipesGlobal returns global recipes as a resource
|
|
||||||
func (s *Server) handleRecipesGlobal(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
|
|
||||||
home, _ := os.UserHomeDir()
|
|
||||||
globalPath := filepath.Join(home, ".config", "grokkit", "recipes")
|
|
||||||
|
|
||||||
recipes, err := recipe.ListRecipes(globalPath)
|
|
||||||
if err != nil {
|
|
||||||
recipes = []string{} // fallback to empty list
|
|
||||||
}
|
|
||||||
|
|
||||||
json := fmt.Sprintf(`{"type": "global", "recipes": %v}`, recipes)
|
|
||||||
|
|
||||||
return []mcp.ResourceContents{
|
|
||||||
mcp.TextResourceContents{
|
|
||||||
URI: "recipes://global",
|
|
||||||
MIMEType: "application/json",
|
|
||||||
Text: json,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handlePrompts returns available language prompts as a resource
|
|
||||||
func (s *Server) handlePrompts(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
|
|
||||||
promptsList := prompts.ListAvailablePrompts()
|
|
||||||
|
|
||||||
json := fmt.Sprintf(`{"type": "prompts", "prompts": %v}`, promptsList)
|
|
||||||
|
|
||||||
return []mcp.ResourceContents{
|
|
||||||
mcp.TextResourceContents{
|
|
||||||
URI: "prompts://language",
|
|
||||||
MIMEType: "application/json",
|
|
||||||
Text: json,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run starts the MCP server using stdio transport (for Claude Code, etc.).
|
|
||||||
func (s *Server) Run(ctx context.Context) error {
|
|
||||||
logger.Info("Starting Grokkit MCP server (stdio transport)")
|
|
||||||
return server.ServeStdio(s.mcpServer)
|
|
||||||
}
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
package mcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewServer(t *testing.T) {
|
|
||||||
srv, err := NewServer()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, srv)
|
|
||||||
assert.NotNil(t, srv.mcpServer)
|
|
||||||
assert.NotNil(t, srv.grokClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServer_RegistersTools(t *testing.T) {
|
|
||||||
srv, err := NewServer()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// We can't easily inspect registered tools without exposing internals,
|
|
||||||
// so we just verify server creation succeeds with tools.
|
|
||||||
assert.NotNil(t, srv)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleLintCode(t *testing.T) {
|
|
||||||
srv, _ := NewServer()
|
|
||||||
|
|
||||||
// Use a file that exists and is lintable
|
|
||||||
req := mockCallToolRequest(map[string]any{
|
|
||||||
"file_path": "main.go",
|
|
||||||
})
|
|
||||||
|
|
||||||
result, err := srv.handleLintCode(context.Background(), req)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, result)
|
|
||||||
// The test may fail if no linter is installed, so we just check it doesn't error
|
|
||||||
assert.NotNil(t, result.Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleAnalyzeCode(t *testing.T) {
|
|
||||||
srv, _ := NewServer()
|
|
||||||
|
|
||||||
req := mockCallToolRequest(map[string]any{
|
|
||||||
"dir": ".",
|
|
||||||
})
|
|
||||||
|
|
||||||
result, err := srv.handleAnalyzeCode(context.Background(), req)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, result)
|
|
||||||
// The test may fail if prompt files are missing, so we just check it doesn't error
|
|
||||||
assert.NotNil(t, result.Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleGenerateDocs(t *testing.T) {
|
|
||||||
srv, _ := NewServer()
|
|
||||||
|
|
||||||
req := mockCallToolRequest(map[string]any{
|
|
||||||
"file_path": "main.go",
|
|
||||||
})
|
|
||||||
|
|
||||||
result, err := srv.handleGenerateDocs(context.Background(), req)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, result)
|
|
||||||
// The test may fail if the file is not readable in test env, so we just check it doesn't error
|
|
||||||
assert.NotNil(t, result.Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to create mock CallToolRequest
|
|
||||||
func mockCallToolRequest(args map[string]any) mcp.CallToolRequest {
|
|
||||||
return mcp.CallToolRequest{
|
|
||||||
Params: mcp.CallToolParams{
|
|
||||||
Arguments: args,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
package prompts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ListAvailablePrompts returns a list of available language prompts.
|
|
||||||
// It scans both project-local and global prompt directories.
|
|
||||||
func ListAvailablePrompts() []string {
|
|
||||||
var prompts []string
|
|
||||||
|
|
||||||
// Check project-local prompts
|
|
||||||
localDir := ".grokkit/prompts"
|
|
||||||
if entries, err := os.ReadDir(localDir); err == nil {
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".md") {
|
|
||||||
name := strings.TrimSuffix(entry.Name(), ".md")
|
|
||||||
prompts = append(prompts, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check global prompts
|
|
||||||
home := os.Getenv("HOME")
|
|
||||||
if home == "" {
|
|
||||||
home = os.Getenv("USERPROFILE")
|
|
||||||
}
|
|
||||||
if home != "" {
|
|
||||||
globalDir := filepath.Join(home, ".config", "grokkit", "prompts")
|
|
||||||
if entries, err := os.ReadDir(globalDir); err == nil {
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".md") {
|
|
||||||
name := strings.TrimSuffix(entry.Name(), ".md")
|
|
||||||
// Avoid duplicates
|
|
||||||
found := false
|
|
||||||
for _, p := range prompts {
|
|
||||||
if p == name {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
prompts = append(prompts, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return prompts
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
package recipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ListRecipes returns a list of recipe names in the given directory.
|
|
||||||
// It looks for .md files that contain recipe frontmatter.
|
|
||||||
func ListRecipes(dir string) ([]string, error) {
|
|
||||||
if dir == "" {
|
|
||||||
dir = "."
|
|
||||||
}
|
|
||||||
|
|
||||||
var recipes []string
|
|
||||||
|
|
||||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.IsDir() {
|
|
||||||
// Skip hidden directories and common ignore dirs
|
|
||||||
name := info.Name()
|
|
||||||
if strings.HasPrefix(name, ".") && name != "." || name == "node_modules" || name == "vendor" {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(strings.ToLower(info.Name()), ".md") {
|
|
||||||
// Simple check for recipe files (contains "Objective:" or "Instructions:")
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err == nil && (strings.Contains(string(data), "**Objective:**") || strings.Contains(string(data), "Objective:")) {
|
|
||||||
name := strings.TrimSuffix(info.Name(), ".md")
|
|
||||||
recipes = append(recipes, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return recipes, err
|
|
||||||
}
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
package testgen
|
|
||||||
|
|
||||||
// GetTestPrompt returns the appropriate test prompt for the given language.
|
|
||||||
// Exported for use by MCP server.
|
|
||||||
func GetTestPrompt(lang string) string {
|
|
||||||
switch lang {
|
|
||||||
case "Go":
|
|
||||||
return `You are an expert Go test writer.
|
|
||||||
|
|
||||||
Generate a COMPLETE, production-ready *_test.go file using ONLY idiomatic Go.
|
|
||||||
|
|
||||||
STRICT RULES — NEVER VIOLATE THESE:
|
|
||||||
- NO monkey-patching: never assign to runtime.GOOS, exec.Command, os.Stdout, os.Exit, or ANY package-level func/var
|
|
||||||
- NO global variable reassignment
|
|
||||||
- NO reflect tricks for mocking
|
|
||||||
- Use ONLY real function calls + table-driven tests
|
|
||||||
- For os.Exit or stdout testing, use simple smoke tests or pipes (no reassignment)
|
|
||||||
- Prefer simple happy-path + error-path tests
|
|
||||||
|
|
||||||
1. Fast unit test:
|
|
||||||
func TestXXX_Unit(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
t.Log("✓ Fast XXX unit test")
|
|
||||||
// table-driven, real calls only
|
|
||||||
}
|
|
||||||
|
|
||||||
2. Optional live test:
|
|
||||||
func TestXXX_Live(t *testing.T) {
|
|
||||||
if !testing.Short() {
|
|
||||||
t.Skip("skipping live integration test. Run with:\n go test -run TestXXX_Live -short -v")
|
|
||||||
}
|
|
||||||
t.Log("🧪 Running live integration test...")
|
|
||||||
}
|
|
||||||
|
|
||||||
Exact rules:
|
|
||||||
- Derive test names from functions (e.g. isInstalled → TestIsInstalled_Unit)
|
|
||||||
- The XXX in t.Skip MUST exactly match the live function name
|
|
||||||
- t.Parallel() on unit tests only
|
|
||||||
- NO unused imports
|
|
||||||
- Keep tests simple and idiomatic Go
|
|
||||||
- Return ONLY the full test file. No explanations, no markdown, no backticks.`
|
|
||||||
|
|
||||||
case "Python":
|
|
||||||
return `You are a pytest expert. Generate COMPLETE pytest unit tests for the Python source.
|
|
||||||
|
|
||||||
- Use pytest fixtures where appropriate
|
|
||||||
- @pytest.mark.parametrize for tables
|
|
||||||
- Cover ALL functions/classes/methods: happy/edge/error cases
|
|
||||||
- pytest.raises for exceptions
|
|
||||||
- Modern Python 3.12+: type hints, match/case if applicable
|
|
||||||
- NO external deps unless source requires
|
|
||||||
|
|
||||||
Respond ONLY with full test_*.py file: imports, fixtures, tests. Pure Python test code.`
|
|
||||||
|
|
||||||
case "C":
|
|
||||||
return `You are a C testing expert using the Check framework.
|
|
||||||
|
|
||||||
Generate COMPLETE test code for the given C source.
|
|
||||||
|
|
||||||
- Use Check framework (check.h)
|
|
||||||
- Test all major functions
|
|
||||||
- Include setup/teardown if needed
|
|
||||||
- Cover edge cases and errors
|
|
||||||
- Return ONLY the full test file.`
|
|
||||||
|
|
||||||
case "C++":
|
|
||||||
return `You are a C++ testing expert using Google Test.
|
|
||||||
|
|
||||||
Generate COMPLETE Google Test code for the given C++ source.
|
|
||||||
|
|
||||||
- Use TEST() and TEST_F() macros
|
|
||||||
- Test all major functions and classes
|
|
||||||
- Cover edge cases and errors
|
|
||||||
- Return ONLY the full test file.`
|
|
||||||
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
package testgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetTestPrompt(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
lang string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{"Go", "expert Go test writer"},
|
|
||||||
{"Python", "pytest expert"},
|
|
||||||
{"C", "Check framework"},
|
|
||||||
{"C++", "Google Test"},
|
|
||||||
{"Unknown", ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.lang, func(t *testing.T) {
|
|
||||||
prompt := GetTestPrompt(tt.lang)
|
|
||||||
if tt.expected == "" {
|
|
||||||
assert.Empty(t, prompt)
|
|
||||||
} else {
|
|
||||||
assert.Contains(t, prompt, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -16,7 +16,6 @@ This document provides a table of contents for all tasks and features currently
|
|||||||
* [10] [git-chglog.md](./queued/git-chglog.md) : grokkit agent git-chglog integration
|
* [10] [git-chglog.md](./queued/git-chglog.md) : grokkit agent git-chglog integration
|
||||||
* [11] [admin.md](./queued/admin.md) : [optional] grokkit admin tool (to show token usage and other admin-only features)
|
* [11] [admin.md](./queued/admin.md) : [optional] grokkit admin tool (to show token usage and other admin-only features)
|
||||||
* [12] [interactive-agent.md](./queued/interactive-agent.md) : [optional] Grokkit Interactive Agent
|
* [12] [interactive-agent.md](./queued/interactive-agent.md) : [optional] Grokkit Interactive Agent
|
||||||
* [13] [mcp-feature.md](./queued/mcp-feature.md) : grokkit MCP server mode
|
|
||||||
|
|
||||||
## Completed
|
## Completed
|
||||||
|
|
||||||
|
|||||||
@ -1,137 +0,0 @@
|
|||||||
# MCP Server Feature
|
|
||||||
|
|
||||||
**Description**: Add an MCP (Model Context Protocol) server mode to grokkit, exposing its capabilities as tools and resources for Claude Code and other MCP-compatible clients.
|
|
||||||
|
|
||||||
## Problem It Solves
|
|
||||||
|
|
||||||
Grokkit's AI-powered code operations (linting, docs, test generation, analysis) are locked behind the CLI. MCP integration lets external AI clients invoke them programmatically, composing grokkit's strengths with broader workflows.
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
- **Composability**: Claude Code (or any MCP client) can call grokkit tools mid-conversation.
|
|
||||||
- **No core changes**: New `grokkit mcp` command wraps existing functionality via interfaces.
|
|
||||||
- **Stdio transport**: No HTTP server to manage — Claude Code pipes stdio directly.
|
|
||||||
- **Recipes as resources**: MCP clients can browse and invoke workflow recipes.
|
|
||||||
- **Leverages existing architecture**: `AIClient` and `GitRunner` interfaces already support dependency injection.
|
|
||||||
|
|
||||||
## Architecture Notes
|
|
||||||
|
|
||||||
Grokkit is well-suited for this:
|
|
||||||
|
|
||||||
- **Interface-based design**: `AIClient` and `GitRunner` are already abstractions — inject real implementations into MCP tool handlers.
|
|
||||||
- **Clean package separation**: Each internal package (grok, git, linter, recipe, prompts) maps naturally to an MCP tool.
|
|
||||||
- **Cobra command tree**: Adding `cmd/mcp.go` is trivial.
|
|
||||||
- **Streaming caveat**: MCP tools return complete results, so use `StreamSilent` (captures without printing) rather than `Stream`.
|
|
||||||
- **File-writing tools** (`edit`, `workon`): Need careful scoping so the MCP client understands what changed.
|
|
||||||
|
|
||||||
## High-Level Implementation
|
|
||||||
|
|
||||||
### Phase 1: Foundation
|
|
||||||
|
|
||||||
1. **Add MCP Go SDK** (e.g., `github.com/mark3labs/mcp-go`) to `go.mod`.
|
|
||||||
2. **Create `internal/mcp/server.go`** — MCP server that registers tools and resources.
|
|
||||||
3. **Create `cmd/mcp.go`** — `grokkit mcp` command that starts the stdio server.
|
|
||||||
|
|
||||||
### Phase 2: Tools
|
|
||||||
|
|
||||||
Wrap existing functionality as MCP tools. Priority order:
|
|
||||||
|
|
||||||
| Tool | Wraps | Notes |
|
|
||||||
|------|-------|-------|
|
|
||||||
| `lint_code` | `internal/linter` | Stateless. File path in, results out. |
|
|
||||||
| `generate_commit_msg` | `cmd/commit.go` logic | Reads git diff, calls Grok, returns message. |
|
|
||||||
| `analyze_code` | `cmd/analyze.go` + `internal/prompts` | File in, analysis out. |
|
|
||||||
| `generate_docs` | `cmd/docs.go` | File in, docs out (9 styles available). |
|
|
||||||
| `generate_tests` | `cmd/testgen.go` | File in, tests out. |
|
|
||||||
| `run_recipe` | `internal/recipe` | Recipe name + params in, results out. |
|
|
||||||
|
|
||||||
Each tool handler:
|
|
||||||
- Extracts core logic from the Cobra `RunE` function into a reusable function.
|
|
||||||
- Injects real `AIClient` and `GitRunner` implementations.
|
|
||||||
- Returns complete text results (no streaming to client).
|
|
||||||
|
|
||||||
### Phase 3: Resources
|
|
||||||
|
|
||||||
| Resource | Source | Description |
|
|
||||||
|----------|--------|-------------|
|
|
||||||
| `recipes://local` | `.grokkit/recipes/` | Project-local workflow recipes |
|
|
||||||
| `recipes://global` | `~/.local/share/grokkit/recipes/` | Global recipes |
|
|
||||||
| `prompts://language` | `.grokkit/prompts/` | Language-specific analysis prompts |
|
|
||||||
|
|
||||||
## Flags / Config
|
|
||||||
|
|
||||||
| Key | Description |
|
|
||||||
|-----|-------------|
|
|
||||||
| `mcp.enabled` | Enable MCP server mode |
|
|
||||||
| `mcp.tools` | List of tools to expose (default: all) |
|
|
||||||
| `mcp.resources` | List of resources to expose (default: all) |
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
- **Entry point**: `grokkit mcp` starts stdio server, blocks until EOF.
|
|
||||||
- **No breaking changes**: Entirely additive — new command, new package.
|
|
||||||
- **Testing**: Mock `AIClient` and `GitRunner` interfaces; table-driven tests per tool.
|
|
||||||
- **Effort**: Medium (~300-500 LOC). SDK does the protocol heavy lifting.
|
|
||||||
- **Prereqs**: Choose and evaluate a Go MCP SDK.
|
|
||||||
|
|
||||||
## ROI
|
|
||||||
|
|
||||||
**High**. Turns grokkit from a standalone CLI into a composable service:
|
|
||||||
|
|
||||||
| Capability | Standalone | With MCP |
|
|
||||||
|------------|-----------|----------|
|
|
||||||
| Lint | `grokkit lint` | Claude calls it mid-review |
|
|
||||||
| Docs | `grokkit docs` | Claude generates docs in context |
|
|
||||||
| Tests | `grokkit testgen` | Claude generates + validates tests |
|
|
||||||
| Recipes | `grokkit recipe` | Claude orchestrates multi-step workflows |
|
|
||||||
| Analysis | `grokkit analyze` | Claude gets language-aware code analysis |
|
|
||||||
## Work Plan
|
|
||||||
|
|
||||||
1. **Evaluate and add MCP SDK**
|
|
||||||
- Research Go MCP SDKs (`github.com/mark3labs/mcp-go` or equivalent).
|
|
||||||
- Add to `go.mod`, run `go mod tidy`.
|
|
||||||
- Commit: `feat(mcp): add MCP SDK dependency`.
|
|
||||||
|
|
||||||
2. **Implement MCP server foundation** (`internal/mcp`)
|
|
||||||
- Create `internal/mcp/server.go`: Initialize stdio server, register tools/resources.
|
|
||||||
- Inject real `AIClient` and `GitRunner` impls.
|
|
||||||
- Use `StreamSilent` for non-streaming results.
|
|
||||||
- Commit: `feat(mcp): add MCP server foundation`.
|
|
||||||
|
|
||||||
3. **Add `cmd/mcp.go`**
|
|
||||||
- Cobra command `grokkit mcp` to start stdio server.
|
|
||||||
- Add config flags: `--tools`, `--resources` (comma-separated).
|
|
||||||
- Blocks until EOF.
|
|
||||||
- Test: Manual `grokkit mcp` + simple MCP client.
|
|
||||||
- Commit: `feat(mcp): add grokkit mcp CLI command`.
|
|
||||||
|
|
||||||
4. **Extract reusable functions from existing commands**
|
|
||||||
- `lint_code`: Extract from `cmd/lint.go` → `internal/linter.RunLint(path) string`.
|
|
||||||
- `generate_commit_msg`: Extract from `cmd/commit.go` → `internal/git.GenerateCommitMsg() string`.
|
|
||||||
- Commit per extraction: `refactor(lint): extract reusable lint function`.
|
|
||||||
|
|
||||||
5. **Implement Phase 2 tools** (priority order, 1 commit per 2 tools)
|
|
||||||
- Register each as MCP tool in `server.go`.
|
|
||||||
- `lint_code`, `analyze_code`.
|
|
||||||
- `generate_docs`, `generate_tests`.
|
|
||||||
- `generate_commit_msg`, `run_recipe`.
|
|
||||||
- Table-driven unit tests with mocked `AIClient`/`GitRunner`.
|
|
||||||
- Commit: `feat(mcp): add lint_code and analyze_code tools`.
|
|
||||||
|
|
||||||
6. **Implement Phase 3 resources**
|
|
||||||
- `recipes://local`, `recipes://global`, `prompts://language`.
|
|
||||||
- List directories and contents as MCP resources.
|
|
||||||
- Unit tests for path resolution and listing.
|
|
||||||
- Commit: `feat(mcp): add recipe and prompt resources`.
|
|
||||||
|
|
||||||
7. **Add config flags and validation**
|
|
||||||
- Support `mcp.enabled`, `mcp.tools`, `mcp.resources` in config.
|
|
||||||
- Validate tool/resource lists on startup.
|
|
||||||
- Integration test: Full `grokkit mcp` run with subset tools.
|
|
||||||
- Commit: `feat(mcp): add config flags and validation`.
|
|
||||||
|
|
||||||
8. **End-to-end testing and docs**
|
|
||||||
- Test with Claude Code or MCP client simulator.
|
|
||||||
- Add `README.md` section: "MCP Server Mode".
|
|
||||||
- Smoke test all tools/resources.
|
|
||||||
- Final commit: `feat(mcp): complete MCP server + docs/tests`.
|
|
||||||
Loading…
Reference in New Issue
Block a user