claw-code/.guardrails/mcp-server/internal/mcp/server.go

889 lines
28 KiB
Go
Executable File

package mcp
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/thearchitectit/guardrail-mcp/internal/audit"
"github.com/thearchitectit/guardrail-mcp/internal/cache"
"github.com/thearchitectit/guardrail-mcp/internal/config"
"github.com/thearchitectit/guardrail-mcp/internal/database"
"github.com/thearchitectit/guardrail-mcp/internal/metrics"
"github.com/thearchitectit/guardrail-mcp/internal/models"
"github.com/thearchitectit/guardrail-mcp/internal/validation"
)
// MCPServer handles MCP protocol requests
type MCPServer struct {
mcpServer *server.MCPServer
db *database.DB
cache *cache.Cache
metrics *metrics.Metrics
audit *audit.AuditLogger
validator *validation.Engine
config *config.Config
visionTools *VisionTools
}
// NewServer creates a new MCP server instance
func NewServer(db *database.DB, cache *cache.Cache, metrics *metrics.Metrics, audit *audit.AuditLogger, validator *validation.Engine, cfg *config.Config) *MCPServer {
s := &MCPServer{
mcpServer: server.NewMCPServer(
"Guardrail Enforcement Server",
cfg.Version,
server.WithResourceCapabilities(true, true),
server.WithLogging(),
),
db: db,
cache: cache,
metrics: metrics,
audit: audit,
validator: validator,
config: cfg,
}
// Initialize vision tools if configured
if cfg.Vision.Enabled {
s.visionTools = NewVisionTools(cfg)
}
s.setupHandlers()
return s
}
func (s *MCPServer) setupHandlers() {
// Register tools
s.mcpServer.HandleListTools(func(ctx context.Context, cursor *string) (*mcp.ListToolsResult, error) {
tools := []mcp.Tool{
{
Name: "guardrail_init_session",
Description: "Initialize a new session with security parameters and session ID",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"user_id": map[string]interface{}{
"type": "string",
"description": "Unique identifier for the user",
},
"environment": map[string]interface{}{
"type": "string",
"description": "Target environment (development, staging, production)",
},
},
Required: []string{"user_id"},
},
},
{
Name: "guardrail_validate_bash",
Description: "Validate a bash command against security policies and prevention rules",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"command": map[string]interface{}{
"type": "string",
"description": "The bash command to validate",
},
"working_dir": map[string]interface{}{
"type": "string",
"description": "Current working directory",
},
},
Required: []string{"command"},
},
},
{
Name: "guardrail_validate_file_edit",
Description: "Validate a file edit operation (search and replace) against safety rules",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"file_path": map[string]interface{}{
"type": "string",
"description": "Path to the file being edited",
},
"old_string": map[string]interface{}{
"type": "string",
"description": "Text to be replaced",
},
"new_string": map[string]interface{}{
"type": "string",
"description": "Replacement text",
},
},
Required: []string{"file_path", "old_string", "new_string"},
},
},
{
Name: "guardrail_validate_git_operation",
Description: "Validate a git operation (commit, push, branch) against policy",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"operation": map[string]interface{}{
"type": "string",
"description": "Git command to validate (e.g., commit, push)",
},
"args": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{"type": "string"},
"description": "Arguments to the git command",
},
},
Required: []string{"operation"},
},
},
{
Name: "guardrail_pre_work_check",
Description: "Perform a mandatory pre-work safety check before starting a new task",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"task_description": map[string]interface{}{
"type": "string",
"description": "Brief description of the planned task",
},
},
Required: []string{"task_description"},
},
},
{
Name: "guardrail_get_context",
Description: "Get the current active guardrail context and applicable rules",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"path": map[string]interface{}{
"type": "string",
"description": "Current working directory or file path",
},
},
},
},
{
Name: "guardrail_validate_scope",
Description: "Verify if a file path is within authorized project scope",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"file_path": map[string]interface{}{
"type": "string",
"description": "Path to the file to validate",
},
"authorized_scope": map[string]interface{}{
"type": "string",
"description": "Root directory of the authorized scope",
},
},
Required: []string{"file_path"},
},
},
{
Name: "guardrail_validate_commit",
Description: "Validate proposed commit message and changed files",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"message": map[string]interface{}{
"type": "string",
"description": "Commit message to validate",
},
"files": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{"type": "string"},
"description": "List of files to be committed",
},
},
Required: []string{"message", "files"},
},
},
{
Name: "guardrail_prevent_regression",
Description: "Check if changes might reintroduce known bugs or violate strict patterns",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"file_path": map[string]interface{}{
"type": "string",
"description": "File being modified",
},
"changes": map[string]interface{}{
"type": "string",
"description": "Description or diff of planned changes",
},
},
Required: []string{"file_path", "changes"},
},
},
{
Name: "guardrail_check_test_prod_separation",
Description: "Enforce strict separation between test code and production code",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"file_path": map[string]interface{}{
"type": "string",
"description": "Path to the file being checked",
},
},
Required: []string{"file_path"},
},
},
{
Name: "guardrail_validate_push",
Description: "Pre-push validation of current branch status and health",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"branch": map[string]interface{}{
"type": "string",
"description": "Branch to be pushed",
},
"remote": map[string]interface{}{
"type": "string",
"description": "Remote name (e.g., origin)",
},
},
Required: []string{"branch"},
},
},
{
Name: "guardrail_record_file_read",
Description: "Record that a file has been read by the agent (Four Laws enforcement)",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"file_path": map[string]interface{}{
"type": "string",
"description": "Path to the file that was read",
},
},
Required: []string{"file_path"},
},
},
{
Name: "guardrail_record_attempt",
Description: "Record a tool use attempt for tracking progress/failure rates",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"tool_name": map[string]interface{}{
"type": "string",
"description": "Name of the tool being attempted",
},
"success": map[string]interface{}{
"type": "boolean",
"description": "Whether the attempt was successful",
},
"error_msg": map[string]interface{}{
"type": "string",
"description": "Error message if failed",
},
},
Required: []string{"tool_name", "success"},
},
},
{
Name: "guardrail_verify_file_read",
Description: "Verify a file has been read in current context before editing",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"file_path": map[string]interface{}{
"type": "string",
"description": "Path to the file to verify",
},
},
Required: []string{"file_path"},
},
},
{
Name: "guardrail_validate_three_strikes",
Description: "Check if current task has hit consecutive failure threshold",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"task_id": map[string]interface{}{
"type": "string",
"description": "Unique identifier for the current task",
},
},
},
},
{
Name: "guardrail_validate_exact_replacement",
Description: "Verify that strings for replacement exactly match target file content",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"file_path": map[string]interface{}{
"type": "string",
"description": "File path to check",
},
"target_string": map[string]interface{}{
"type": "string",
"description": "The string to find for replacement",
},
},
Required: []string{"file_path", "target_string"},
},
},
{
Name: "guardrail_reset_attempts",
Description: "Reset failure counters for a given task or tool",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"task_id": map[string]interface{}{
"type": "string",
"description": "ID of task to reset",
},
},
},
},
{
Name: "guardrail_check_uncertainty",
Description: "Force self-reflection when confidence in next step is low",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"current_plan": map[string]interface{}{
"type": "string",
"description": "Description of the current plan",
},
"uncertainty_reason": map[string]interface{}{
"type": "string",
"description": "Reason for uncertainty",
},
},
Required: []string{"current_plan", "uncertainty_reason"},
},
},
{
Name: "guardrail_check_halt_conditions",
Description: "Evaluate if current state requires manual human escalation",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"status": map[string]interface{}{
"type": "string",
"description": "Current system/task status",
},
},
},
},
{
Name: "guardrail_record_halt",
Description: "Record a system-forced halt event",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"reason": map[string]interface{}{
"type": "string",
"description": "Reason for the halt",
},
},
Required: []string{"reason"},
},
},
{
Name: "guardrail_acknowledge_halt",
Description: "Acknowledged a previously recorded halt to resume operation",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"halt_id": map[string]interface{}{
"type": "string",
"description": "ID of the halt being acknowledged",
},
},
Required: []string{"halt_id"},
},
},
{
Name: "guardrail_validate_production_first",
Description: "Ensure production changes are prioritized or isolated correctly",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"path": map[string]interface{}{
"type": "string",
"description": "Path being modified",
},
},
},
},
{
Name: "guardrail_detect_feature_creep",
Description: "Analyze if changes exceed original task scope",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"task_id": map[string]interface{}{
"type": "string",
"description": "Original task identifier",
},
"current_changes": map[string]interface{}{
"type": "string",
"description": "Diff or summary of changes so far",
},
},
Required: []string{"task_id", "current_changes"},
},
},
{
Name: "guardrail_verify_fixes_intact",
Description: "Ensure recent bugfixes haven't been regressed by new edits",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"bug_id": map[string]interface{}{
"type": "string",
"description": "Known bug ID or description",
},
"file_path": map[string]interface{}{
"type": "string",
"description": "File to check",
},
},
Required: []string{"bug_id", "file_path"},
},
},
{
Name: "guardrail_team_init",
Description: "Initialize a new project team with roles and rules",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"project_name": map[string]interface{}{
"type": "string",
"description": "Name of the project",
},
"teams": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{"type": "string"},
"description": "List of team names to initialize",
},
},
Required: []string{"project_name", "teams"},
},
},
{
Name: "guardrail_team_list",
Description: "List all active teams and their configurations",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"project_name": map[string]interface{}{
"type": "string",
"description": "Filter by project name",
},
},
},
},
{
Name: "guardrail_team_config_get",
Description: "Get detailed configuration for a specific team",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"project_name": map[string]interface{}{
"type": "string",
"description": "Name of the project",
},
"team_name": map[string]interface{}{
"type": "string",
"description": "Name of the team",
},
},
Required: []string{"project_name", "team_name"},
},
},
{
Name: "guardrail_team_config_update",
Description: "Update rules or roles for an existing team",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"project_name": map[string]interface{}{
"type": "string",
"description": "Name of the project",
},
"team_name": map[string]interface{}{
"type": "string",
"description": "Name of the team",
},
"config": map[string]interface{}{
"type": "object",
"description": "New configuration data",
},
},
Required: []string{"project_name", "team_name", "config"},
},
},
{
Name: "guardrail_advisor_list",
Description: "List all available AI advisors and their specialties",
InputSchema: mcp.ToolInputSchema{
Type: "object",
},
},
{
Name: "guardrail_advisor_query",
Description: "Ask a specialist AI advisor for guidance on a specific topic",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"advisor_name": map[string]interface{}{
"type": "string",
"description": "Name of the specialist advisor",
},
"query": map[string]interface{}{
"type": "string",
"description": "Your question or request",
},
},
Required: []string{"advisor_name", "query"},
},
},
{
Name: "guardrail_team_assign",
Description: "Assign a specific team member (AI advisor) to a project",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"project_name": map[string]interface{}{
"type": "string",
"description": "Project name",
},
"advisor_name": map[string]interface{}{
"type": "string",
"description": "Advisor to assign",
},
"role": map[string]interface{}{
"type": "string",
"description": "Specific project role",
},
},
Required: []string{"project_name", "advisor_name"},
},
},
{
Name: "guardrail_team_remove",
Description: "Remove a team or advisor assignment from a project",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"project_name": map[string]interface{}{
"type": "string",
"description": "Project name",
},
"team_id": map[string]interface{}{
"type": "number",
"description": "Team ID to delete (1-12)",
},
"confirmed": map[string]interface{}{
"type": "boolean",
"description": "Set to true to confirm deletion. First call without this to see confirmation prompt.",
},
},
},
},
{
Name: "guardrail_project_delete",
Description: "Delete an entire project and all its teams. Requires confirmation.",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"project_name": map[string]interface{}{
"type": "string",
"description": "Name of the project to delete",
},
"confirmed": map[string]interface{}{
"type": "boolean",
"description": "Set to true to confirm deletion. First call without this to see confirmation prompt.",
},
},
},
},
{
Name: "guardrail_team_health",
Description: "Check team_manager.py health status - validates Python backend and file system access",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"project_name": map[string]interface{}{
"type": "string",
"description": "Optional: Project name for config directory check",
},
},
},
},
{
Name: "guardrail_install_skills",
Description: "Install or clone guardrails skill configs. Use 'skill' for per-skill install/clone, 'platforms' for full platform install, or 'path' for single-file clone.",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: mcp.ToolInputSchemaProperties{
"target_path": map[string]interface{}{
"type": "string",
"description": "Target project directory path (default: current directory)",
},
"platforms": map[string]interface{}{
"type": "string",
"description": "Comma-separated list of platforms: claude, cursor, opencode, windsurf, copilot (default: all). Use with action=install.",
},
"skill": map[string]interface{}{
"type": "string",
"description": "Install a single skill by name (e.g. 'guardrails-enforcer', 'commit-validator', 'four-laws'). Use action=install. Run list_skills=true to see all.",
},
"path": map[string]interface{}{
"type": "string",
"description": "Clone a single file by repo path (e.g. '.claude/skills/guardrails-enforcer.json'). Downloads from GitHub raw. Use with action=clone.",
},
"action": map[string]interface{}{
"type": "string",
"description": "Action to perform: 'install' (default), 'clone' (download from GitHub), 'list' (list skills/platforms)",
"enum": []string{"install", "clone", "list"},
},
"list_skills": map[string]interface{}{
"type": "boolean",
"description": "List all available skills and exit",
},
"list_platforms": map[string]interface{}{
"type": "boolean",
"description": "List all available platforms and exit",
},
"mode": map[string]interface{}{
"type": "string",
"description": "Installation mode: 'copy' or 'symlink' (default: copy). Applies to action=install.",
"enum": []string{"copy", "symlink"},
},
"dry_run": map[string]interface{}{
"type": "boolean",
"description": "Preview what would be done without making changes (default: false)",
},
},
},
},
}
if s.visionTools != nil {
tools = append(tools, s.visionTools.visionToolList()...)
}
return &mcp.ListToolsResult{
Tools: tools,
}, nil
})
// Handle tool calls
s.mcpServer.HandleCallTool(func(ctx context.Context, name string, arguments map[string]interface{}) (*mcp.CallToolResult, error) {
// Try vision tools first if enabled
if s.visionTools != nil {
result, err := s.visionTools.dispatch(ctx, name, arguments)
if err == nil {
return result, nil
}
}
return s.handleToolCall(ctx, name, arguments)
})
// Handle resource list requests
s.mcpServer.HandleListResources(func(ctx context.Context, cursor *string) (*mcp.ListResourcesResult, error) {
return &mcp.ListResourcesResult{
Resources: []mcp.Resource{
{
URI: "guardrail://config",
Name: "Guardrail Configuration",
},
{
URI: "guardrail://stats",
Name: "Guardrail Usage Stats",
},
},
}, nil
})
// Handle resource read requests
s.mcpServer.HandleReadResource(func(ctx context.Context, uri string) (*mcp.ReadResourceResult, error) {
if uri == "guardrail://config" {
configJSON, _ := json.MarshalIndent(s.config, "", " ")
return &mcp.ReadResourceResult{
Contents: []mcp.ResourceContent{
{
URI: uri,
MimeType: "application/json",
Text: string(configJSON),
},
},
}, nil
}
return nil, fmt.Errorf("resource not found: %s", uri)
})
}
func (s *MCPServer) handleToolCall(ctx context.Context, name string, args map[string]interface{}) (*mcp.CallToolResult, error) {
slog.Info("Tool call received", "name", name, "args", args)
switch name {
case "guardrail_init_session":
return s.handleInitSession(ctx, args)
case "guardrail_validate_bash":
return s.handleValidateBash(ctx, args)
case "guardrail_validate_file_edit":
return s.handleValidateFileEdit(ctx, args)
case "guardrail_validate_git_operation":
return s.handleValidateGitOperation(ctx, args)
case "guardrail_pre_work_check":
return s.handlePreWorkCheck(ctx, args)
case "guardrail_get_context":
return s.handleGetContext(ctx, args)
case "guardrail_validate_scope":
return s.handleValidateScope(ctx, args)
case "guardrail_validate_commit":
return s.handleValidateCommit(ctx, args)
case "guardrail_prevent_regression":
return s.handlePreventRegression(ctx, args)
case "guardrail_check_test_prod_separation":
return s.handleCheckTestProdSeparation(ctx, args)
case "guardrail_validate_push":
return s.handleValidatePush(ctx, args)
case "guardrail_record_file_read":
return s.handleRecordFileRead(ctx, args)
case "guardrail_record_attempt":
return s.handleRecordAttempt(ctx, args)
case "guardrail_verify_file_read":
return s.handleVerifyFileRead(ctx, args)
case "guardrail_validate_three_strikes":
return s.handleValidateThreeStrikes(ctx, args)
case "guardrail_validate_exact_replacement":
return s.handleValidateExactReplacement(ctx, args)
case "guardrail_reset_attempts":
return s.handleResetAttempts(ctx, args)
case "guardrail_check_uncertainty":
return s.handleCheckUncertainty(ctx, args)
case "guardrail_check_halt_conditions":
return s.handleCheckHaltConditions(ctx, args)
case "guardrail_record_halt":
return s.handleRecordHalt(ctx, args)
case "guardrail_acknowledge_halt":
return s.handleAcknowledgeHalt(ctx, args)
case "guardrail_validate_production_first":
return s.handleValidateProductionFirst(ctx, args)
case "guardrail_detect_feature_creep":
return s.handleDetectFeatureCreep(ctx, args)
case "guardrail_verify_fixes_intact":
return s.handleVerifyFixesIntact(ctx, args)
case "guardrail_team_init":
return s.handleTeamInit(ctx, args)
case "guardrail_team_list":
return s.handleTeamList(ctx, args)
case "guardrail_team_config_get":
return s.handleTeamConfigGet(ctx, args)
case "guardrail_team_config_update":
return s.handleTeamConfigUpdate(ctx, args)
case "guardrail_advisor_list":
return s.handleAdvisorList(ctx, args)
case "guardrail_advisor_query":
return s.handleAdvisorQuery(ctx, args)
case "guardrail_team_assign":
return s.handleTeamAssign(ctx, args)
case "guardrail_team_remove":
return s.handleTeamRemove(ctx, args)
case "guardrail_project_delete":
return s.handleProjectDelete(ctx, args)
case "guardrail_team_health":
return s.handleTeamHealth(ctx, args)
case "guardrail_install_skills":
return s.handleInstallSkills(ctx, args)
default:
return nil, fmt.Errorf("unknown tool: %s", name)
}
}
// buildToolResult is a helper to centralize formatting of MCP tool returns
func buildToolResult(data interface{}, isJson bool) (*mcp.CallToolResult, error) {
var text string
if isJson {
j, _ := json.MarshalIndent(data, "", " ")
text = string(j)
} else {
text = fmt.Sprintf("%v", data)
}
return &mcp.CallToolResult{
Content: []mcp.CallToolContent{
{
Type: "text",
Text: text,
},
},
}, nil
}
func (s *MCPServer) handleInitSession(ctx context.Context, args map[string]interface{}) (*mcp.CallToolResult, error) {
userID, _ := args["user_id"].(string)
env, _ := args["environment"].(string)
token := make([]byte, 8)
rand.Read(token)
sessionID := hex.EncodeToString(token)
result := models.SessionInfo{
SessionID: sessionID,
UserID: userID,
Environment: env,
StartTime: time.Now(),
}
return buildToolResult(result, true)
}
// Serve HTTP requests (SSE for MCP)
func (s *MCPServer) Serve(addr string) error {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/mcp", func(c echo.Context) error {
s.mcpServer.HandleSSE(c.Response().Writer, c.Request())
return nil
})
e.POST("/mcp", func(c echo.Context) error {
s.mcpServer.HandleSSE(c.Response().Writer, c.Request())
return nil
})
return e.Start(addr)
}
func (s *MCPServer) handleGetContext(ctx context.Context, args map[string]interface{}) (*mcp.CallToolResult, error) {
path, _ := args["path"].(string)
if path == "" {
path, _ = os.Getwd()
}
rules := s.validator.GetRulesForPath(ctx, path)
result := map[string]interface{}{
"path": path,
"applicable_rules": rules,
"timestamp": time.Now().Format(time.RFC3339),
}
return buildToolResult(result, true)
}