Files
kit/cmd/hooks.go
T
Ed Zynda 63704f55b5 godoc
2025-11-12 16:48:46 +03:00

182 lines
5.8 KiB
Go

package cmd
import (
"fmt"
"os"
"text/tabwriter"
"github.com/mark3labs/mcphost/internal/hooks"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
// hooksCmd represents the hooks command for managing MCPHost hook configurations.
// Hooks allow users to execute custom scripts or commands at various points
// during MCPHost execution, such as before/after tool use or when prompts are submitted.
var hooksCmd = &cobra.Command{
Use: "hooks",
Short: "Manage MCPHost hooks",
Long: "Commands for managing and testing MCPHost hooks configuration",
}
// hooksListCmd represents the list subcommand for displaying all configured hooks.
// It shows a formatted table of hook events, matchers, commands, and timeouts
// to help users understand their current hook configuration.
var hooksListCmd = &cobra.Command{
Use: "list",
Short: "List all configured hooks",
RunE: func(cmd *cobra.Command, args []string) error {
config, err := hooks.LoadHooksConfig()
if err != nil {
return fmt.Errorf("loading hooks config: %w", err)
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "EVENT\tMATCHER\tCOMMAND\tTIMEOUT")
for event, matchers := range config.Hooks {
for _, matcher := range matchers {
for _, hook := range matcher.Hooks {
timeout := "60s"
if hook.Timeout > 0 {
timeout = fmt.Sprintf("%ds", hook.Timeout)
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
event, matcher.Matcher, hook.Command, timeout)
}
}
}
return w.Flush()
},
}
// hooksValidateCmd represents the validate subcommand for checking hook configuration validity.
// It loads and validates the hooks configuration file, ensuring proper syntax,
// valid event types, and correct matcher patterns before use.
var hooksValidateCmd = &cobra.Command{
Use: "validate",
Short: "Validate hooks configuration",
RunE: func(cmd *cobra.Command, args []string) error {
config, err := hooks.LoadHooksConfig()
if err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Additional validation
if err := hooks.ValidateHookConfig(config); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
fmt.Println("✓ Hooks configuration is valid")
return nil
},
}
// hooksInitCmd represents the init subcommand for generating an example hooks configuration.
// It creates a .mcphost/hooks.yml file with sample hook configurations demonstrating
// various hook events and common use cases like logging commands and tool usage.
var hooksInitCmd = &cobra.Command{
Use: "init",
Short: "Generate example hooks configuration",
RunE: func(cmd *cobra.Command, args []string) error {
example := &hooks.HookConfig{
Hooks: map[hooks.HookEvent][]hooks.HookMatcher{
// PreToolUse - runs before any tool execution
hooks.PreToolUse: {
{
Matcher: "bash.*",
Hooks: []hooks.HookEntry{
{
Type: "command",
Command: `mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/mcphost/logs" && jq -r '"[" + (now | strftime("%Y-%m-%d %H:%M:%S")) + "] $ " + .tool_input.command' >> "${XDG_CONFIG_HOME:-$HOME/.config}/mcphost/logs/bash-commands.log"`,
Timeout: 5,
},
},
},
{
Matcher: ".*", // Log all tool usage
Hooks: []hooks.HookEntry{
{
Type: "command",
Command: `jq -c '{time: now | strftime("%Y-%m-%d %H:%M:%S"), event: "pre", tool: .tool_name, input: .tool_input}' >> "${XDG_CONFIG_HOME:-$HOME/.config}/mcphost/logs/all-tools.jsonl"`,
Timeout: 5,
},
},
},
},
// PostToolUse - runs after tool execution completes
hooks.PostToolUse: {
{
Matcher: "bash.*",
Hooks: []hooks.HookEntry{
{
Type: "command",
Command: `jq -c '{time: now | strftime("%Y-%m-%d %H:%M:%S"), cmd: .tool_input.command, exit: .tool_response._meta.exit, stdout: (.tool_response._meta.stdout | rtrimstr("\n") | .[0:100]), stderr: (.tool_response._meta.stderr | rtrimstr("\n"))}' >> "${XDG_CONFIG_HOME:-$HOME/.config}/mcphost/logs/bash-audit.jsonl"`,
Timeout: 5,
},
},
},
{
Matcher: "mcp__.*", // Log MCP tool responses
Hooks: []hooks.HookEntry{
{
Type: "command",
Command: `jq -c '{time: now | strftime("%Y-%m-%d %H:%M:%S"), tool: .tool_name, response_preview: (.tool_response | tostring | .[0:200])}' >> "${XDG_CONFIG_HOME:-$HOME/.config}/mcphost/logs/mcp-tools.jsonl"`,
Timeout: 5,
},
},
},
},
// UserPromptSubmit - runs when user submits a prompt
hooks.UserPromptSubmit: {
{
Hooks: []hooks.HookEntry{
{
Type: "command",
Command: `mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/mcphost/logs" && jq -r '"[" + (now | strftime("%Y-%m-%d %H:%M:%S")) + "] " + .prompt' >> "${XDG_CONFIG_HOME:-$HOME/.config}/mcphost/logs/prompts.log"`,
},
},
},
},
// Stop - runs when the main agent finishes responding
hooks.Stop: {
{
Hooks: []hooks.HookEntry{
{
Type: "command",
Command: `jq -r '"[" + (now | strftime("%Y-%m-%d %H:%M:%S")) + "] Session " + .session_id + " stopped"' >> "${XDG_CONFIG_HOME:-$HOME/.config}/mcphost/logs/sessions.log"`,
},
},
},
},
},
}
// Create .mcphost directory if it doesn't exist
if err := os.MkdirAll(".mcphost", 0755); err != nil {
return fmt.Errorf("creating .mcphost directory: %w", err)
}
// Write example configuration
data, err := yaml.Marshal(example)
if err != nil {
return fmt.Errorf("marshaling example: %w", err)
}
if err := os.WriteFile(".mcphost/hooks.yml", data, 0644); err != nil {
return fmt.Errorf("writing example: %w", err)
}
fmt.Println("Created .mcphost/hooks.yml with example configuration")
return nil
},
}
func init() {
rootCmd.AddCommand(hooksCmd)
hooksCmd.AddCommand(hooksListCmd)
hooksCmd.AddCommand(hooksValidateCmd)
hooksCmd.AddCommand(hooksInitCmd)
}