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
29 changes: 29 additions & 0 deletions cmd/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"context"
"encoding/json"
"fmt"
"os"
"os/signal"
Expand Down Expand Up @@ -66,6 +67,34 @@ Use --socket to serve over a Unix domain socket (future).`,
}

cmd.Flags().StringVar(&socketPath, "socket", "", "Unix socket path for TUI+MCP mode (not yet implemented)")
cmd.AddCommand(newMCPToolsCmd())

return cmd
}

const mcpToolsUse = "tools"

func newMCPToolsCmd() *cobra.Command {
var jsonOut bool

cmd := &cobra.Command{
Use: mcpToolsUse,
Short: "List grut MCP tools",
RunE: func(cmd *cobra.Command, args []string) error {
tools := grut_mcp.ToolInventory()
w := cmd.OutOrStdout()
if jsonOut {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(tools)
}
fmt.Fprintf(w, "%-24s %-6s %s\n", "NAME", "TYPE", "DESCRIPTION")
for _, tool := range tools {
fmt.Fprintf(w, "%-24s %-6s %s\n", tool.Name, tool.Category, tool.Description)
}
return nil
},
}
cmd.Flags().BoolVar(&jsonOut, "json", false, "Print tools as JSON")
return cmd
}
40 changes: 40 additions & 0 deletions cmd/mcp_tools_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cmd

import (
"bytes"
"encoding/json"
"testing"

grut_mcp "github.com/jongio/grut/internal/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestMCPToolsCommandListsTools(t *testing.T) {
cmd := newMCPCmd()
cmd.SetArgs([]string{"tools"})
var out bytes.Buffer
cmd.SetOut(&out)

err := cmd.Execute()

require.NoError(t, err)
assert.Contains(t, out.String(), "NAME")
assert.Contains(t, out.String(), "git_status")
assert.Contains(t, out.String(), "file_read")
}

func TestMCPToolsCommandJSON(t *testing.T) {
cmd := newMCPCmd()
cmd.SetArgs([]string{"tools", "--json"})
var out bytes.Buffer
cmd.SetOut(&out)

err := cmd.Execute()

require.NoError(t, err)
var tools []grut_mcp.ToolInfo
require.NoError(t, json.Unmarshal(out.Bytes(), &tools))
assert.NotEmpty(t, tools)
assert.Equal(t, "git_status", tools[0].Name)
}
29 changes: 29 additions & 0 deletions internal/mcp/inventory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package mcp

import (
"slices"

mcpserver "github.com/mark3labs/mcp-go/server"
)

type ToolInfo struct {
Name string `json:"name"`
Category string `json:"category"`
Description string `json:"description"`
}

func ToolInventory() []ToolInfo {
s := &Server{
mcp: mcpserver.NewMCPServer("grut", "inventory"),
}
registerGitReadTools(s)
registerGitWriteTools(s)
registerGitOpsTools(s)
registerFileTools(s)
return s.Tools()
}

func (s *Server) Tools() []ToolInfo {
out := slices.Clone(s.tools)
return out
}
34 changes: 34 additions & 0 deletions internal/mcp/inventory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package mcp

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestToolInventoryIncludesReadWriteAndFileTools(t *testing.T) {
tools := ToolInventory()
require.NotEmpty(t, tools)

byName := make(map[string]ToolInfo, len(tools))
for _, tool := range tools {
byName[tool.Name] = tool
}

assert.Equal(t, categoryRead, byName["git_status"].Category)
assert.Equal(t, categoryWrite, byName["git_stage"].Category)
assert.Equal(t, categoryRead, byName["file_read"].Category)
assert.Equal(t, categoryWrite, byName["file_write"].Category)
assert.NotEmpty(t, byName["git_status"].Description)
}

func TestServerToolsReturnsCopy(t *testing.T) {
tools := ToolInventory()
require.NotEmpty(t, tools)

tools[0].Name = "changed"
again := ToolInventory()

assert.NotEqual(t, "changed", again[0].Name)
}
6 changes: 6 additions & 0 deletions internal/mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Server struct {
limiter *RateLimiter
audit *AuditLogger
repoRoot string
tools []ToolInfo
}

// Default rate limits (calls per minute) when not configured.
Expand Down Expand Up @@ -92,6 +93,11 @@ func (s *Server) MCPServer() *mcpserver.MCPServer {

// addTool registers a tool with security middleware wrapping.
func (s *Server) addTool(name string, category string, tool mcplib.Tool, handler mcpserver.ToolHandlerFunc) {
s.tools = append(s.tools, ToolInfo{
Name: name,
Category: category,
Description: tool.Description,
})
s.mcp.AddTool(tool, s.wrapHandler(name, category, handler))
}

Expand Down
Loading