internal/mcp/hooks.go
1
package mcp
3
import (
4
"context"
5
"encoding/json"
6
"fmt"
7
"strings"
9
sdk "github.com/modelcontextprotocol/go-sdk/mcp"
11
"github.com/koment-dev/koment/internal/agentpolicy"
12
)
14
const preToolDescription = "Run the koment pre-tool hook against an edit or write intent. " +
15
"Returns a decision: 'allow' when the proposed content does not add ordinary comment intent, " +
16
"'deny' when it does, with the reason naming the offending file and line. " +
17
"Use this from a client plugin to gate tool calls the same way `koment agents hook pre-tool` does."
19
type PreToolInput struct {
20
ToolName string `json:"tool_name" jsonschema:"the agent's tool name; recognised values are apply_patch and opencode_edit"`
21
FilePath string `json:"filePath,omitempty" jsonschema:"for opencode_edit, the path the tool would write"`
22
Content string `json:"content,omitempty" jsonschema:"for opencode_edit, the content the tool would write"`
23
Command string `json:"command,omitempty" jsonschema:"for apply_patch, the patch command body"`
24
}
26
type PreToolOutput struct {
27
Decision string `json:"decision"`
28
Reason string `json:"reason,omitempty"`
29
}
31
func preTool(_ context.Context, _ *sdk.CallToolRequest, input PreToolInput) (*sdk.CallToolResult, PreToolOutput, error) {
32
payload, err := json.Marshal(map[string]any{
33
"tool_name": input.ToolName,
34
"tool_input": map[string]any{
35
"command": input.Command,
36
"filePath": input.FilePath,
37
"content": input.Content,
38
},
39
})
40
if err != nil {
41
return nil, PreToolOutput{}, fmt.Errorf("encoding pre-tool input: %w", err)
42
}
43
raw, err := agentpolicy.PreToolOutput(payload)
44
if err != nil {
45
return nil, PreToolOutput{}, err
46
}
47
trimmed := strings.TrimSpace(string(raw))
48
if trimmed == "" || trimmed == "{}" {
49
return nil, PreToolOutput{Decision: "allow"}, nil
50
}
51
var decoded struct {
52
HookSpecificOutput struct {
53
PermissionDecision string `json:"permissionDecision"`
54
PermissionDecisionReason string `json:"permissionDecisionReason"`
55
} `json:"hookSpecificOutput"`
56
}
57
if err := json.Unmarshal(raw, &decoded); err != nil {
58
return nil, PreToolOutput{}, fmt.Errorf("decoding pre-tool output: %w", err)
59
}
60
decision := "allow"
61
reason := decoded.HookSpecificOutput.PermissionDecisionReason
62
if strings.EqualFold(decoded.HookSpecificOutput.PermissionDecision, "deny") {
63
decision = "deny"
64
}
65
return nil, PreToolOutput{Decision: decision, Reason: reason}, nil
66
}