package mcp import ( "context" "encoding/json" "log/slog" "time" chassisauth "git.d-ma.be/mathias/mcp-chassis/auth" "git.d-ma.be/mathias/gitea-mcp/internal/auth" "git.d-ma.be/mathias/gitea-mcp/internal/gitea" ) // auditTargetKeys are the only argument values copied into the audit record: // they identify what a call acted on without copying what it wrote. Every // other argument (file content, issue bodies) is reduced to args_bytes so the // audit trail never becomes a second copy of repo content (gitea-mcp#64). var auditTargetKeys = []string{"owner", "repo", "number", "branch", "path", "tag"} // logToolCall emits one structured record per tools/call (OWASP MCP08). func logToolCall(ctx context.Context, name string, args json.RawMessage, start time.Time, err error) { attrs := []any{ "tool", name, "caller", auth.Caller(ctx), "args_bytes", len(args), "duration_ms", time.Since(start).Milliseconds(), } if tok, ok := gitea.TokenFromContext(ctx); ok { attrs = append(attrs, "token_fp", chassisauth.Fingerprint(tok)) } var parsed map[string]any if json.Unmarshal(args, &parsed) == nil { for _, k := range auditTargetKeys { if v, ok := parsed[k]; ok { attrs = append(attrs, k, v) } } } outcome := "ok" if err != nil { outcome = err.Error() } attrs = append(attrs, "outcome", outcome) slog.InfoContext(ctx, "mcp tool call", attrs...) }